fix: 修复 Liblib provider 认证和多个图片生成关键缺陷

- 重写 LiblibImageProvider:Bearer token 改为 HMAC-SHA1 签名认证,
  适配 Liblib 真实 API(Star-3 Alpha 文生图端点)
- 修复 chapter.images JSON 列原地修改不持久化(深拷贝+整列重赋值)
- 修复 generate_chapter_images 在事务提交前派发(改为 commit 后统一 delay)
- 修复 initialize_chapter_images 覆盖已完成图片(新增 merge 去重逻辑)
- 修复 Android failed 图片渲染为错误卡片(改为隐藏,保持正文连续)
- 模型模板 UUID 改为环境变量配置(LIBLIB_TEMPLATE_UUID)
- 更新 .env 凭证格式为 ACCESS_KEY/SECRET_KEY
- 补充 test_memoir_image_bootstrap 缺失的 unittest.mock 导入

Made-with: Cursor
This commit is contained in:
Kevin
2026-03-10 17:02:50 +08:00
parent 830b6efc39
commit 0970cb7408
11 changed files with 484 additions and 111 deletions

View File

@@ -1,6 +1,7 @@
package com.huaga.life_echo.ui.components.memoir
import androidx.activity.ComponentActivity
import androidx.compose.ui.test.assertDoesNotExist
import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.junit4.createAndroidComposeRule
import androidx.compose.ui.test.onNodeWithTag
@@ -50,7 +51,7 @@ class ChapterReadingImageBlocksTest {
}
@Test
fun chapterReadingView_showsFailureCard_forFailedImage_withoutRawPlaceholderText() {
fun chapterReadingView_hidesFailedImageBlock() {
val chapter = ChapterContentDto(
id = "chapter-1",
title = "童年的夏天",
@@ -81,6 +82,6 @@ class ChapterReadingImageBlocksTest {
composeRule.setContent { ChapterReadingView(chapter = chapter) }
composeRule.onNodeWithTag("memoir-image-error-0").assertIsDisplayed()
composeRule.onNodeWithTag("memoir-image-error-0").assertDoesNotExist()
}
}

View File

@@ -24,7 +24,7 @@ fun splitMemoirContent(content: String, images: List<ChapterImageDto>): List<Mem
if (before.isNotBlank()) blocks += MemoirContentBlock.Text(before)
if (image.status == "completed" && !image.url.isNullOrBlank()) {
blocks += MemoirContentBlock.Image(image)
} else if (image.status in listOf("pending", "processing", "failed")) {
} else if (image.status == "pending" || image.status == "processing") {
blocks += MemoirContentBlock.Image(image)
}
remaining = parts.getOrElse(1) { "" }

View File

@@ -8,10 +8,8 @@ import androidx.compose.animation.core.tween
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
@@ -21,7 +19,6 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import coil.compose.AsyncImage
import com.huaga.life_echo.network.models.ChapterImageDto
@@ -73,29 +70,6 @@ fun MemoirInlineImage(
)
}
}
"failed" -> Column(
modifier = modifier
.fillMaxWidth()
.clip(RoundedCornerShape(16.dp))
.background(LightPurple.copy(alpha = 0.10f))
.padding(16.dp)
.testTag("memoir-image-error-${image.index}")
) {
Text(
text = "图片生成失败",
fontSize = AppTypography.bodyMedium,
fontWeight = FontWeight.Medium,
color = SlatePurple,
)
if (image.description.isNotBlank()) {
Text(
text = image.description,
fontSize = AppTypography.captionMedium,
color = SlatePurple.copy(alpha = 0.6f),
modifier = Modifier.padding(top = 4.dp),
)
}
}
else -> Unit
}
}

View File

@@ -2,6 +2,7 @@ package com.huaga.life_echo.ui.components.memoir
import com.huaga.life_echo.network.models.ChapterImageDto
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Test
@@ -56,4 +57,33 @@ class MemoirContentBlocksTest {
val text = (blocks[0] as MemoirContentBlock.Text).content
assertTrue(!text.contains("IMAGE:"))
}
@Test
fun splitMemoirContent_skipsFailedImages_andRemovesTheirPlaceholders() {
val blocks = splitMemoirContent(
content = "开头。\n\n{{{{IMAGE:生成失败的图}}}}\n\n结尾。",
images = listOf(
ChapterImageDto(
index = 0,
placeholder = "{{{{IMAGE:生成失败的图}}}}",
description = "生成失败的图",
prompt = null,
url = null,
status = "failed",
provider = "liblib",
style = "watercolor",
size = "1024x1024",
error = "provider timeout",
created_at = null,
updated_at = null,
)
)
)
assertFalse(blocks.any { it is MemoirContentBlock.Image })
val combinedText = blocks.filterIsInstance<MemoirContentBlock.Text>().joinToString("\n") { it.content }
assertFalse(combinedText.contains("IMAGE:"))
assertTrue(combinedText.contains("开头"))
assertTrue(combinedText.contains("结尾"))
}
}