Skip to content
个人作品推荐
栾媛爱动物
播放动物叫声趣味微信小程序
栾媛爱动物微信小程序
微信扫码体验

日志插件(usage_log)业务说明

文档地图(usage_log 相关)

文档读它当你需要…
本文业务:目标、流程、环境变量、附件、API、前端展示、限制。
插件开发.md机制ext 包、目录树、新建插件、表初始化、联调、附录。
usage_log与上游合并-源码入侵.md合并:主仓库非 ext 须保留的代码与 i18n。
与上游-Gitee-手动合并步骤.mdGit:与 Gitee 无共同祖先时的合并命令。

本文定位: ext/usage_log 做什么、怎么配(以本文为准);不写合并用的逐文件代码块(见上表第三列)。


概述

ext/usage_log 在网关中的主要作用,是把与单次中继请求相关的可读摘要写入独立表 usage_log_content,并通过 request_id 与主库 logs 关联,供管理端查询「使用日志」。

落库字段通常包括:系统提示词摘要用户问题模型回答摘要(具体解析与截断见下文)。不修改上游 logs / model.Log 等已有表结构;计费与主日志仍走原有链路。

本文档结构(与后文编号小节对应)

#主题说明
1目标与边界维护扩展日志、不改变主库表结构。
2接入顺序(与 Gin 行为相关)Gin 中间件注册顺序及对 capture 路径的影响。
3业务主流程(一次请求)从读 body 到写扩展表、关联附件的端到端步骤。
4–5请求侧逻辑 / 响应侧逻辑捕获路径、request_id、解析规则与流式/非流式响应提取。
6落库规则request_id 为键的 upsert 等行为。
7长度与截断(业务语义)落库前按配置截断长文本。
8配置项汇总(环境变量)环境变量与业务含义对照。
9附件(消息内链接 + 预上传文件)消息内链接、multipart 预上传与注入等;9.6 说明 Excel 表格与主仓 go.mod 中 excelize 的作用。
10只读 API扩展提供的查询接口。
11前端(可选扩展)管理端 Web 展示(扩展组件)。
12限制与已知情况使用边界与已知问题。
13源码索引关键源码路径速查。
14四份文档怎样配合插件开发源码侵入Gitee 合并 的分工。

本文旨在细化 usage_log 的用途与实现细节,并与《插件开发.md》形成互补。


1. 目标与边界

项目说明
目标在独立表 usage_log_content 中,按 request_id 与主库 logs 关联,额外保存 system 提示词摘要用户问题模型回答摘要,供管理端「使用日志」展示或接口查询。
不修改不改动上游 model.Log / logs 表结构;计费、主日志写入逻辑仍由网关原有链路完成。
数据库读写使用 model.LOG_DB(与主日志库一致);表由注册时 AutoMigrateext/table_init 脚本初始化。

2. 接入顺序(与 Gin 行为相关)

要点:ext.MustRegister 须在 router.SetRouter 之前ext.RegisterRelayMiddleware 须在 relay 侧解压之后、第一个 /v1 等子路由 Group 之前——否则捕获链进不了聊天接口。Session 须在 MustRegister 之前挂载(详见 插件开发 · 第 9.1 节)。

可复制的代码块(含 middleware/auth.goEnforceUserAuthgo.mod 等)见 usage_log与上游合并-源码入侵 · 后端 Go非 ext 文件一览;合并冲突时以该文 〔合并段〕 为准。


3. 业务主流程(一次请求)

请求进入 CaptureMiddleware
  → 总开关关闭? → 直接 Next()
  → 非 POST 或非 capture 路径? → 直接 Next()
  → 无 request_id(common.RequestIdKey)? → 直接 Next()
  → 读请求体,解析 system / prompt / stream(见第 4 节)
  → 还原 BodyStorage,与 common.KeyBodyStorage 对齐,供下游 relay 复用
  → 包装 ResponseWriter,执行 c.Next()(relay 处理)
  → c.Next() 后:若存在「有效 system」(KeyEffectiveSystemPrompt),覆盖 system 文本(见第 4.4 节)
  → 非流式:从完整响应 JSON 提取回答;流式:从缓冲的 SSE 拼接回答(见第 5 节)
  → 按 MaxPromptLen / MaxCompletionLen 截断(见第 7 节)
  → SaveContent:按 request_id upsert 到 usage_log_content
  → processAttachmentsFromBody:关联 usage_log_attachment_tokens、写入消息内 URL 附件

