You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
6.9 KiB
6.9 KiB
修复 Markdown 文件编码乱码问题
日期: 2026-02-06
类型: Bug 修复
关联: RFC 140 - 剧本文件存储重构
🐛 问题描述
上传到 OSS 的解析后 Markdown 文件在浏览器中打开时显示为乱码。
症状
- 解析后的 Markdown 文件内容在浏览器中无法正常显示中文
- 中文字符显示为乱码或
?符号 - 下载文件后用文本编辑器打开也可能显示为乱码
根本原因
在上传 Markdown 文件到 OSS 时,虽然内容使用了 UTF-8 编码(markdown_content.encode('utf-8')),但没有在 HTTP 响应头中明确指定 charset=utf-8,导致:
- OSS 返回文件时,HTTP 响应头为:
Content-Type: text/markdown - 浏览器无法确定文件编码,可能按默认编码(如 GBK、Latin-1)解析
- 导致中文字符显示为乱码
✅ 解决方案
修改 StorageService.upload_bytes() 方法
文件: server/app/core/storage.py
在上传文本类型文件时,自动在 Content-Type 头中添加 charset=utf-8:
async def upload_bytes(
self,
data: bytes,
object_name: str,
content_type: Optional[str] = None
) -> str:
try:
extra_args = {}
if content_type:
extra_args['ContentType'] = content_type
# ✅ 如果是文本类型,添加 UTF-8 charset
if content_type and content_type.startswith('text/'):
extra_args['ContentType'] = f"{content_type}; charset=utf-8"
self.client.put_object(
Bucket=self.bucket_name,
Key=object_name,
Body=data,
**extra_args
)
修改前后对比
修改前
GET /jointo/screenplays/parsed/xxx.md HTTP/1.1
...
HTTP/1.1 200 OK
Content-Type: text/markdown
Content-Length: 12345
...
《狮⼦回头》 ← 浏览器按错误编码解析,显示为乱码
修改后
GET /jointo/screenplays/parsed/xxx.md HTTP/1.1
...
HTTP/1.1 200 OK
Content-Type: text/markdown; charset=utf-8 ← ✅ 明确指定 UTF-8
Content-Length: 12345
...
《狮子回头》 ← 正确显示中文
🎯 影响范围
受影响的文件类型
此修复自动应用于所有以 text/ 开头的 MIME 类型:
text/markdown- Markdown 文件(剧本解析后的文件)text/plain- 纯文本文件text/html- HTML 文件text/csv- CSV 文件text/xml- XML 文件
不受影响的文件类型
二进制文件不受影响(无需 charset):
application/pdfapplication/vnd.openxmlformats-officedocument.wordprocessingml.document(DOCX)image/*video/*audio/*
🧪 验证方法
1. 上传新剧本文件
# 上传 PDF/DOCX 剧本文件
curl -X POST "http://localhost:6170/api/v1/screenplays/file" \
-H "Authorization: Bearer <token>" \
-F "file=@test.pdf" \
-F "name=测试剧本" \
-F "projectId=<project_id>"
2. 等待解析完成
轮询 parse-status 接口直到 parsingStatus: "completed"。
3. 获取 Markdown 文件 URL
{
"data": {
"parsingStatus": "completed",
"fileUrl": "http://localhost:9000/jointo/screenplays/parsed/019c326b-ee0e-7fa0-8361-12924ba196d2.md"
}
}
4. 在浏览器中打开
直接访问 fileUrl,验证中文字符是否正确显示。
5. 检查 HTTP 响应头
使用浏览器开发者工具或 curl:
curl -I "http://localhost:9000/jointo/screenplays/parsed/019c326b-ee0e-7fa0-8361-12924ba196d2.md"
预期响应头:
HTTP/1.1 200 OK
Content-Type: text/markdown; charset=utf-8 ← ✅ 包含 charset=utf-8
Content-Length: 12345
...
6. 验证内容
curl "http://localhost:9000/jointo/screenplays/parsed/019c326b-ee0e-7fa0-8361-12924ba196d2.md"
预期输出(正确显示中文):
《狮子回头》
## 第一场
场景:夜晚,街头
角色A:你好...
🔄 已上传文件的处理
旧文件(已上传)
已经上传到 OSS 的 Markdown 文件不会自动更新,因为:
- OSS 对象的 metadata 是在上传时设置的
- 已上传的文件保持原有的
Content-Type: text/markdown(无 charset)
解决方法
选项 1: 重新上传(推荐)
重新上传剧本文件,触发重新解析和 Markdown 文件生成:
- 删除旧剧本
- 重新上传同一文件
- 新生成的 Markdown 文件将包含正确的
charset=utf-8
选项 2: 批量更新 OSS 对象 metadata
如果有大量旧文件需要修复,可以使用以下脚本:
import boto3
from app.core.config import settings
s3_client = boto3.client(
's3',
endpoint_url=settings.S3_ENDPOINT_URL,
aws_access_key_id=settings.S3_ACCESS_KEY_ID,
aws_secret_access_key=settings.S3_SECRET_ACCESS_KEY,
region_name=settings.S3_REGION
)
# 列出所有 Markdown 文件
response = s3_client.list_objects_v2(
Bucket=settings.S3_BUCKET_NAME,
Prefix='screenplays/',
)
for obj in response.get('Contents', []):
if obj['Key'].endswith('.md'):
# 复制对象到自身,更新 Content-Type
s3_client.copy_object(
Bucket=settings.S3_BUCKET_NAME,
CopySource={'Bucket': settings.S3_BUCKET_NAME, 'Key': obj['Key']},
Key=obj['Key'],
ContentType='text/markdown; charset=utf-8',
MetadataDirective='REPLACE'
)
print(f"✅ 已更新: {obj['Key']}")
📝 编码最佳实践
1. Python 字符串与字节的转换
# ✅ 正确:明确指定 UTF-8 编码
content_bytes = markdown_content.encode('utf-8')
# ❌ 错误:使用默认编码(可能不是 UTF-8)
content_bytes = markdown_content.encode()
2. 文件读写
# ✅ 正确:明确指定 UTF-8
async with aiofiles.open(file_path, 'r', encoding='utf-8') as f:
content = await f.read()
# ❌ 错误:使用系统默认编码
async with aiofiles.open(file_path, 'r') as f: # 可能是 GBK/Latin-1
content = await f.read()
3. HTTP 响应头
# ✅ 正确:文本类型应指定 charset
Content-Type: text/markdown; charset=utf-8
Content-Type: text/plain; charset=utf-8
# ❌ 错误:缺少 charset
Content-Type: text/markdown
# ✅ 正确:二进制文件不需要 charset
Content-Type: application/pdf
Content-Type: image/jpeg
🔗 相关文档
✨ 总结
- 问题: Markdown 文件上传到 OSS 后,缺少
charset=utf-8导致乱码 - 原因: HTTP 响应头未明确指定字符编码
- 解决: 在上传文本文件时,自动添加
; charset=utf-8到Content-Type - 影响: 所有新上传的文本文件(
text/*)都将正确设置编码 - 旧文件: 需要重新上传或手动更新 OSS 对象 metadata