114 lines
4.9 KiB
Go
114 lines
4.9 KiB
Go
package protocol
|
||
|
||
import (
|
||
"embed"
|
||
"fmt"
|
||
"strings"
|
||
)
|
||
|
||
//go:embed docs/*.md
|
||
var builtinDocumentationFS embed.FS
|
||
|
||
func attachDocumentation(info *Metadata) {
|
||
if info == nil || strings.TrimSpace(info.Documentation) != "" {
|
||
return
|
||
}
|
||
document, err := builtinDocumentationFS.ReadFile("docs/" + info.ID + ".md")
|
||
if err != nil {
|
||
return
|
||
}
|
||
info.Documentation = renderBuiltinDocumentation(*info, string(document))
|
||
}
|
||
|
||
// AttachDocumentation fills the reference document for a shipped protocol.
|
||
func AttachDocumentation(info *Metadata) {
|
||
attachDocumentation(info)
|
||
}
|
||
|
||
func renderBuiltinDocumentation(info Metadata, document string) string {
|
||
replacements := map[string]string{
|
||
"{{NAME}}": info.Name,
|
||
"{{OPERATIONS}}": renderOperations(info),
|
||
"{{PARAMETERS}}": renderParameters(info.Parameters),
|
||
"{{CONTRACT}}": renderRuntimeContract(info),
|
||
}
|
||
for marker, replacement := range replacements {
|
||
document = strings.ReplaceAll(document, marker, replacement)
|
||
}
|
||
return strings.TrimSpace(document) + "\n"
|
||
}
|
||
|
||
func renderOperations(info Metadata) string {
|
||
var b strings.Builder
|
||
b.WriteString("| 阶段 | 方法与相对路径 | 请求类型 | 宿主行为 |\n| --- | --- | --- | --- |\n")
|
||
writeOperation(&b, "创建", info.Create, firstNonEmpty(info.ContentType, "application/json"), "提交请求;同步协议直接解析结果,异步协议读取任务 ID")
|
||
if strings.TrimSpace(info.Poll) != "" {
|
||
writeOperation(&b, "轮询", info.Poll, "application/json", "查询状态,成功后提取媒体地址")
|
||
}
|
||
if strings.TrimSpace(info.Cancel) != "" {
|
||
writeOperation(&b, "取消", info.Cancel, "application/json", "取消仍在运行的任务")
|
||
}
|
||
return strings.TrimSpace(b.String())
|
||
}
|
||
|
||
func renderParameters(parameters []Parameter) string {
|
||
if len(parameters) == 0 {
|
||
return "> 当前适配器没有声明可配置参数;这不表示上游没有参数,只表示宿主尚未建立稳定映射。"
|
||
}
|
||
var b strings.Builder
|
||
b.WriteString("| 宿主字段 | 类型 | 必填 | 实际上游映射 | 说明 |\n| --- | --- | --- | --- | --- |\n")
|
||
for _, parameter := range parameters {
|
||
description := strings.TrimSpace(parameter.Description)
|
||
if len(parameter.Values) > 0 {
|
||
description = strings.TrimSpace(description + " 可选值:" + strings.Join(parameter.Values, "、") + "。")
|
||
}
|
||
fmt.Fprintf(&b, "| `%s` | `%s` | %s | `%s` | %s |\n", markdownCell(parameter.Name), markdownCell(parameter.Type), yesNo(parameter.Required), markdownCell(firstNonEmpty(parameter.Mapping, "未映射")), markdownCell(firstNonEmpty(description, "由当前协议适配器按请求类型转换。")))
|
||
}
|
||
return strings.TrimSpace(b.String())
|
||
}
|
||
|
||
func renderRuntimeContract(info Metadata) string {
|
||
var b strings.Builder
|
||
b.WriteString("## 宿主运行时合同\n\n")
|
||
b.WriteString("- Base URL 只填写协议服务根地址,表中的相对路径由适配器拼接;不要把同一路径重复写进 Base URL。\n")
|
||
b.WriteString("- 密钥由后端渠道中转读取,插件详情和浏览器请求不保存密钥。Bearer、供应商签名或专用版本头以本文鉴权章节为准。\n")
|
||
b.WriteString("- 模型名来自渠道模型配置并原样发送。此插件不暗改模型名,也不根据名称猜测价格、额度或能力。\n")
|
||
b.WriteString("- HTTP 非 2xx、非 JSON 响应、缺少任务 ID、成功状态却没有可识别结果都会作为真实错误向上返回。\n")
|
||
if strings.TrimSpace(info.Poll) != "" {
|
||
b.WriteString("- 异步状态统一为:`queued/pending/created/submitted` -> 等待,`running/processing/in_progress` -> 处理中,`succeeded/completed/done` -> 成功,`failed/error/expired/cancelled` -> 失败。\n")
|
||
b.WriteString("- 轮询频率应遵守上游速率限制;媒体 URL 通常有有效期,成功后应立即进入宿主资源保存流程。\n")
|
||
}
|
||
b.WriteString("- 参考素材只接受上游可访问的 HTTP(S) URL 或带 MIME 前缀的 data URL;裸 Base64、登录后才能访问的链接和 Cookie 保护地址不可用。\n")
|
||
b.WriteString("- 插件只保证下方“当前实现”明确列出的字段。上游新增能力不会自动获得宿主支持,必须补适配、解析和测试。\n")
|
||
return strings.TrimSpace(b.String())
|
||
}
|
||
|
||
func writeOperation(b *strings.Builder, phase, value, contentType, purpose string) {
|
||
parts := strings.SplitN(strings.TrimSpace(value), " ", 2)
|
||
method, path := "", strings.TrimSpace(value)
|
||
if len(parts) == 2 {
|
||
method, path = parts[0], parts[1]
|
||
}
|
||
fmt.Fprintf(b, "| %s | `%s %s` | `%s` | %s |\n", phase, markdownCell(method), markdownCell(path), markdownCell(contentType), purpose)
|
||
}
|
||
|
||
func markdownCell(value string) string {
|
||
return strings.ReplaceAll(strings.ReplaceAll(value, "|", "\\|"), "\n", " ")
|
||
}
|
||
|
||
func yesNo(value bool) string {
|
||
if value {
|
||
return "是"
|
||
}
|
||
return "否"
|
||
}
|
||
|
||
func firstNonEmpty(values ...string) string {
|
||
for _, value := range values {
|
||
if strings.TrimSpace(value) != "" {
|
||
return value
|
||
}
|
||
}
|
||
return ""
|
||
}
|