4. 请求侧逻辑

4.1 捕获路径(仅 POST)

当前固定为以下路径(与 IsCapturePath 一致):

  • /v1/chat/completions
  • /v1/completions
  • /v1/messages
  • /pg/chat/completions

4.2 依赖 request_id

中间件从 Gin Context 读取 common.RequestIdKey(与响应头 X-Oneapi-Request-Id、使用日志表 logs.request_id 一致)。若为空则不捕获、不写扩展表。

4.3 请求体解析

字段含义说明
systemmessagesrole=systemcontent 拼接多段 system 合并为多行文本;与 MaxPromptLen 截断后落 system_prompt_text
prompt / 问题非 system 的 messagescontent,或旧版 prompt 字段支持字符串与多模态数组中的 type:text;截断后落 prompt_text
stream是否流式除标准 true/false 外,兼容字符串 "true"/"false""1"/"0" 等,避免误判分支。

4.4 有效 system 提示词(渠道注入)

Relay 可能在处理过程中写入实际生效的 system 文本。中间件在 c.Next() 之后 若读到 KeyEffectiveSystemPrompt(定义在 ext/usage_log/keys.go),则用其替换此前从请求体解析的 system 摘要,再截断落库。


5. 响应侧逻辑

5.1 非流式(stream 为 false)

  • 使用 responseCaptureWriter 缓存 Write / WriteString 输出,并转发 Flush(保证 SSE 类实现不丢流)。
  • 响应体按 OpenAI 风格 JSON 解析回答,支持:
    • Chat:choices[0].messagecontent,以及 reasoning_content / reasoning(与 content 合并);
    • 旧版补全:choices[0].text

5.2 流式(stream 为 true)

  • 同样缓存完整 SSE 字节流,结束后按 data: 行解析 JSON。
  • 从每条 choices[0].delta 中拼接文本,字段包括 reasoning_contentreasoningcontenttext(兼容推理模型与旧补全流)。
  • RecordStreamCompletionfalse 时:仍写 system / 问题不写 completion_text(见配置表)。

5.3 与上游失败的关系

若 relay 未返回 200 或 body 非预期,则可能得到空回答;扩展仍可能写入 prompt 侧,completion 为空


6. 落库规则

规则说明
usage_log_content
唯一键request_id
写入方式SaveContent单条 upsert(冲突则更新正文与时间戳);CreatedAt 在更新时也会刷新。
字段system_prompt_textprompt_textcompletion_textcreated_at

7. 长度与截断(业务语义)

配置在 ext/usage_log/config.go 中加载,落库前在 TruncateUTF8 中执行(按 Unicode 码点 / rune,不是字节)。

配置项环境变量默认说明
问题侧上限LOG_EXT_MAX_PROMPT_LEN2000system 提示词用户问题各自分别截断到该长度
回答上限LOG_EXT_MAX_COMPLETION_LEN1000合并后的回答截断到该长度

特殊值:

  • LOG_EXT_*_LEN=0:与 TruncateUTF8 约定 maxLen <= 0 不截断(该维度不设上限)。
  • 未设置、解析失败或负数:回退到上表默认值(2000 / 1000)。

8. 配置项汇总(环境变量)

环境变量默认业务含义
LOG_EXT_RECORD_PROMPT_COMPLETIONtrue总开关false不写扩展表;但若 LOG_EXT_ATTACHMENT_ENABLED=true 且仍开启 LOG_EXT_ATTACHMENT_INJECT_TO_MESSAGESLOG_EXT_FILE_URL_FETCH_ENABLED,中间件仍会读 body 并做附件注入 / file_url 展开后再转发。
LOG_EXT_MAX_PROMPT_LEN2000问题侧(system 与 user 各自)最大记录长度(rune)。
LOG_EXT_MAX_COMPLETION_LEN1000回答最大记录长度(rune)。
LOG_EXT_RECORD_STREAM_COMPLETION未单独设置时,见下一行流式是否写入回答false 时流式只记 system/问题。
LOG_EXT_STREAM_ONLYtrue(在未设置上一项时生效)旧名,与 LOG_EXT_RECORD_STREAM_COMPLETION 同义;若同时设置了 LOG_EXT_RECORD_STREAM_COMPLETION以该新变量为准

