Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -326,6 +326,12 @@ class OperitApplication : Application(), ImageLoaderFactory, WorkConfiguration.P
AppLogger.d(TAG, "【启动计时】全局图片加载器初始化完成(超时配置:连接30s/读取60s) - ${System.currentTimeMillis() - startTime}ms")

// 初始化图片池管理器,支持本地持久化缓存
// 先读取「保留聊天图片」设置再初始化,避免启动即清空磁盘图片
val keepChatImages =
runBlocking(Dispatchers.IO) {
UserPreferencesManager.getInstance(applicationContext).keepChatImages.first()
}
ImagePoolManager.keepChatImages = keepChatImages
ImagePoolManager.initialize(filesDir, preloadNow = false)
AppLogger.d(TAG, "【启动计时】图片池管理器初始化完成 - ${System.currentTimeMillis() - startTime}ms")

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ class UserPreferencesManager private constructor(private val context: Context) {
// 主题设置相关键
private val THEME_MODE = stringPreferencesKey("theme_mode")
private val USE_SYSTEM_THEME = booleanPreferencesKey("use_system_theme")
private val KEEP_CHAT_IMAGES = booleanPreferencesKey("keep_chat_images")
private val CUSTOM_PRIMARY_COLOR = intPreferencesKey("custom_primary_color")
private val CUSTOM_SECONDARY_COLOR = intPreferencesKey("custom_secondary_color")
private val USE_CUSTOM_COLORS = booleanPreferencesKey("use_custom_colors")
Expand Down Expand Up @@ -557,6 +558,18 @@ class UserPreferencesManager private constructor(private val context: Context) {
preferences[USE_SYSTEM_THEME] ?: true
}

// 保留聊天图片(开启后图片不在重启/缓存淘汰时丢失)
val keepChatImages: Flow<Boolean> =
context.userPreferencesDataStore.data.map { preferences ->
preferences[KEEP_CHAT_IMAGES] ?: false
}

suspend fun setKeepChatImages(value: Boolean) {
context.userPreferencesDataStore.edit { preferences ->
preferences[KEEP_CHAT_IMAGES] = value
}
}

val customPrimaryColor: Flow<Int?> =
context.userPreferencesDataStore.data.map { preferences ->
preferences[CUSTOM_PRIMARY_COLOR]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,9 @@ fun ChatHistorySettingsScreen() {
val userPreferencesManager = remember { UserPreferencesManager.getInstance(context) }
val activeProfileId by userPreferencesManager.activeMemorySpaceIdFlow.collectAsState(initial = "default")

// 保留聊天图片开关状态
val keepChatImages by userPreferencesManager.keepChatImages.collectAsState(initial = false)

val characterCardStatsState by chatHistoryManager.characterCardStatsFlow
.collectAsState(initial = null as List<CharacterCardChatStats>?)
val characterCardStats = characterCardStatsState ?: emptyList()
Expand Down Expand Up @@ -275,6 +278,40 @@ fun ChatHistorySettingsScreen() {
activeProfileName = activeProfileName
)
}

// 保留聊天图片设置
item {
ElevatedCard(modifier = Modifier.fillMaxWidth()) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(20.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween
) {
Column(modifier = Modifier.weight(1f).padding(end = 16.dp)) {
Text(
text = stringResource(id = R.string.keep_chat_images),
style = MaterialTheme.typography.titleMedium
)
Spacer(modifier = Modifier.height(4.dp))
Text(
text = stringResource(id = R.string.keep_chat_images_desc),
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
Switch(
checked = keepChatImages,
onCheckedChange = { enabled ->
scope.launch {
userPreferencesManager.setKeepChatImages(enabled)
}
}
)
}
}
}
item {
CharacterCardStatsCard(
stats = characterCardStats,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,14 @@ object ImagePoolManager {
}
}

/**
* 是否保留聊天图片。开启后:
* 1) 应用启动时不再清空磁盘图片缓存;
* 2) LRU 淘汰时仅将图片移出内存池,不删除磁盘文件。
* 由用户设置控制,默认 false(保持原有行为)。
*/
var keepChatImages: Boolean = false

private var cacheDir: File? = null
private var hasResetCacheOnInitialize = false

Expand Down Expand Up @@ -86,7 +94,10 @@ object ImagePoolManager {
val shouldRemove = size > maxPoolSize
if (shouldRemove && eldest != null) {
AppLogger.d(TAG, "池子已满,移除最旧的图片: ${eldest.key}")
deleteFromDisk(eldest.key)
// 保留聊天图片模式下,仅移出内存池,不删除磁盘文件
if (!keepChatImages) {
deleteFromDisk(eldest.key)
}
}
return shouldRemove
}
Expand All @@ -109,7 +120,10 @@ object ImagePoolManager {

if (shouldResetCache) {
imagePool.clear()
clearDiskCache()
// 保留聊天图片模式下,启动不清空磁盘缓存,避免用户已保存的图片丢失
if (!keepChatImages) {
clearDiskCache()
}
hasResetCacheOnInitialize = true
AppLogger.d(TAG, "启动时已清空旧图片池缓存")
}
Expand Down
4 changes: 4 additions & 0 deletions app/src/main/res/values-en/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -7960,6 +7960,7 @@
<string name="ai_message_use_package_failed">use_package call failed: %1$s</string>
<string name="ai_message_use_package_empty">use_package returned empty.</string>
<string name="ai_message_package_warmup_item">%1$d. Package %2$s (hit %3$d times)</string>

<string name="artifact_publish_encrypt_market_toolpkg_title">Minify and Obfuscate for Publishing</string>

<string name="market_manage_resubmit">Resubmit</string>
Expand Down Expand Up @@ -8003,4 +8004,7 @@
<string name="notification_kind_review_rejected">Rejected</string>
<string name="notification_kind_review_changes">Changes Required</string>
<string name="notification_kind_entry_curated">Featured</string>

<string name="keep_chat_images">Keep chat images</string>
<string name="keep_chat_images_desc">When enabled, images in chats are not deleted on app restart or cache eviction, and are kept locally for the long term.</string>
</resources>
3 changes: 3 additions & 0 deletions app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -7983,4 +7983,7 @@
<string name="notification_kind_review_changes">需要修改</string>
<string name="notification_kind_entry_curated">入选精选</string>

<string name="keep_chat_images">保留聊天图片</string>
<string name="keep_chat_images_desc">开启后,聊天中的图片不会在应用重启或缓存淘汰时被删除,会长期保留在本地。</string>

</resources>
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
package com.ai.assistance.operit.core.tools.calculator

import org.junit.Assert.assertEquals
import org.junit.Assert.assertThrows
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Test

/**
* JsCalculator 回归测试。
*
* 覆盖表达式求值的核心路径(运算符优先级、括号、幂、三元、变量、内置函数、
* 统计函数、常量、无穷大与错误输入),防止后续对 core/tools 的重构破坏
* 计算器工具的基础行为。
*
* 注意:JsCalculator / ExpressionContext 均为纯 JVM 实现(无 Android 依赖),
* 因此可在单元测试中直接运行。
*/
class JsCalculatorRegressionTest {

@Before
fun setUp() {
// ExpressionContext 持有全局可变变量,测试间需重置以避免污染
JsCalculator.clearVariables()
}

@Test
fun arithmeticPrecedence_multiplicationBeforeAddition() {
assertEquals("7", JsCalculator.calc("1 + 2 * 3"))
}

@Test
fun parentheses_overridePrecedence() {
assertEquals("9", JsCalculator.calc("(1 + 2) * 3"))
}

@Test
fun division_producesFractionalResult() {
assertEquals("2.5", JsCalculator.calc("10 / 4"))
}

@Test
fun power_operator() {
assertEquals("1024", JsCalculator.calc("2 ^ 10"))
}

@Test
fun ternary_operator_selectsBranch() {
assertEquals("20", JsCalculator.calc("1 > 2 ? 10 : 20"))
assertEquals("10", JsCalculator.calc("2 > 1 ? 10 : 20"))
}

@Test
fun variable_assignmentAndReadback() {
JsCalculator.setVariable("x", 5.0)
assertEquals("10", JsCalculator.calc("x * 2"))
}

@Test
fun builtinMathFunctions() {
assertEquals("4", JsCalculator.calc("sqrt(16)"))
assertEquals("7", JsCalculator.calc("abs(-7)"))
assertEquals("9", JsCalculator.calc("max(3, 9, 4)"))
assertEquals("3", JsCalculator.calc("min(3, 9, 4)"))
assertEquals("3", JsCalculator.calc("round(2.6)"))
}

@Test
fun statsMean_function() {
assertEquals("2.5", JsCalculator.calc("stats.mean(1, 2, 3, 4)"))
}

@Test
fun constants_piIsAvailable() {
assertTrue(JsCalculator.calc("PI").startsWith("3.14159"))
}

@Test
fun divisionByZero_yieldsInfinity() {
assertEquals("Infinity", JsCalculator.calc("1 / 0"))
}

@Test
fun malformedExpression_throws() {
assertThrows(RuntimeException::class.java) {
JsCalculator.evaluate("1 +")
}
}

@Test
fun evaluateReturnsDouble_forArithmetic() {
assertEquals(7.0, JsCalculator.evaluate("1 + 2 * 3"), 0.0)
}
}
Loading