✨ feat(api): 统一响应格式并完善参数体系与文档

- 正文参数支持语义值(plain/html/keep/remove)与别名(img_mode/review_mode),URL 参数优先于配置
- 修复纯文本被段评强制包回 HTML、HTML 模式段评注入失效的问题
- 修复 PKCS7 填充弱校验导致漫画/正文偶发解密乱码返回 200 的问题
- 搜索结果展平:去 search_tabs 层与 selected_tab_idx/server_ab_params,提升 has_more/next_offset
- 全接口解包字符串形式的 JSON 字段(category_schema 等),缓存出口幂等处理
- 序列化关闭 HTML 转义,video/audio 播放链接可直接复制播放
- video 响应透传 CENC 加密状态(key_seed/kid/encryption_method)
- 新增 gzip 中间件,JSON/文本响应压缩约 81%,二进制透传
- 漫画默认输出模式改为 proxy(url 模式链接 404 且内容加密不可用)
- openapi.yaml:补全 23 个路径参数定义、tab_type 完整枚举、CDN 签名说明,示例更新
This commit is contained in:
chenxin committed 2026-10-09 21:01:18 +08:00
1 parent bfb4290c15
commit 1a4997e031
25 files changed
+489 -199

No files matched your search

+33 -95
View File
@@ -6,6 +6,8 @@ import (
"encoding/json"
"fanqie/internal/common"
"fmt"
"regexp"
"strconv"
"strings"
)
@@ -62,8 +64,11 @@ func postReviewAPI(url string, requestBody interface{}) (map[string]interface{},
return nil, fmt.Errorf("解析响应失败: %w", err)
}
// 提取 data 字段
// 提取 data 字段(并解包 JSON 字符串字段,如 post_data.content)
if data, ok := result["data"].(map[string]interface{}); ok {
if unwrapped, ok := UnwrapJSONStrings(data).(map[string]interface{}); ok {
return unwrapped, nil
}
return data, nil
}
@@ -169,8 +174,11 @@ func GetParaReview(itemID, bookID, commentID string, para, sort, offset, count i
return nil, fmt.Errorf("解析响应失败: %w", err)
}
// 提取 data 字段
// 提取 data 字段(并解包 JSON 字符串字段,如 post_data.content)
if data, ok := result["data"].(map[string]interface{}); ok {
if unwrapped, ok := UnwrapJSONStrings(data).(map[string]interface{}); ok {
return unwrapped, nil
}
return data, nil
}
@@ -235,103 +243,33 @@ func GenerateParagraphsWithComments(itemID, bookID, content, baseURL string) (st
strings.Contains(content, "<div") ||
strings.Contains(content, "<article")
// 关键:如果检测到有段评数据,强制使用HTML模式(包裹<p>标签)
// 因为<comment>标签需要XML兼容的格式
hasAnyComments := len(commentData) > 0
if hasAnyComments && !isHTMLContent {
isHTMLContent = true
// 纯文本模式下 <comment> 标签无法承载,保持纯文本原样返回(不注入段评);
// 需要段评标记时配置 remove_html_tags: false 或 text_mode=html
if !isHTMLContent {
return content, nil
}
lines := strings.Split(content, "\n")
var result strings.Builder
paragraphIndex := 0
for i, line := range lines {
isLastLine := i == len(lines)-1
trimmedLine := strings.TrimSpace(line)
// 完全空行:直接输出换行符
if trimmedLine == "" {
if !isLastLine {
result.WriteString("\n")
}
continue
}
// 跳过图片标签和div标签
if strings.HasPrefix(trimmedLine, "<img") ||
strings.HasPrefix(trimmedLine, "<div") ||
strings.HasPrefix(trimmedLine, "</div>") ||
strings.HasPrefix(trimmedLine, "<article") ||
strings.HasPrefix(trimmedLine, "</article") {
result.WriteString(line)
if !isLastLine {
result.WriteString("\n")
}
continue
}
// 如果原内容已经有<p>标签,在</p>之前插入段评标记
if strings.HasPrefix(trimmedLine, "<p>") && strings.HasSuffix(trimmedLine, "</p>") {
// 提取<p>和</p>之间的内容
innerContent := trimmedLine[3 : len(trimmedLine)-4]
result.WriteString("<p>")
result.WriteString(innerContent)
// 添加评论标记
key := fmt.Sprintf("%d", paragraphIndex)
if commentInfo, exists := commentData[key]; exists && commentInfo.Count > 0 {
commentTag := fmt.Sprintf(`<comment ident="%s/review_html?item_id=%s&book_id=%s&para=%d" count="%d" />`,
baseURL, itemID, bookID, paragraphIndex, commentInfo.Count)
result.WriteString(commentTag)
}
result.WriteString("</p>")
if !isLastLine {
result.WriteString("\n")
}
paragraphIndex++
continue
}
// 如果是HTML模式,包裹<p>标签
if isHTMLContent {
result.WriteString("<p>")
result.WriteString(line)
// 添加评论标记
key := fmt.Sprintf("%d", paragraphIndex)
if commentInfo, exists := commentData[key]; exists && commentInfo.Count > 0 {
commentTag := fmt.Sprintf(`<comment ident="%s/review_html?item_id=%s&book_id=%s&para=%d" count="%d" />`,
baseURL, itemID, bookID, paragraphIndex, commentInfo.Count)
result.WriteString(commentTag)
}
result.WriteString("</p>")
if !isLastLine {
result.WriteString("\n")
}
} else {
// 纯文本模式:只在行尾添加comment标记
result.WriteString(line)
// 添加评论标记(纯文本格式)
key := fmt.Sprintf("%d", paragraphIndex)
if commentInfo, exists := commentData[key]; exists && commentInfo.Count > 0 {
commentTag := fmt.Sprintf(`<comment ident="%s/review_html?item_id=%s&book_id=%s&para=%d" count="%d" />`,
baseURL, itemID, bookID, paragraphIndex, commentInfo.Count)
result.WriteString(commentTag)
}
if !isLastLine {
result.WriteString("\n")
// HTML 内容:在每个 </p> 前插入段评标记。
// 兼容 <p idx="n">(HTML 模式结构)与无属性的 <p>;段号取 idx,无 idx 时按出现顺序。
// header/article/div/img 等非段落标签不受影响。
pRe := regexp.MustCompile(`(?s)<p\b[^>]*>.*?</p>`)
idxRe := regexp.MustCompile(`idx="(\d+)"`)
order := 0
return pRe.ReplaceAllStringFunc(content, func(m string) string {
defer func() { order++ }()
para := order
if idx := idxRe.FindStringSubmatch(m); idx != nil {
if n, err := strconv.Atoi(idx[1]); err == nil {
para = n
}
}
paragraphIndex++
}
return result.String(), nil
if commentInfo, exists := commentData[fmt.Sprintf("%d", para)]; exists && commentInfo.Count > 0 {
commentTag := fmt.Sprintf(`<comment ident="%s/review_html?item_id=%s&book_id=%s&para=%d" count="%d" />`,
baseURL, itemID, bookID, para, commentInfo.Count)
return m[:len(m)-len("</p>")] + commentTag + "</p>"
}
return m
}), nil
}
// extractDeviceID 从 device header 中提取 device_id