.env 需在主程序加载后再执行 LoadConfig(见 Register 时机),否则进程启动时读不到变量。


9. 附件(消息内链接 + 预上传文件)

LOG_EXT_RECORD_PROMPT_COMPLETION=trueLOG_EXT_ATTACHMENT_ENABLED=true(默认) 时生效;附件元数据表 usage_log_attachment,文件体落在 LOG_EXT_ATTACHMENT_ROOT(默认 ./data/usage_log_attachments)。

9.1 消息内的链接(无需上传)

中间件在 SaveContent 之后 解析请求 JSON 的 messages:对 content 为数组时,收集

  • type: "image_url"image_url.url
  • type: "file"file.file_url(若存在)

同一请求内 URL 去重 后写入表,kind=urlsource_url 存完整链接;下载接口对 url 类型返回 302 到该地址(仅 http/https)。
说明:若启用了下文 9.1.1file_url 展开,附件采集仍使用展开前的 JSON,因此 file.file_url 仍会写入附件表。

9.1.1 file_url 网关侧拉取(无需本地上传)

LOG_EXT_FILE_URL_FETCH_ENABLED=true(默认)LOG_EXT_ATTACHMENT_ENABLED=true 时,在转发给上游 relay 之前,若 messages[].content 中存在 type: "file",且 file.file_urlhttp/https 绝对地址,且设置 file.file_id(或为空),网关会:

  1. 服务端 GET 该 URL(响应体积 ≤ LOG_EXT_ATTACHMENT_MAX_BYTES,并做内网/回环等 SSRF 限制);
  2. Content-Type 与内容:将 UTF-8 文本HTML(抽取可见文本)、JSON 等转为一段 {"type":"text","text":"..."}替换file 段再交给下游(行为上接近「外链图用 URL」:由网关侧代为取内容,客户端不必先上传文件);
  3. PDFContent-Type: application/pdf%PDF 魔数)当前不做服务端抽字,请求会 400,提示改用 文本/Markdown 公链或先上传取得 file_id

关闭本行为:LOG_EXT_FILE_URL_FETCH_ENABLED=false,则 file_url 展开,原样进入 relay(直连要求 file_id 的上游时仍可能报错)。

9.2 预上传文件(本地文件)

  1. 调用 POST /api/ext/attachment/upload,鉴权与 /v1 一致:TokenOrUserAuth —— 管理端会话Authorization: Bearer sk-...(与调用 chat 时同一密钥即可)。multipart 支持:

    • 字段名 files:一次选多个文件;或
    • 字段名 file:可重复多个 part(与 HTML <input type="file" multiple name="file"> 一致);
    • 仍兼容仅一个 file 的旧用法。
  2. 响应:

    • data.items:数组,每项含 pending_tokenfilenamesize
    • data.pending_tokens:全部 token 的字符串数组;
    • 若本次仅 1 个文件,仍返回 data.pending_token / data.filename / data.size(与旧版一致)。
  3. 调用 Relay POST /v1/chat/completions 时,将预上传 token 交给网关的方式二选一(或同时提供,会去重合并):

    • 推荐:请求头 X-Usage-Log-Attachment-Tokens,值为 逗号分隔 的 token,或 JSON 数组字符串 ["t1","t2"] —— 无需在 JSON body 顶层写 usage_log_attachment_tokens
    • 兼容:JSON 顶层 usage_log_attachment_tokens["token1","token2"]
  4. 转发前注入消息(默认):在 LOG_EXT_ATTACHMENT_INJECT_TO_MESSAGES=true(默认) 时,中间件在交给 relay 之前会根据 token 读取待定文件,向 messages 中最后一条 role=usercontent 追加 OpenAI 兼容段:

    • PDF(默认)LOG_EXT_ATTACHMENT_PDF_INJECT_MODE=file(默认) 时,PDF 以 type:file + file_data: data:application/pdf;base64,... 注入最后一条 user 消息(不依赖系统 pdftotext)。须上游在 Chat 路径上支持 messages 内该形态;若上游忽略未知段,模型会看不到 PDF。
    • LOG_EXT_ATTACHMENT_PDF_INJECT_MODE=text 时,网关调用 pdftotext(需 poppler-utils)将 PDF 抽成纯文本,以 type:text 注入——适合只接受纯文本、或不接受 type:file 的上游。
    • 非 PDF:仍使用 type:file + Data URL。
      须与预上传使用同一鉴权Authorization: Bearer sk-... 或管理端会话),否则返回 400。若 LOG_EXT_ATTACHMENT_INJECT_TO_MESSAGES=false,则 token 用于下文第 5 步关联数据库,不会把文件塞进 messages(旧行为)。
  5. 中间件在 c.Next() 之后 根据 logs.request_id,将待定行中 pending_token 匹配、且 user_id 等于该日志用户或为 0(历史匿名数据) 的记录写入 request_id,并把 user_id 更新为日志所属用户,完成关联。

