把“章节正文 + 图片”从 chapters 单表/JSON 结构,重构为“章节 chapter + 段落 section + 图片 memoir_images 独立表”的新数据模型,同时联动修改接口、PDF 导出、异步任务、迁移脚本、测试,以及修复 Android 端聊天列表显示问题。 (#9)

* refactor: 表结构重构,新增段落section和图片image新表

* fix: fix android app import error

* refactor: 重构文件名

* fix: 优化提示词

* fix: 消息气泡显示位置异常问题

---------

Co-authored-by: yangshilin <2157598560@qq.com>
This commit is contained in:
Sully
2026-03-13 11:12:10 +08:00
committed by GitHub
parent 1cb804fa37
commit 2eb066dbec
19 changed files with 1280 additions and 624 deletions

View File

@@ -7,6 +7,7 @@ from api.tasks.memoir_tasks import initialize_chapter_images
class MemoirImageBootstrapTest(unittest.TestCase):
def test_initialize_chapter_images_keeps_only_completed_assets_when_disabled(self):
"""图片初始化已迁移到 _save_narrative_to_sections此处为兼容 no-op直接返回 []"""
chapter = type(
"ChapterStub",
(),
@@ -14,35 +15,16 @@ class MemoirImageBootstrapTest(unittest.TestCase):
"id": "chapter-1",
"title": "童年的夏天",
"category": "childhood",
"content": "那条路我一直记得。",
"images": [
{
"index": 0,
"placeholder": "{{IMAGE:南方小镇的青石板路}}",
"description": "南方小镇的青石板路",
"status": "completed",
"url": "https://cos.example.com/existing.png",
},
{
"index": 1,
"placeholder": "{{IMAGE:奶奶坐在院子里的藤椅上}}",
"description": "奶奶坐在院子里的藤椅上",
"status": "pending",
"url": None,
},
],
},
)()
with unittest.mock.patch.dict(os.environ, {"MEMOIR_IMAGE_ENABLED": "false"}, clear=False):
assets = initialize_chapter_images(chapter)
self.assertEqual(len(assets), 1)
self.assertEqual(assets[0]["status"], "completed")
self.assertEqual(assets[0]["url"], "https://cos.example.com/existing.png")
self.assertEqual(chapter.images, assets)
self.assertEqual(assets, [])
def test_initialize_chapter_images_sets_pending_assets_when_enabled(self):
"""图片初始化已迁移到 _save_narrative_to_sections此处为兼容 no-op"""
chapter = type(
"ChapterStub",
(),
@@ -50,234 +32,68 @@ class MemoirImageBootstrapTest(unittest.TestCase):
"id": "chapter-1",
"title": "童年的夏天",
"category": "childhood",
"content": "那条路我一直记得。\n\n{{{{IMAGE:南方小镇的青石板路}}}}",
"images": [],
},
)()
with unittest.mock.patch.dict(os.environ, {"MEMOIR_IMAGE_ENABLED": "true"}, clear=False):
assets = initialize_chapter_images(chapter)
self.assertEqual(len(assets), 1)
self.assertEqual(assets[0]["status"], "pending")
self.assertEqual(assets, [])
def test_initialize_chapter_images_preserves_completed_assets_and_adds_only_new_placeholders(self):
chapter = type(
"ChapterStub",
(),
{
"id": "chapter-1",
"title": "童年的夏天",
"category": "childhood",
"content": (
"那条路我一直记得。\n\n"
"{{{{IMAGE:南方小镇的青石板路}}}}\n\n"
"奶奶总坐在门口。\n\n"
"{{{{IMAGE:奶奶坐在院子里的藤椅上}}}}"
),
"images": [
{
"index": 0,
"placeholder": "{{{{IMAGE:南方小镇的青石板路}}}}",
"description": "南方小镇的青石板路",
"prompt": "A serene southern China town",
"url": "https://cos.example.com/existing.png",
"status": "completed",
"provider": "liblib",
"style": "watercolor",
"size": "1024x1024",
"error": None,
"created_at": "2026-03-10T10:00:00Z",
"updated_at": "2026-03-10T10:00:00Z",
}
],
},
)()
"""图片初始化已迁移到 _save_narrative_to_sections此处为兼容 no-op"""
chapter = type("ChapterStub", (), {"id": "chapter-1", "title": "童年的夏天", "category": "childhood"})()
with unittest.mock.patch.dict(os.environ, {"MEMOIR_IMAGE_ENABLED": "true"}, clear=False):
assets = initialize_chapter_images(chapter)
self.assertEqual(len(assets), 2)
self.assertEqual(assets[0]["status"], "completed")
self.assertEqual(assets[0]["url"], "https://cos.example.com/existing.png")
self.assertEqual(assets[1]["status"], "pending")
self.assertEqual(assets[1]["description"], "奶奶坐在院子里的藤椅上")
self.assertEqual(assets, [])
def test_initialize_chapter_images_accepts_double_brace_placeholders(self):
chapter = type(
"ChapterStub",
(),
{
"id": "chapter-1",
"title": "童年的夏天",
"category": "childhood",
"content": "开头。\n\n{{IMAGE:1938年初的上海弄堂口冬日萧瑟}}\n\n结尾。",
"images": [],
},
)()
"""图片初始化已迁移到 _save_narrative_to_sections此处为兼容 no-op"""
chapter = type("ChapterStub", (), {"id": "chapter-1", "title": "童年的夏天", "category": "childhood"})()
with unittest.mock.patch.dict(os.environ, {"MEMOIR_IMAGE_ENABLED": "true"}, clear=False):
assets = initialize_chapter_images(chapter)
self.assertEqual(len(assets), 1)
self.assertEqual(assets[0]["status"], "pending")
self.assertEqual(assets[0]["placeholder"], "{{IMAGE:1938年初的上海弄堂口冬日萧瑟}}")
self.assertEqual(assets, [])
def test_initialize_chapter_images_normalizes_invalid_existing_asset_status(self):
chapter = type(
"ChapterStub",
(),
{
"id": "chapter-1",
"title": "童年的夏天",
"category": "childhood",
"content": "开头。\n\n{{IMAGE:南方小镇的青石板路}}\n\n结尾。",
"images": [
{
"index": 0,
"placeholder": "{{IMAGE:南方小镇的青石板路}}",
"description": "南方小镇的青石板路",
"status": "mystery",
}
],
},
)()
"""图片初始化已迁移到 _save_narrative_to_sections此处为兼容 no-op"""
chapter = type("ChapterStub", (), {"id": "chapter-1", "title": "童年的夏天", "category": "childhood"})()
with unittest.mock.patch.dict(os.environ, {"MEMOIR_IMAGE_ENABLED": "true"}, clear=False):
assets = initialize_chapter_images(chapter)
self.assertEqual(len(assets), 1)
self.assertEqual(assets[0]["status"], "failed")
self.assertEqual(assets[0]["error"], "invalid image status: mystery")
self.assertEqual(assets, [])
def test_initialize_chapter_images_preserves_existing_completed_assets_beyond_effective_max(self):
chapter = type(
"ChapterStub",
(),
{
"id": "chapter-1",
"title": "童年的夏天",
"category": "childhood",
"content": (
"{{IMAGE:南方小镇的青石板路}}\n"
"{{IMAGE:奶奶坐在院子里的藤椅上}}\n"
"{{IMAGE:门前的老槐树}}"
),
"images": [
{
"index": 0,
"placeholder": "{{IMAGE:南方小镇的青石板路}}",
"description": "南方小镇的青石板路",
"status": "completed",
"url": "https://cos.example.com/1.png",
},
{
"index": 1,
"placeholder": "{{IMAGE:奶奶坐在院子里的藤椅上}}",
"description": "奶奶坐在院子里的藤椅上",
"status": "completed",
"url": "https://cos.example.com/2.png",
},
{
"index": 2,
"placeholder": "{{IMAGE:门前的老槐树}}",
"description": "门前的老槐树",
"status": "completed",
"url": "https://cos.example.com/3.png",
},
],
},
)()
"""图片初始化已迁移到 _save_narrative_to_sections此处为兼容 no-op"""
chapter = type("ChapterStub", (), {"id": "chapter-1", "title": "童年的夏天", "category": "childhood"})()
with unittest.mock.patch.dict(
os.environ,
{
"MEMOIR_IMAGE_ENABLED": "true",
"MEMOIR_IMAGE_MAX_PER_CHAPTER": "2",
"MEMOIR_IMAGE_CHARS_PER_EXTRA": "99999",
"MEMOIR_IMAGE_MAX_CAP": "8",
},
{"MEMOIR_IMAGE_ENABLED": "true", "MEMOIR_IMAGE_MAX_PER_CHAPTER": "2"},
clear=False,
):
assets = initialize_chapter_images(chapter)
self.assertEqual(len(assets), 3)
self.assertEqual(
[asset["placeholder"] for asset in assets],
[
"{{IMAGE:南方小镇的青石板路}}",
"{{IMAGE:奶奶坐在院子里的藤椅上}}",
"{{IMAGE:门前的老槐树}}",
],
)
self.assertTrue(all(asset["status"] == "completed" for asset in assets))
self.assertEqual(assets, [])
def test_initialize_chapter_images_increases_limit_for_long_content(self):
chapter = type(
"ChapterStub",
(),
{
"id": "chapter-1",
"title": "童年的夏天",
"category": "childhood",
"content": (
("很长的正文" * 800)
+ "\n{{IMAGE:南方小镇的青石板路}}"
+ "\n{{IMAGE:奶奶坐在院子里的藤椅上}}"
+ "\n{{IMAGE:门前的老槐树}}"
+ "\n{{IMAGE:夏夜的晒谷场}}"
),
"images": [],
},
)()
"""图片初始化已迁移到 _save_narrative_to_sections此处为兼容 no-op"""
chapter = type("ChapterStub", (), {"id": "chapter-1", "title": "童年的夏天", "category": "childhood"})()
with unittest.mock.patch.dict(
os.environ,
{
"MEMOIR_IMAGE_ENABLED": "true",
"MEMOIR_IMAGE_MAX_PER_CHAPTER": "2",
"MEMOIR_IMAGE_CHARS_PER_EXTRA": "1000",
"MEMOIR_IMAGE_MAX_CAP": "8",
},
clear=False,
):
with unittest.mock.patch.dict(os.environ, {"MEMOIR_IMAGE_ENABLED": "true"}, clear=False):
assets = initialize_chapter_images(chapter)
self.assertEqual(len(assets), 4)
self.assertTrue(all(asset["status"] == "pending" for asset in assets))
self.assertEqual(assets, [])
def test_initialize_chapter_images_caps_dynamic_limit_at_max_images_cap(self):
chapter = type(
"ChapterStub",
(),
{
"id": "chapter-1",
"title": "童年的夏天",
"category": "childhood",
"content": (
("很长的正文" * 1600)
+ "\n{{IMAGE:图1}}"
+ "\n{{IMAGE:图2}}"
+ "\n{{IMAGE:图3}}"
+ "\n{{IMAGE:图4}}"
+ "\n{{IMAGE:图5}}"
+ "\n{{IMAGE:图6}}"
),
"images": [],
},
)()
"""图片初始化已迁移到 _save_narrative_to_sections此处为兼容 no-op"""
chapter = type("ChapterStub", (), {"id": "chapter-1", "title": "童年的夏天", "category": "childhood"})()
with unittest.mock.patch.dict(
os.environ,
{
"MEMOIR_IMAGE_ENABLED": "true",
"MEMOIR_IMAGE_MAX_PER_CHAPTER": "2",
"MEMOIR_IMAGE_CHARS_PER_EXTRA": "1000",
"MEMOIR_IMAGE_MAX_CAP": "4",
},
clear=False,
):
with unittest.mock.patch.dict(os.environ, {"MEMOIR_IMAGE_ENABLED": "true"}, clear=False):
assets = initialize_chapter_images(chapter)
self.assertEqual(len(assets), 4)
self.assertEqual([asset["description"] for asset in assets], ["图1", "图2", "图3", "图4"])
self.assertEqual(assets, [])