Merge pull request #3873 from Juude/master

优化 api service,
This commit is contained in:
王召德 2025-09-05 17:57:39 +08:00 committed by GitHub
commit 148c8f7201
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 152 additions and 51 deletions

View File

@ -2,3 +2,4 @@
toc.pb
*.apks
.cursorrules
release_outputs/

View File

@ -61,8 +61,8 @@ This is our full multimodal language model (LLM) Android app
```
# Releases
## Version 0.7.3
+ Click here to [download](https://meta.alicdn.com/data/mnn/mnn_chat_0_7_3.apk)
## Version 0.7.3.1
+ Click here to [download](https://meta.alicdn.com/data/mnn/mnn_chat_0_7_3_1.apk)
+ Optimize ApiService
## Version 0.7.2
+ Click here to [download](https://meta.alicdn.com/data/mnn/mnn_chat_0_7_2.apk)

View File

@ -54,7 +54,7 @@
# Releases
## Version 0.7.3
+ 点击这里 [下载](https://meta.alicdn.com/data/mnn/mnn_chat_0_7_3.apk)
+ 点击这里 [下载](https://meta.alicdn.com/data/mnn/mnn_chat_0_7_3_1.apk)
+ 优化 API 服务
## 版本 0.7.2

View File

@ -59,8 +59,8 @@ android {
applicationId "com.alibaba.mnnllm.android"
minSdk 26
targetSdk 35
versionCode 703
versionName "0.7.3"
versionCode 731
versionName "0.7.3.1"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
externalNativeBuild {

View File

@ -74,6 +74,22 @@
background: white;
}
#model-select:disabled {
background-color: #f8f9fa;
color: #6c757d;
cursor: not-allowed;
opacity: 0.7;
}
.model-notice {
margin-top: 0.5rem;
padding: 0.5rem;
background-color: #fff3cd;
border: 1px solid #ffeaa7;
border-radius: 6px;
text-align: center;
}
.button {
padding: 0.5rem 1rem;
font-size: 0.9rem;
@ -254,10 +270,13 @@
<div class="model-selector">
<div class="model-row">
<label for="model-select">Current Model:</label>
<select id="model-select">
<select id="model-select" disabled>
<option value="unknown">Loading models...</option>
</select>
<button class="button button-secondary" onclick="refreshModels()">Refresh Models</button>
<button class="button button-secondary" onclick="showModelSwitchAlert()">Switch Model</button>
</div>
<div class="model-notice">
<small style="color: #6c757d;">⚠️ Model switching is temporarily not supported. Pleas change models in the app.</small>
</div>
</div>
@ -299,9 +318,14 @@
refreshModels();
};
// 显示模型切换提醒
function showModelSwitchAlert() {
alert("Model switching is temporarily not supported.\n\nTo change models, please:\n1. Close this web page\n2. Restart the MNN Chat app\n3. Select a different model from the model list");
}
async function refreshModels() {
try {
updateStatus("Loading models...");
updateStatus("Loading current model...");
const response = await fetch("/v1/models", {
method: "GET",
headers: {
@ -324,21 +348,16 @@
modelSelect.innerHTML = '<option value="unknown">No models available</option>';
updateStatus("No models available");
} else {
availableModels.forEach(model => {
// 只显示第一个模型(当前使用的模型)
const currentModelData = availableModels[0];
const option = document.createElement("option");
option.value = model.id;
// 只显示模型名称的后缀部分
const modelName = extractModelSuffix(model.id);
option.textContent = modelName;
option.value = currentModelData.id;
option.textContent = extractModelSuffix(currentModelData.id);
modelSelect.appendChild(option);
});
// Set first model as default
if (availableModels.length > 0) {
currentModel = availableModels[0].id;
currentModel = currentModelData.id;
modelSelect.value = currentModel;
updateStatus(`Model loaded: ${extractModelSuffix(currentModel)}`);
}
updateStatus(`Current model: ${extractModelSuffix(currentModel)}`);
}
} catch (error) {
console.error("Failed to load models:", error);
@ -378,11 +397,7 @@
return suffix || modelId;
}
// Handle model selection change
document.getElementById("model-select").addEventListener("change", function() {
currentModel = this.value;
updateStatus(`Model switched to: ${extractModelSuffix(currentModel)}`);
});
// Model selection change is disabled - no event listener needed
function updateStatus(message) {
document.getElementById("status").textContent = message;

View File

@ -337,7 +337,7 @@ class ChatActivity : AppCompatActivity() {
}
// Check API service settings and start service
if (isApiServiceEnabled(this)) {
ApiServiceManager.startApiService(this)
ApiServiceManager.startApiService(this, modelId)
}
}
}

View File

@ -16,12 +16,13 @@ object ApiServiceManager {
/**
* 启动API服务
* @param context 上下文必须是ChatActivity实例
* @param modelId 当前模型ID
* @return 是否成功启动
*/
fun startApiService(context: Context): Boolean {
fun startApiService(context: Context, modelId: String? = null): Boolean {
return try {
OpenAIService.startService(context)
Timber.tag(TAG).i("API service start requested")
OpenAIService.startService(context, modelId)
Timber.tag(TAG).i("API service start requested with modelId: $modelId")
true
} catch (e: Exception) {
Timber.tag(TAG).e(e, "Failed to start API service")

View File

@ -0,0 +1,28 @@
package com.alibaba.mnnllm.api.openai.manager
/**
* Current model manager
* Used to store and access the currently active model ID in the API service
*/
object CurrentModelManager {
private var currentModelId: String? = null
/**
* Set current model ID
*/
fun setCurrentModelId(modelId: String?) {
currentModelId = modelId
}
/**
* Get current model ID
*/
fun getCurrentModelId(): String? = currentModelId
/**
* Clear current model ID
*/
fun clearCurrentModelId() {
currentModelId = null
}
}

View File

@ -136,7 +136,6 @@ class ApiNotificationManager(private val context: Context) {
.setOngoing(true)
.setAutoCancel(false)
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
.setContentIntent(mainActivityPendingIntent)
// .addAction(
// R.drawable.ic_dialog_alert,
// context.getString(com.alibaba.mnnllm.android.R.string.api_service_stop),

View File

@ -6,6 +6,7 @@ import com.alibaba.mnnllm.api.openai.network.models.ModelPermission
import com.alibaba.mnnllm.api.openai.network.models.ModelsResponse
import com.alibaba.mnnllm.android.modelist.ModelListManager
import com.alibaba.mnnllm.android.MnnLlmApplication
import com.alibaba.mnnllm.api.openai.manager.CurrentModelManager
import io.ktor.http.HttpStatusCode
import io.ktor.server.application.call
import io.ktor.server.response.respond
@ -23,31 +24,58 @@ class MNNModelsService {
try {
logger.logRequestStart(traceId, call)
val currentModelId = CurrentModelManager.getCurrentModelId()
if (currentModelId == null) {
Timber.tag("MNNModelsService").w("No current model ID available")
logger.logError(traceId, Exception("No current model ID available"), "No current model ID available")
call.respond(HttpStatusCode.InternalServerError, mapOf("error" to "No current model available"))
return
}
val context = MnnLlmApplication.getAppContext()
val availableModels = runBlocking {
ModelListManager.loadAvailableModels(context)
}
val modelDataList = availableModels.map { modelWrapper ->
ModelData(
id = modelWrapper.modelItem.modelId ?: "unknown",
// 只返回当前正在使用的模型
val currentModelWrapper = availableModels.find {
it.modelItem.modelId == currentModelId
}
val modelDataList = if (currentModelWrapper != null) {
listOf(ModelData(
id = currentModelWrapper.modelItem.modelId ?: "unknown",
created = System.currentTimeMillis() / 1000, // Unix timestamp
permission = listOf(
ModelPermission(
id = "modelperm-${modelWrapper.modelItem.modelId}",
id = "modelperm-${currentModelWrapper.modelItem.modelId}",
created = System.currentTimeMillis() / 1000
)
)
))
} else {
// 如果找不到当前模型,返回一个默认的模型数据
listOf(ModelData(
id = currentModelId,
created = System.currentTimeMillis() / 1000,
permission = listOf(
ModelPermission(
id = "modelperm-$currentModelId",
created = System.currentTimeMillis() / 1000
)
)
))
}
val response = ModelsResponse(data = modelDataList)
call.respond(response)
logger.logInfo(traceId, "Models list returned successfully")
logger.logInfo(traceId, "Current model returned successfully: $currentModelId")
} catch (e: Exception) {
Timber.tag("MNNModelsService").e(e, "Error getting available models")
logger.logError(traceId, e, "Failed to get available models")
call.respond(HttpStatusCode.InternalServerError, mapOf("error" to "Failed to get available models"))
Timber.tag("MNNModelsService").e(e, "Error getting current model")
logger.logError(traceId, e, "Failed to get current model")
call.respond(HttpStatusCode.InternalServerError, mapOf("error" to "Failed to get current model"))
}
}
}

View File

@ -16,17 +16,19 @@ import androidx.core.content.ContextCompat
import com.alibaba.mnnllm.android.chat.ChatActivity
import com.alibaba.mnnllm.api.openai.service.ApiServiceCoordinator
import com.alibaba.mnnllm.api.openai.manager.ApiNotificationManager
import com.alibaba.mnnllm.api.openai.manager.CurrentModelManager
import timber.log.Timber
class OpenAIService : Service() {
private val TAG = this::class.java.simpleName
private lateinit var coordinator: ApiServiceCoordinator
private var currentModelId: String? = null
companion object {
private var isServiceRunning = false
private var serviceConnection: ServiceConnection? = null
fun startService(context: Context) {
fun startService(context: Context, modelId: String? = null) {
if (context !is ChatActivity) {
Timber.tag("ServiceStartCondition").w("Invalid context. Not starting service.")
return
@ -44,6 +46,8 @@ class OpenAIService : Service() {
}
val serviceIntent = Intent(context, OpenAIService::class.java)
// 传递 modelId 到服务
modelId?.let { serviceIntent.putExtra("modelId", it) }
isServiceRunning = true
try {
context.startForegroundService(serviceIntent)
@ -135,6 +139,14 @@ class OpenAIService : Service() {
stopSelf()
return START_NOT_STICKY
}
// 获取传递的 modelId
intent?.getStringExtra("modelId")?.let { modelId ->
currentModelId = modelId
CurrentModelManager.setCurrentModelId(modelId)
Timber.tag(TAG).i("Service started with modelId: $modelId")
}
val notification = coordinator.getNotification()
if (notification != null) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
@ -187,6 +199,9 @@ class OpenAIService : Service() {
Timber.tag(TAG).e(e, "Failed to cleanup coordinator")
}
// 清除全局模型ID
CurrentModelManager.clearCurrentModelId()
try {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
stopForeground(STOP_FOREGROUND_REMOVE)
@ -218,4 +233,6 @@ class OpenAIService : Service() {
fun getServerPort(): Int? = coordinator.getServerPort()
fun isServerRunning(): Boolean = coordinator.isServerRunning
fun getCurrentModelId(): String? = currentModelId
}

View File

@ -107,11 +107,15 @@ build_standard_debug() {
./gradlew assembleStandardDebug
# Copy APK to output directory
# Generate version-based filename (replace dots with underscores)
VERSION_FILENAME=$(echo "$VERSION_NAME" | sed 's/\./_/g')
APK_FILENAME="mnn_chat_${VERSION_FILENAME}.apk"
# Copy APK to output directory with version-based name
APK_PATH="$BUILD_DIR/outputs/apk/standard/debug/app-standard-debug.apk"
if [[ -f "$APK_PATH" ]]; then
cp "$APK_PATH" "$CDN_UPLOAD_DIR/"
log_success "Standard debug APK built: $CDN_UPLOAD_DIR/app-standard-debug.apk"
cp "$APK_PATH" "$CDN_UPLOAD_DIR/$APK_FILENAME"
log_success "Standard debug APK built: $CDN_UPLOAD_DIR/$APK_FILENAME"
else
log_error "Standard debug APK not found at $APK_PATH"
exit 1
@ -171,13 +175,17 @@ upload_to_cdn() {
# Configure ossutil
ossutil config -e "$CDN_ENDPOINT" -i "$CDN_ACCESS_KEY" -k "$CDN_SECRET_KEY"
# Generate version-based filename for upload
VERSION_FILENAME=$(echo "$VERSION_NAME" | sed 's/\./_/g')
APK_FILENAME="mnn_chat_${VERSION_FILENAME}.apk"
# Upload APK to CDN
APK_FILE="$CDN_UPLOAD_DIR/app-standard-debug.apk"
APK_FILE="$CDN_UPLOAD_DIR/$APK_FILENAME"
if [[ -f "$APK_FILE" ]]; then
ossutil cp "$APK_FILE" "oss://$CDN_BUCKET/releases/$VERSION_NAME/app-standard-debug-$VERSION_NAME-$BUILD_DATE.apk"
log_success "APK uploaded to CDN: oss://$CDN_BUCKET/releases/$VERSION_NAME/app-standard-debug-$VERSION_NAME-$BUILD_DATE.apk"
ossutil cp "$APK_FILE" "oss://$CDN_BUCKET/releases/$VERSION_NAME/$APK_FILENAME"
log_success "APK uploaded to CDN: oss://$CDN_BUCKET/releases/$VERSION_NAME/$APK_FILENAME"
else
log_error "APK file not found for CDN upload"
log_error "APK file not found for CDN upload: $APK_FILE"
fi
}
@ -237,6 +245,10 @@ EOF
generate_release_notes() {
log_info "Generating release notes..."
# Generate version-based filename for documentation
VERSION_FILENAME=$(echo "$VERSION_NAME" | sed 's/\./_/g')
APK_FILENAME="mnn_chat_${VERSION_FILENAME}.apk"
RELEASE_NOTES_FILE="$OUTPUT_DIR/release_notes.md"
cat > "$RELEASE_NOTES_FILE" << EOF
# Release Notes - $PROJECT_NAME v$VERSION_NAME
@ -250,7 +262,7 @@ generate_release_notes() {
## Build Outputs
### Standard Flavor (Debug)
- **APK**: \`app-standard-debug.apk\`
- **APK**: \`$APK_FILENAME\`
- **Purpose**: CDN distribution
- **Location**: \`$CDN_UPLOAD_DIR/\`