约束:每个文件大小 ≤ LOG_EXT_ATTACHMENT_MAX_BYTES(默认 20MiB);单次请求最多 20 个文件

9.2.1 单次请求内联上传(与对话同发,无需先调 /api/ext/attachment/upload

POST /v1/chat/completions(以及 /v1/completions/v1/messages/pg/chat/completions 等捕获路径)使用 Content-Type: multipart/form-data

表单字段说明
requestjson必填,值为与原先 application/json body 完全相同的 JSON;可为纯文本字段,也可用 curl -F request=@chat.json 上传文件(服务端会从 multipart 文件部件读取)。
filesfile(可多个)可选,本地文件;网关会写入待定附件并自动合并到本次请求的 usage_log_attachment_tokens

鉴权需 Authorization: Bearer sk-...(与纯 JSON 聊天相同)或管理端会话带文件时必须能解析出用户。

bash
# 1)准备 chat.json(与原来 JSON body 一致,不要写 usage_log_attachment_tokens)
# 2)一次请求带上文件
curl -N -sS -X POST 'http://127.0.0.1:3000/v1/chat/completions' \
  -H 'Authorization: Bearer YOUR_API_KEY' \
  -F 'request=@chat.json' \
  -F 'files=@/path/to/report.pdf' \
  -F 'files=@/path/to/notes.docx'

网关内部会把 body 还原为 application/json 再交给下游 relay,行为与「先预上传 + 头里带 token」等价。

9.3 配置(附件)

环境变量默认说明
LOG_EXT_ATTACHMENT_ENABLEDtrue关闭则不上传、不解析链接、不写入附件表。
LOG_EXT_ATTACHMENT_ROOT./data/usage_log_attachments本地存储根目录(自动 mkdir)。
LOG_EXT_ATTACHMENT_MAX_BYTES20971520单次上传最大字节数。
LOG_EXT_FILE_URL_FETCH_ENABLEDtrue是否将无 file_idfile.file_url 拉取并替换为 type:text(见 9.1.1)。
LOG_EXT_FILE_URL_MAX_TEXT_RUNES200000单条 URL 展开后的文本最大 rune 数;0 表示该条不截断(与 TruncateUTF8 约定一致)。
LOG_EXT_ATTACHMENT_INJECT_TO_MESSAGEStrue是否将预上传 token 对应内容注入最后一条 user 消息后再转发(见 9.2 第 4 点)。
LOG_EXT_ATTACHMENT_PDF_INJECT_MODEfilePDF:file(默认)= type:file + Data URL,无需 pdftotexttext= pdftotext 抽字后以 type:text 注入(需安装 poppler-utils)。

9.3.1 预上传 PDF 后模型像「没读到文档」

  • 现象upload 成功、请求头已带 X-Usage-Log-Attachment-Tokens,回复却与 PDF 无关。
  • 常见原因(默认 file 直传):下游 Chat Completions 实现未实现丢弃 messages 里的 type:file,模型只看到文字指令。
  • 处理:换用支持该字段的渠道/上游;或改为 LOG_EXT_ATTACHMENT_PDF_INJECT_MODE=text 并在网关机安装 poppler-utils(将 PDF 抽成 type:text,兼容性更好,但依赖系统命令)。

9.4 下载与权限

  • GET /api/ext/attachment/download?id=<附件主键>(需登录):校验附件的 request_id 对应 logs 归属当前用户或管理员。
  • kind=upload:读磁盘文件并 Content-Disposition 下载
  • kind=url302 重定向到 source_url

9.5 联调示例(curl:预上传 + 流式聊天)

本机上的 PDF / Word / Excel 等:OpenAI 风格 messages[].content没有「直接塞本地文件二进制」的标准字段;网关侧 file.file_url 只解析 http(s) 外链。因此 本地文档 须:

  1. POST /api/ext/attachment/uploadmultipart 上传(可多文件);
  2. 聊天请求用请求头 X-Usage-Log-Attachment-Tokens 带上返回的 pending_token(与 Authorization: Bearer sk-... 同一密钥),才会记入 **usage_log_attachment** 并与本次 request_id 关联。

与外链、内嵌图的区别(摘要):

内容怎么交给网关 / 日志
外链图片 / 外链 PDFimage_url.urlfile.file_url
本地小图(给模型看)常用 data:image/...;base64,... 写在 image_url.url
本机 pdf / doc(x) / xlsx 等预上传 + X-Usage-Log-Attachment-Tokens不能只靠 JSON 里一段 base64 文件名)

