mirror of https://github.com/alibaba/MNN.git
commit
148c8f7201
|
@ -1,4 +1,5 @@
|
|||
*.jks
|
||||
toc.pb
|
||||
*.apks
|
||||
.cursorrules
|
||||
.cursorrules
|
||||
release_outputs/
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 option = document.createElement("option");
|
||||
option.value = model.id;
|
||||
// 只显示模型名称的后缀部分
|
||||
const modelName = extractModelSuffix(model.id);
|
||||
option.textContent = modelName;
|
||||
modelSelect.appendChild(option);
|
||||
});
|
||||
// 只显示第一个模型(当前使用的模型)
|
||||
const currentModelData = availableModels[0];
|
||||
const option = document.createElement("option");
|
||||
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;
|
||||
modelSelect.value = currentModel;
|
||||
updateStatus(`Model loaded: ${extractModelSuffix(currentModel)}`);
|
||||
}
|
||||
currentModel = currentModelData.id;
|
||||
modelSelect.value = 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;
|
||||
|
|
|
@ -337,7 +337,7 @@ class ChatActivity : AppCompatActivity() {
|
|||
}
|
||||
// Check API service settings and start service
|
||||
if (isApiServiceEnabled(this)) {
|
||||
ApiServiceManager.startApiService(this)
|
||||
ApiServiceManager.startApiService(this, modelId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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),
|
||||
|
|
|
@ -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"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
@ -186,6 +198,9 @@ class OpenAIService : Service() {
|
|||
} catch (e: Exception) {
|
||||
Timber.tag(TAG).e(e, "Failed to cleanup coordinator")
|
||||
}
|
||||
|
||||
// 清除全局模型ID
|
||||
CurrentModelManager.clearCurrentModelId()
|
||||
|
||||
try {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
|
@ -218,4 +233,6 @@ class OpenAIService : Service() {
|
|||
fun getServerPort(): Int? = coordinator.getServerPort()
|
||||
|
||||
fun isServerRunning(): Boolean = coordinator.isServerRunning
|
||||
|
||||
fun getCurrentModelId(): String? = currentModelId
|
||||
}
|
||||
|
|
|
@ -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/\`
|
||||
|
||||
|
|
Loading…
Reference in New Issue