Files
ttstd-infinite-canvas/backend/internal/protocol/documentation.go

114 lines
4.9 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 ""
}