1)预上传本地文件(与 chat 使用同一 Authorization: Bearer sk-... 密钥)。示例同时上传图片与多种办公文档:

bash
curl -sS -X POST 'http://127.0.0.1:3000/api/ext/attachment/upload' \
  -H 'Authorization: Bearer YOUR_API_KEY' \
  -F 'files=@/path/to/photo.png' \
  -F 'files=@/path/to/report.pdf' \
  -F 'files=@/path/to/notes.docx' \
  -F 'files=@/path/to/data.xlsx'

从响应 data.pending_tokens(或 data.items[].pending_token)复制全部 token。

2)调用聊天接口:在 X-Usage-Log-Attachment-Tokens 中填入上一步 token(逗号分隔);messages 里仍可写外链图、外链 PDF、base64 图等。

bash
curl -N -sS -X POST 'http://127.0.0.1:3000/v1/chat/completions' \
  -H 'Authorization: Bearer YOUR_API_KEY' \
  -H 'X-Usage-Log-Attachment-Tokens: TOKEN_FROM_PNG,TOKEN_FROM_PDF,TOKEN_FROM_DOCX,TOKEN_FROM_XLSX' \
  -H 'Content-Type: application/json' \
  -d @- <<'EOF'
{
  "model": "gpt-4o",
  "stream": true,
  "messages": [
    {
      "role": "system",
      "content": "你是专业助手,回答简洁。"
    },
    {
      "role": "user",
      "content": [
        {
          "type": "text",
          "text": "请结合:外链图、外链 PDF、base64 本地小图;本机的 pdf/docx/xlsx 已通过预上传并在请求头关联 token。"
        },
        {
          "type": "image_url",
          "image_url": {
            "url": "https://img-s.msn.cn/tenant/amp/entityid/AA1Z6SrD.img?w=600&h=466&m=6"
          }
        },
        {
          "type": "image_url",
          "image_url": {
            "url": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg=="
          }
        }
      ]
    }
  ]
}
EOF

说明:OpenAI 官方等上游若要在 content 里带文档,通常要求 file.file_id(先走 Files API),不会把任意 file.file_url 当可下载文档交给模型。若开启 9.1.1(默认),本网关会先把 file_url(且无 file_id 拉成 纯文本再转发,则下游收到的是 type:text,一般不再出现「缺少 file_id」。若关闭 LOG_EXT_FILE_URL_FETCH_ENABLED,则 file_url 仍可能原样进入上游,此时仅便于 usage_log 从消息里收集 URL 记库,联调直连 OpenAI 时可能报 missing_required_parameter模型是否真的能「读」本地 pdf/doc 仍取决于渠道;usage_log 侧 token 关联成功即可在管理端看到附件。

(若仍使用 JSON 顶层的 usage_log_attachment_tokens,可与请求头合并,重复 token 会去重。)

9.6 主仓库 Go 依赖:github.com/xuri/excelize/v2

作用(业务)
预上传或内联上传的 .xlsx / .xlsm(OpenXML 电子表格)在需要注入到 messages 时,扩展不会把整个二进制当 PDF/图片处理,而是由 ext/usage_log/attachment_xlsx.go 调用 excelize 打开工作簿,按工作表读出单元格,拼成分段纯文本(制表符分列、多表带标题、并限制单表行数/工作表数量以防爆内存),再按与其它办公附件类似的逻辑转为可注入内容(例如以 type:text 等形式参与转发,具体与同路径下 attachment_process.go 等一致)。

作用(工程)
该文件 import github.com/xuri/excelize/v2,因此主仓库go.mod / go.sum 必须包含该模块(及 go mod tidy 写入的间接依赖)。否则 go build 报错no required module provides package github.com/xuri/excelize/v2

是否「整个日志插件都必须」

  • 编译当前源码必须保留该依赖,否则无法完成构建。
  • 功能上:仅在与 Excel 类附件的解析/注入路径相关;不涉及 PDF、纯链接、图片等其它附件逻辑。若将来删除 attachment_xlsx.go 及相关调用链,可再通过 go mod tidy 尝试移除 excelize(本文不展开)。

合并上游后若依赖丢失,补全方式见 usage_log与上游合并-源码入侵.md 中的 go.mod / go.sum 一节。


10. 只读 API

方法路径说明
GET/api/ext/log-content?request_id=xxx需登录;校验 request_id;返回 system_prompt_textprompt_textcompletion_textattachments 列表(含 download_url)。无文本记录时仍可能返回仅 attachments
POST/api/ext/attachment/upload预上传,见第 9.2 节。
GET/api/ext/attachment/download?id=下载或跳转,见第 9.4 节。

11. 前端(可选扩展)

管理端「使用日志」通过 web/src/ext/usage-logs/ 展示「问题 / 对话详情」列;详情内为 提示词、问题(正文与关联附件同属提问区)、回答VITE_EXT_USAGE_LOG_CONTENT=false 可关闭扩展表。

入口替换、locales 所需 keyusage_log与上游合并-源码入侵 · 前端;组件职责见 插件开发 · 第 2.7.5 节


12. 限制与已知情况

  • 流式:依赖完整 SSE 缓冲;客户端中断、网关超时可能导致回答不完整
  • 超长:受 LOG_EXT_MAX_* 截断。
  • 路径:仅覆盖第 4.1 节所列;未列出的 relay 路径不会写入扩展表。
  • 附件:依赖 logs 已写入 后才能按 user_id 关联预上传令牌;若极端情况下日志滞后,关联可能失败(见服务端日志)。

13. 源码索引

模块路径
配置与加载ext/usage_log/config.go
捕获中间件与解析ext/usage_log/middleware.gofile_url_expand.goattachment_inject.go
落库ext/usage_log/store.go
附件存储与解析ext/usage_log/attachment_store.goattachment_parse.goattachment_process.goattachment_handler.go
表格(xlsx/xlsm)纯文本抽取ext/usage_log/attachment_xlsx.go(依赖主仓 go.mod 中的 github.com/xuri/excelize/v2,见 第 9.6 节
模型与迁移ext/usage_log/model_ext.go
路由ext/usage_log/router_ext.go
Context 键ext/usage_log/keys.go
注册ext/usage_log/register.go
Relay 单点接入ext/relay.go
表初始化脚本ext/table_init/usage_log_attachment_*.sql
主仓库侵入点(合并上游用)usage_log与上游合并-源码入侵.md(仅非 ext/ 源码)

14. 四份文档怎样配合

文档职责
本文(日志插件)业务:目标、流程、环境变量、附件、API、前端语义、限制;Excel / excelize第 9.6 节
插件开发.md机制ext 包、目录、新建插件、表初始化、源码摘录、附录;业务数字以本文为准。
usage_log与上游合并-源码入侵.md合并:主仓库须保留的 Go / 前端 / i18n / go.mod〔合并段〕 与源码注释 // 开始 对应。
与上游-Gitee-手动合并步骤.mdGit:仅当与 Gitee 无共同祖先时的 mergestash-X theirs不包含业务与侵入代码块。

典型路径: 日常查配置 → 本文;写 ext/ 或联调 → 插件开发;拉上游代码 → Gitee 步骤(若需要)→ 源码侵入 逐项恢复 → go build