feat: restore android build path and update status docs
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -23,6 +23,8 @@ build/
|
||||
|
||||
# Runtime data and artifacts
|
||||
data/
|
||||
!android-app/app/src/main/java/com/aiglasses/app/data/
|
||||
!android-app/app/src/main/java/com/aiglasses/app/data/**
|
||||
output/
|
||||
*.log
|
||||
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
package com.aiglasses.app.data
|
||||
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.ExperimentalSerializationApi
|
||||
import java.util.concurrent.TimeUnit
|
||||
import okhttp3.MediaType.Companion.toMediaType
|
||||
import okhttp3.Protocol
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import okhttp3.logging.HttpLoggingInterceptor
|
||||
import retrofit2.Retrofit
|
||||
import retrofit2.create
|
||||
import com.jakewharton.retrofit2.converter.kotlinx.serialization.asConverterFactory
|
||||
|
||||
object ApiClient {
|
||||
@OptIn(ExperimentalSerializationApi::class)
|
||||
val json = Json {
|
||||
ignoreUnknownKeys = true
|
||||
explicitNulls = false
|
||||
}
|
||||
|
||||
inline fun <reified T : Any> createService(baseUrl: String): T {
|
||||
val logging = HttpLoggingInterceptor().apply {
|
||||
level = HttpLoggingInterceptor.Level.BODY
|
||||
}
|
||||
val client = OkHttpClient.Builder()
|
||||
.protocols(listOf(Protocol.HTTP_1_1))
|
||||
.connectTimeout(12, TimeUnit.SECONDS)
|
||||
.readTimeout(20, TimeUnit.SECONDS)
|
||||
.writeTimeout(20, TimeUnit.SECONDS)
|
||||
.callTimeout(25, TimeUnit.SECONDS)
|
||||
.addInterceptor { chain ->
|
||||
val request: Request = chain.request().newBuilder()
|
||||
.header("Connection", "close")
|
||||
.build()
|
||||
chain.proceed(request)
|
||||
}
|
||||
.addInterceptor(logging)
|
||||
.build()
|
||||
|
||||
val normalizedBaseUrl = if (baseUrl.endsWith("/")) baseUrl else "$baseUrl/"
|
||||
|
||||
return Retrofit.Builder()
|
||||
.baseUrl(normalizedBaseUrl)
|
||||
.client(client)
|
||||
.addConverterFactory(json.asConverterFactory("application/json".toMediaType()))
|
||||
.build()
|
||||
.create<T>()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
package com.aiglasses.app.data
|
||||
|
||||
import retrofit2.http.Body
|
||||
import retrofit2.http.GET
|
||||
import retrofit2.http.Header
|
||||
import retrofit2.http.POST
|
||||
import retrofit2.http.Path
|
||||
import retrofit2.http.Query
|
||||
|
||||
interface ApiService {
|
||||
@GET("/healthz")
|
||||
suspend fun healthz(): ApiEnvelope<HealthzData>
|
||||
|
||||
@POST("/api/v1/devices/bind-confirm")
|
||||
suspend fun bindConfirm(
|
||||
@Body request: BindConfirmRequest
|
||||
): ApiEnvelope<BindConfirmData>
|
||||
|
||||
@POST("/api/v1/ai/sessions")
|
||||
suspend fun createSession(
|
||||
@Header("Idempotency-Key") idempotencyKey: String?,
|
||||
@Body request: CreateSessionRequest
|
||||
): ApiEnvelope<SessionData>
|
||||
|
||||
@POST("/api/v1/ai/sessions/{sessionId}/stop")
|
||||
suspend fun stopSession(
|
||||
@Path("sessionId") sessionId: String,
|
||||
@Body request: StopSessionRequest
|
||||
): ApiEnvelope<StopSessionData>
|
||||
|
||||
@POST("/api/v1/ai/sessions/{sessionId}/heartbeat")
|
||||
suspend fun heartbeat(
|
||||
@Path("sessionId") sessionId: String,
|
||||
@Body request: HeartbeatRequest
|
||||
): ApiEnvelope<HeartbeatData>
|
||||
|
||||
@GET("/api/v1/devices/{deviceId}/status")
|
||||
suspend fun getDeviceStatus(
|
||||
@Path("deviceId") deviceId: String
|
||||
): ApiEnvelope<DeviceStatusData>
|
||||
|
||||
@POST("/api/v1/events")
|
||||
suspend fun postEvent(
|
||||
@Body request: ClientEventRequest
|
||||
): ApiEnvelope<EventSavedData>
|
||||
|
||||
@POST("/api/v1/events/batch")
|
||||
suspend fun postEventsBatch(
|
||||
@Body request: ClientEventBatchRequest
|
||||
): ApiEnvelope<EventsBatchSavedData>
|
||||
|
||||
@POST("/api/v1/ai/sessions/{sessionId}/messages")
|
||||
suspend fun sendMessage(
|
||||
@Path("sessionId") sessionId: String,
|
||||
@Body request: SessionMessageRequest
|
||||
): ApiEnvelope<ProviderActionData>
|
||||
|
||||
@POST("/api/v1/ai/sessions/{sessionId}/scene-role")
|
||||
suspend fun switchRole(
|
||||
@Path("sessionId") sessionId: String,
|
||||
@Body request: SwitchRoleRequest
|
||||
): ApiEnvelope<ProviderActionData>
|
||||
|
||||
@POST("/api/v1/ai/sessions/{sessionId}/interrupt")
|
||||
suspend fun interruptSession(
|
||||
@Path("sessionId") sessionId: String,
|
||||
@Body request: SessionInterruptRequest
|
||||
): ApiEnvelope<ProviderActionData>
|
||||
|
||||
@GET("/api/v1/baidu/activation/query")
|
||||
suspend fun activationQuery(
|
||||
@Query("deviceId") deviceId: String,
|
||||
@Query("appId") appId: String? = null
|
||||
): ApiEnvelope<ActivationQueryData>
|
||||
|
||||
@POST("/api/v1/licenses/reload")
|
||||
suspend fun reloadLicenses(): ApiEnvelope<ReloadLicensesData>
|
||||
|
||||
@GET("/api/v1/admin/overview")
|
||||
suspend fun adminOverview(): ApiEnvelope<AdminOverviewData>
|
||||
|
||||
@GET("/api/v1/app/update/latest")
|
||||
suspend fun appUpdateLatest(
|
||||
@Query("platform") platform: String = "android",
|
||||
@Query("channel") channel: String = "stable",
|
||||
@Query("currentVersionCode") currentVersionCode: Int
|
||||
): ApiEnvelope<AppUpdateLatestData>
|
||||
|
||||
@GET("/v2/douyin/accounts")
|
||||
suspend fun listDouyinAccounts(): ApiEnvelope<List<DouyinAccountSummary>>
|
||||
|
||||
@POST("/v2/douyin/accounts/sync")
|
||||
suspend fun syncDouyinAccount(
|
||||
@Body request: DouyinAccountSyncRequest
|
||||
): ApiEnvelope<DouyinAccountWorkspace>
|
||||
|
||||
@GET("/v2/douyin/accounts/{accountId}")
|
||||
suspend fun getDouyinAccount(
|
||||
@Path("accountId") accountId: String
|
||||
): ApiEnvelope<DouyinAccountWorkspace>
|
||||
|
||||
@GET("/v2/douyin/accounts/{accountId}/workspace")
|
||||
suspend fun getDouyinWorkspace(
|
||||
@Path("accountId") accountId: String
|
||||
): ApiEnvelope<DouyinAccountWorkspace>
|
||||
|
||||
@GET("/v2/douyin/accounts/{accountId}/snapshots")
|
||||
suspend fun listDouyinSnapshots(
|
||||
@Path("accountId") accountId: String
|
||||
): ApiEnvelope<List<DouyinSnapshotSummary>>
|
||||
|
||||
@GET("/v2/douyin/accounts/{accountId}/snapshots/{snapshotId}")
|
||||
suspend fun getDouyinSnapshot(
|
||||
@Path("accountId") accountId: String,
|
||||
@Path("snapshotId") snapshotId: String
|
||||
): ApiEnvelope<DouyinSnapshotDetail>
|
||||
|
||||
@GET("/v2/douyin/accounts/{accountId}/creator-fields")
|
||||
suspend fun getDouyinCreatorFields(
|
||||
@Path("accountId") accountId: String
|
||||
): ApiEnvelope<DouyinSnapshotDetail>
|
||||
|
||||
@POST("/v2/douyin/accounts/{accountId}/analysis")
|
||||
suspend fun analyzeDouyinAccount(
|
||||
@Path("accountId") accountId: String,
|
||||
@Body request: DouyinAccountAnalysisRequest
|
||||
): ApiEnvelope<DouyinAnalysisResult>
|
||||
|
||||
@GET("/v2/douyin/accounts/{accountId}/analysis-reports")
|
||||
suspend fun listDouyinAnalysisReports(
|
||||
@Path("accountId") accountId: String
|
||||
): ApiEnvelope<List<DouyinAnalysisReport>>
|
||||
|
||||
@POST("/v2/douyin/similar-searches")
|
||||
suspend fun createDouyinSimilarSearch(
|
||||
@Body request: DouyinSimilarSearchRequest
|
||||
): ApiEnvelope<DouyinSimilaritySearchResult>
|
||||
|
||||
@GET("/v2/douyin/similar-searches/{searchId}")
|
||||
suspend fun getDouyinSimilarSearch(
|
||||
@Path("searchId") searchId: String
|
||||
): ApiEnvelope<DouyinSimilaritySearchDetail>
|
||||
|
||||
@GET("/v2/douyin/accounts/{accountId}/benchmark-links")
|
||||
suspend fun listDouyinBenchmarkLinks(
|
||||
@Path("accountId") accountId: String
|
||||
): ApiEnvelope<List<DouyinLinkedAccount>>
|
||||
|
||||
@POST("/v2/douyin/accounts/{accountId}/benchmark-links")
|
||||
suspend fun createDouyinBenchmarkLinks(
|
||||
@Path("accountId") accountId: String,
|
||||
@Body request: DouyinBenchmarkLinkRequest
|
||||
): ApiEnvelope<DouyinBenchmarkLinkResult>
|
||||
}
|
||||
@@ -0,0 +1,276 @@
|
||||
package com.aiglasses.app.data
|
||||
|
||||
import java.util.UUID
|
||||
|
||||
class BackendRepository(private var baseUrl: String) {
|
||||
private var api: ApiService = ApiClient.createService(baseUrl)
|
||||
|
||||
fun updateBaseUrl(url: String) {
|
||||
if (url != baseUrl) {
|
||||
baseUrl = url
|
||||
api = ApiClient.createService(baseUrl)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun bindDevice(deviceId: String, userId: String): BindConfirmData {
|
||||
val resp = api.bindConfirm(BindConfirmRequest(deviceId = deviceId, appUserId = userId))
|
||||
return resp.data
|
||||
}
|
||||
|
||||
suspend fun healthz(): HealthzData {
|
||||
val resp = api.healthz()
|
||||
return resp.data
|
||||
}
|
||||
|
||||
suspend fun createSession(deviceId: String, userId: String): SessionData {
|
||||
val idempotencyKey = "app-${UUID.randomUUID()}"
|
||||
val resp = api.createSession(
|
||||
idempotencyKey = idempotencyKey,
|
||||
request = CreateSessionRequest(deviceId = deviceId, appUserId = userId)
|
||||
)
|
||||
return resp.data
|
||||
}
|
||||
|
||||
suspend fun stopSession(sessionId: String): StopSessionData {
|
||||
val resp = api.stopSession(sessionId, StopSessionRequest())
|
||||
return resp.data
|
||||
}
|
||||
|
||||
suspend fun heartbeat(sessionId: String): HeartbeatData {
|
||||
val resp = api.heartbeat(sessionId, HeartbeatRequest())
|
||||
return resp.data
|
||||
}
|
||||
|
||||
suspend fun getDeviceStatus(deviceId: String): DeviceStatusData {
|
||||
val resp = api.getDeviceStatus(deviceId)
|
||||
return resp.data
|
||||
}
|
||||
|
||||
suspend fun postDemoEvent(deviceId: String, sessionId: String?): EventSavedData {
|
||||
return postEvent(
|
||||
deviceId = deviceId,
|
||||
sessionId = sessionId,
|
||||
eventType = "APP_DEBUG_PING",
|
||||
eventLevel = "INFO",
|
||||
payload = mapOf("source" to "android")
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun postEvent(
|
||||
deviceId: String,
|
||||
sessionId: String?,
|
||||
eventType: String,
|
||||
eventLevel: String = "INFO",
|
||||
payload: Map<String, String> = emptyMap()
|
||||
): EventSavedData {
|
||||
val resp = api.postEvent(
|
||||
ClientEventRequest(
|
||||
sessionId = sessionId,
|
||||
deviceId = deviceId,
|
||||
eventType = eventType,
|
||||
eventLevel = eventLevel,
|
||||
payload = payload
|
||||
)
|
||||
)
|
||||
return resp.data
|
||||
}
|
||||
|
||||
suspend fun postEventsBatch(events: List<ClientEventRequest>): EventsBatchSavedData {
|
||||
val resp = api.postEventsBatch(ClientEventBatchRequest(events = events))
|
||||
return resp.data
|
||||
}
|
||||
|
||||
suspend fun sendMessage(sessionId: String, message: String): ProviderActionData {
|
||||
val resp = api.sendMessage(
|
||||
sessionId = sessionId,
|
||||
request = SessionMessageRequest(message = message)
|
||||
)
|
||||
return resp.data
|
||||
}
|
||||
|
||||
suspend fun sendVoiceMessage(
|
||||
sessionId: String,
|
||||
pcmBase64: String,
|
||||
sampleRate: Int,
|
||||
durationMs: Int,
|
||||
rms: Int
|
||||
): ProviderActionData {
|
||||
val resp = api.sendMessage(
|
||||
sessionId = sessionId,
|
||||
request = SessionMessageRequest(
|
||||
message = "voice_chunk",
|
||||
messageType = "voice",
|
||||
extra = mapOf(
|
||||
"audio_base64" to pcmBase64,
|
||||
"audio_format" to "pcm_s16le",
|
||||
"sample_rate" to sampleRate.toString(),
|
||||
"channels" to "1",
|
||||
"duration_ms" to durationMs.toString(),
|
||||
"rms" to rms.toString(),
|
||||
"encoding" to "base64"
|
||||
)
|
||||
)
|
||||
)
|
||||
return resp.data
|
||||
}
|
||||
|
||||
suspend fun sendVisionMessage(
|
||||
sessionId: String,
|
||||
message: String,
|
||||
imageBase64: String,
|
||||
width: Int,
|
||||
height: Int,
|
||||
bytes: Int
|
||||
): ProviderActionData {
|
||||
val resp = api.sendMessage(
|
||||
sessionId = sessionId,
|
||||
request = SessionMessageRequest(
|
||||
message = message,
|
||||
messageType = "text",
|
||||
extra = mapOf(
|
||||
"image_base64" to imageBase64,
|
||||
"imageBase64" to imageBase64,
|
||||
"image" to imageBase64,
|
||||
"resource_base64" to imageBase64,
|
||||
"resourceBase64" to imageBase64,
|
||||
"image_encoding" to "base64",
|
||||
"imageEncoding" to "base64",
|
||||
"encoding" to "base64",
|
||||
"image_format" to "jpeg",
|
||||
"imageFormat" to "jpeg",
|
||||
"mime_type" to "image/jpeg",
|
||||
"mimeType" to "image/jpeg",
|
||||
"image_width" to width.toString(),
|
||||
"imageWidth" to width.toString(),
|
||||
"image_height" to height.toString(),
|
||||
"imageHeight" to height.toString(),
|
||||
"image_bytes" to bytes.toString(),
|
||||
"imageBytes" to bytes.toString(),
|
||||
"resource_type" to "image",
|
||||
"resourceType" to "image",
|
||||
"camera_source" to "android_phone",
|
||||
"multimodal" to "true",
|
||||
"with_vision" to "1"
|
||||
)
|
||||
)
|
||||
)
|
||||
return resp.data
|
||||
}
|
||||
|
||||
suspend fun switchRole(sessionId: String, sceneId: String, roleId: String): ProviderActionData {
|
||||
val resp = api.switchRole(
|
||||
sessionId = sessionId,
|
||||
request = SwitchRoleRequest(sceneId = sceneId, roleId = roleId)
|
||||
)
|
||||
return resp.data
|
||||
}
|
||||
|
||||
suspend fun interrupt(
|
||||
sessionId: String,
|
||||
interrupt: Boolean,
|
||||
extra: Map<String, String> = emptyMap()
|
||||
): ProviderActionData {
|
||||
val resp = api.interruptSession(
|
||||
sessionId = sessionId,
|
||||
request = SessionInterruptRequest(interrupt = interrupt, extra = extra)
|
||||
)
|
||||
return resp.data
|
||||
}
|
||||
|
||||
suspend fun activationQuery(deviceId: String): ActivationQueryData {
|
||||
val resp = api.activationQuery(deviceId = deviceId)
|
||||
return resp.data
|
||||
}
|
||||
|
||||
suspend fun reloadLicenses(): ReloadLicensesData {
|
||||
val resp = api.reloadLicenses()
|
||||
return resp.data
|
||||
}
|
||||
|
||||
suspend fun adminOverview(): AdminOverviewData {
|
||||
val resp = api.adminOverview()
|
||||
return resp.data
|
||||
}
|
||||
|
||||
suspend fun appUpdateLatest(currentVersionCode: Int): AppUpdateLatestData {
|
||||
val resp = api.appUpdateLatest(
|
||||
platform = "android",
|
||||
channel = "stable",
|
||||
currentVersionCode = currentVersionCode
|
||||
)
|
||||
return resp.data
|
||||
}
|
||||
|
||||
suspend fun listDouyinAccounts(): List<DouyinAccountSummary> {
|
||||
val resp = api.listDouyinAccounts()
|
||||
return resp.data
|
||||
}
|
||||
|
||||
suspend fun syncDouyinAccount(request: DouyinAccountSyncRequest): DouyinAccountWorkspace {
|
||||
val resp = api.syncDouyinAccount(request)
|
||||
return resp.data
|
||||
}
|
||||
|
||||
suspend fun getDouyinAccount(accountId: String): DouyinAccountWorkspace {
|
||||
val resp = api.getDouyinAccount(accountId)
|
||||
return resp.data
|
||||
}
|
||||
|
||||
suspend fun getDouyinWorkspace(accountId: String): DouyinAccountWorkspace {
|
||||
val resp = api.getDouyinWorkspace(accountId)
|
||||
return resp.data
|
||||
}
|
||||
|
||||
suspend fun listDouyinSnapshots(accountId: String): List<DouyinSnapshotSummary> {
|
||||
val resp = api.listDouyinSnapshots(accountId)
|
||||
return resp.data
|
||||
}
|
||||
|
||||
suspend fun getDouyinSnapshot(accountId: String, snapshotId: String): DouyinSnapshotDetail {
|
||||
val resp = api.getDouyinSnapshot(accountId, snapshotId)
|
||||
return resp.data
|
||||
}
|
||||
|
||||
suspend fun getDouyinCreatorFields(accountId: String): DouyinSnapshotDetail {
|
||||
val resp = api.getDouyinCreatorFields(accountId)
|
||||
return resp.data
|
||||
}
|
||||
|
||||
suspend fun analyzeDouyinAccount(
|
||||
accountId: String,
|
||||
request: DouyinAccountAnalysisRequest
|
||||
): DouyinAnalysisResult {
|
||||
val resp = api.analyzeDouyinAccount(accountId, request)
|
||||
return resp.data
|
||||
}
|
||||
|
||||
suspend fun listDouyinAnalysisReports(accountId: String): List<DouyinAnalysisReport> {
|
||||
val resp = api.listDouyinAnalysisReports(accountId)
|
||||
return resp.data
|
||||
}
|
||||
|
||||
suspend fun createDouyinSimilarSearch(
|
||||
request: DouyinSimilarSearchRequest
|
||||
): DouyinSimilaritySearchResult {
|
||||
val resp = api.createDouyinSimilarSearch(request)
|
||||
return resp.data
|
||||
}
|
||||
|
||||
suspend fun getDouyinSimilarSearch(searchId: String): DouyinSimilaritySearchDetail {
|
||||
val resp = api.getDouyinSimilarSearch(searchId)
|
||||
return resp.data
|
||||
}
|
||||
|
||||
suspend fun listDouyinBenchmarkLinks(accountId: String): List<DouyinLinkedAccount> {
|
||||
val resp = api.listDouyinBenchmarkLinks(accountId)
|
||||
return resp.data
|
||||
}
|
||||
|
||||
suspend fun createDouyinBenchmarkLinks(
|
||||
accountId: String,
|
||||
request: DouyinBenchmarkLinkRequest
|
||||
): DouyinBenchmarkLinkResult {
|
||||
val resp = api.createDouyinBenchmarkLinks(accountId, request)
|
||||
return resp.data
|
||||
}
|
||||
}
|
||||
540
android-app/app/src/main/java/com/aiglasses/app/data/Models.kt
Normal file
540
android-app/app/src/main/java/com/aiglasses/app/data/Models.kt
Normal file
@@ -0,0 +1,540 @@
|
||||
package com.aiglasses.app.data
|
||||
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.json.JsonElement
|
||||
import kotlinx.serialization.json.JsonObject
|
||||
|
||||
@Serializable
|
||||
data class ApiEnvelope<T>(
|
||||
val code: Int,
|
||||
val message: String,
|
||||
val traceId: String,
|
||||
val data: T
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class HealthzData(
|
||||
val status: String = "",
|
||||
val env: String = "",
|
||||
val dbPath: String = ""
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class BindConfirmRequest(
|
||||
val deviceId: String,
|
||||
val deviceSn: String? = null,
|
||||
val deviceModel: String? = null,
|
||||
val deviceFwVer: String? = null,
|
||||
val appUserId: String
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class BindConfirmData(
|
||||
val bindStatus: String,
|
||||
val licenseStatus: String,
|
||||
val licenseKeyMasked: String,
|
||||
val licenseKey: String = ""
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class CreateSessionRequest(
|
||||
val deviceId: String,
|
||||
val appUserId: String,
|
||||
val scene: String = "voice_assistant",
|
||||
val language: String = "zh-CN",
|
||||
val clientTs: Long? = null
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class SessionData(
|
||||
val sessionId: String,
|
||||
val provider: String,
|
||||
val cid: String,
|
||||
val token: String,
|
||||
val tokenExpireAt: Long,
|
||||
val wsUrl: String,
|
||||
val heartbeatSec: Int,
|
||||
val appId: String = "",
|
||||
val context: String = "",
|
||||
val realtimeWsUrl: String = ""
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class StopSessionRequest(
|
||||
val reason: String = "user_stop"
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class StopSessionData(
|
||||
val sessionStatus: String
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class HeartbeatRequest(
|
||||
val networkType: String? = "wifi",
|
||||
val bleRssi: Int? = null
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class HeartbeatData(
|
||||
val sessionStatus: String,
|
||||
val heartbeatAt: Long
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class ClientEventRequest(
|
||||
val sessionId: String? = null,
|
||||
val deviceId: String,
|
||||
val eventType: String,
|
||||
val eventLevel: String = "INFO",
|
||||
val payload: Map<String, String> = emptyMap(),
|
||||
val ts: Long? = null
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class ClientEventBatchRequest(
|
||||
val events: List<ClientEventRequest> = emptyList()
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class EventSavedData(
|
||||
val saved: Boolean
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class EventsBatchSavedData(
|
||||
val saved: Int = 0
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class SessionMessageRequest(
|
||||
val message: String,
|
||||
val messageType: String = "text",
|
||||
val messageId: String? = null,
|
||||
val extra: Map<String, String> = emptyMap()
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class ProviderActionData(
|
||||
val status: String = "UNKNOWN",
|
||||
val detail: String = "",
|
||||
val asrText: String = "",
|
||||
val ttsText: String = "",
|
||||
val audioBase64: String = "",
|
||||
val audioUrl: String = ""
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class SwitchRoleRequest(
|
||||
val sceneId: String,
|
||||
val roleId: String,
|
||||
val extra: Map<String, String> = emptyMap()
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class SessionInterruptRequest(
|
||||
val interrupt: Boolean = true,
|
||||
val extra: Map<String, String> = emptyMap()
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class DeviceStatusData(
|
||||
val bindStatus: String,
|
||||
val licenseStatus: String,
|
||||
val activeSessionId: String? = null,
|
||||
val activeSessionStatus: String? = null
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class AdminStats(
|
||||
val totalDevices: Int = 0,
|
||||
val totalSessions: Int = 0,
|
||||
val runningSessions: Int = 0,
|
||||
val totalLicenses: Int = 0,
|
||||
val usedLicenseQuota: Int = 0
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class BaiduInfo(
|
||||
val mode: String = "-",
|
||||
val generateConfigured: Boolean = false,
|
||||
val stopConfigured: Boolean = false,
|
||||
val activationQueryConfigured: Boolean = false
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class AdminOverviewData(
|
||||
val stats: AdminStats = AdminStats(),
|
||||
val baidu: BaiduInfo = BaiduInfo()
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class ActivationQueryData(
|
||||
val deviceId: String = "",
|
||||
val appId: String = "",
|
||||
val status: String = "UNKNOWN",
|
||||
val detail: String = "",
|
||||
val licenseKeyMasked: String = ""
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class ReloadLicensesData(
|
||||
val inserted: Int = 0
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class AppUpdateLatestData(
|
||||
val platform: String = "android",
|
||||
val channel: String = "stable",
|
||||
val hasUpdate: Boolean = false,
|
||||
val latestVersionCode: Int = 0,
|
||||
val latestVersionName: String = "",
|
||||
val minSupportedCode: Int = 0,
|
||||
val downloadUrl: String = "",
|
||||
val apkSha256: String = "",
|
||||
val releaseNotes: String = "",
|
||||
val forceUpdate: Boolean = false,
|
||||
val publishedAt: Long = 0L
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class DouyinManualPageCaptureRequest(
|
||||
val url: String = "",
|
||||
val title: String = "",
|
||||
val payload: JsonObject = JsonObject(emptyMap())
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class DouyinAccountSyncRequest(
|
||||
@SerialName("profile_url")
|
||||
val profileUrl: String = "",
|
||||
@SerialName("session_cookie")
|
||||
val sessionCookie: String = "",
|
||||
@SerialName("creator_center_urls")
|
||||
val creatorCenterUrls: List<String> = emptyList(),
|
||||
@SerialName("manual_profile_payload")
|
||||
val manualProfilePayload: JsonObject? = null,
|
||||
@SerialName("manual_creator_pages")
|
||||
val manualCreatorPages: List<DouyinManualPageCaptureRequest> = emptyList(),
|
||||
@SerialName("manual_work_payloads")
|
||||
val manualWorkPayloads: List<JsonObject> = emptyList(),
|
||||
@SerialName("discovery_note")
|
||||
val discoveryNote: String = ""
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class DouyinProfileStats(
|
||||
val followers: Double = 0.0,
|
||||
val following: Double = 0.0,
|
||||
val likes: Double = 0.0,
|
||||
val videos: Double = 0.0
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class DouyinVideoStats(
|
||||
val play: Double = 0.0,
|
||||
val like: Double = 0.0,
|
||||
val comment: Double = 0.0,
|
||||
val share: Double = 0.0,
|
||||
val collect: Double = 0.0
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class DouyinVideoSummaryItem(
|
||||
@SerialName("aweme_id")
|
||||
val awemeId: String = "",
|
||||
val title: String = "",
|
||||
val description: String = "",
|
||||
val tags: List<String> = emptyList(),
|
||||
@SerialName("published_at")
|
||||
val publishedAt: String? = null,
|
||||
val stats: DouyinVideoStats = DouyinVideoStats()
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class DouyinVideoSummary(
|
||||
val count: Int = 0,
|
||||
@SerialName("top_tags")
|
||||
val topTags: List<String> = emptyList(),
|
||||
@SerialName("avg_play")
|
||||
val avgPlay: Double = 0.0,
|
||||
@SerialName("avg_like")
|
||||
val avgLike: Double = 0.0,
|
||||
@SerialName("avg_comment")
|
||||
val avgComment: Double = 0.0,
|
||||
@SerialName("avg_share")
|
||||
val avgShare: Double = 0.0,
|
||||
val videos: List<DouyinVideoSummaryItem> = emptyList()
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class DouyinAccountSummary(
|
||||
val id: String = "",
|
||||
val nickname: String = "",
|
||||
val signature: String = "",
|
||||
@SerialName("profile_url")
|
||||
val profileUrl: String = "",
|
||||
@SerialName("avatar_url")
|
||||
val avatarUrl: String = "",
|
||||
@SerialName("sec_uid")
|
||||
val secUid: String = "",
|
||||
@SerialName("douyin_id")
|
||||
val douyinId: String = "",
|
||||
@SerialName("profile_stats")
|
||||
val profileStats: DouyinProfileStats = DouyinProfileStats(),
|
||||
val tags: List<String> = emptyList(),
|
||||
val keywords: List<String> = emptyList(),
|
||||
@SerialName("sync_status")
|
||||
val syncStatus: String = "",
|
||||
@SerialName("video_summary")
|
||||
val videoSummary: DouyinVideoSummary = DouyinVideoSummary()
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class DouyinSnapshotSummary(
|
||||
val id: String = "",
|
||||
@SerialName("snapshot_type")
|
||||
val snapshotType: String = "",
|
||||
@SerialName("source_url")
|
||||
val sourceUrl: String = "",
|
||||
@SerialName("field_count")
|
||||
val fieldCount: Int = 0,
|
||||
@SerialName("collected_at")
|
||||
val collectedAt: String = "",
|
||||
val summary: JsonObject = JsonObject(emptyMap())
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class DouyinModelProfileSummary(
|
||||
val id: String = "",
|
||||
val name: String = "",
|
||||
@SerialName("model_name")
|
||||
val modelName: String = "",
|
||||
@SerialName("base_url")
|
||||
val baseUrl: String = "",
|
||||
@SerialName("is_default")
|
||||
val isDefault: Boolean = false
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class DouyinAnalysisSuggestion(
|
||||
val id: String = "",
|
||||
@SerialName("model_profile_id")
|
||||
val modelProfileId: String = "",
|
||||
@SerialName("model_label")
|
||||
val modelLabel: String = "",
|
||||
val status: String = "",
|
||||
@SerialName("suggestion_text")
|
||||
val suggestionText: String = "",
|
||||
@SerialName("parsed_json")
|
||||
val parsedJson: JsonElement = JsonObject(emptyMap())
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class DouyinAnalysisReport(
|
||||
val id: String = "",
|
||||
@SerialName("focus_text")
|
||||
val focusText: String = "",
|
||||
@SerialName("model_profile_ids")
|
||||
val modelProfileIds: List<String> = emptyList(),
|
||||
@SerialName("linked_account_ids")
|
||||
val linkedAccountIds: List<String> = emptyList(),
|
||||
@SerialName("created_at")
|
||||
val createdAt: String = "",
|
||||
val suggestions: List<DouyinAnalysisSuggestion> = emptyList()
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class DouyinSimilaritySearchPreview(
|
||||
val id: String = "",
|
||||
val keywords: List<String> = emptyList(),
|
||||
@SerialName("created_at")
|
||||
val createdAt: String = ""
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class DouyinLinkedAccount(
|
||||
@SerialName("relation_id")
|
||||
val relationId: String = "",
|
||||
@SerialName("relation_type")
|
||||
val relationType: String = "",
|
||||
val note: String = "",
|
||||
@SerialName("search_id")
|
||||
val searchId: String = "",
|
||||
@SerialName("created_at")
|
||||
val createdAt: String = "",
|
||||
@SerialName("target_account_id")
|
||||
val targetAccountId: String? = null,
|
||||
@SerialName("target_profile_url")
|
||||
val targetProfileUrl: String = "",
|
||||
@SerialName("target_nickname")
|
||||
val targetNickname: String = "",
|
||||
@SerialName("target_signature")
|
||||
val targetSignature: String = "",
|
||||
@SerialName("target_profile_stats")
|
||||
val targetProfileStats: DouyinProfileStats = DouyinProfileStats(),
|
||||
@SerialName("target_tags")
|
||||
val targetTags: List<String> = emptyList()
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class DouyinAccountWorkspace(
|
||||
val account: DouyinAccountSummary = DouyinAccountSummary(),
|
||||
@SerialName("latest_public_snapshot")
|
||||
val latestPublicSnapshot: DouyinSnapshotSummary? = null,
|
||||
@SerialName("latest_creator_snapshot")
|
||||
val latestCreatorSnapshot: DouyinSnapshotSummary? = null,
|
||||
@SerialName("linked_accounts")
|
||||
val linkedAccounts: List<DouyinLinkedAccount> = emptyList(),
|
||||
@SerialName("recent_reports")
|
||||
val recentReports: List<DouyinAnalysisReport> = emptyList(),
|
||||
@SerialName("recent_similarity_searches")
|
||||
val recentSimilaritySearches: List<DouyinSimilaritySearchPreview> = emptyList(),
|
||||
@SerialName("available_model_profiles")
|
||||
val availableModelProfiles: List<DouyinModelProfileSummary> = emptyList(),
|
||||
@SerialName("sync_errors")
|
||||
val syncErrors: List<String> = emptyList()
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class DouyinAccountAnalysisRequest(
|
||||
@SerialName("model_profile_ids")
|
||||
val modelProfileIds: List<String> = emptyList(),
|
||||
@SerialName("linked_account_ids")
|
||||
val linkedAccountIds: List<String> = emptyList(),
|
||||
@SerialName("include_linked_accounts")
|
||||
val includeLinkedAccounts: Boolean = true,
|
||||
@SerialName("include_recent_similar_candidates")
|
||||
val includeRecentSimilarCandidates: Boolean = true,
|
||||
@SerialName("max_videos")
|
||||
val maxVideos: Int = 12,
|
||||
@SerialName("extra_focus")
|
||||
val extraFocus: String = "",
|
||||
val temperature: Double = 0.35
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class DouyinAnalysisResult(
|
||||
@SerialName("report_id")
|
||||
val reportId: String = "",
|
||||
@SerialName("created_at")
|
||||
val createdAt: String = "",
|
||||
val context: JsonElement = JsonObject(emptyMap()),
|
||||
val suggestions: List<DouyinAnalysisSuggestion> = emptyList()
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class DouyinSimilarSearchRequest(
|
||||
@SerialName("source_account_id")
|
||||
val sourceAccountId: String? = null,
|
||||
@SerialName("profile_url")
|
||||
val profileUrl: String? = null,
|
||||
@SerialName("candidate_urls")
|
||||
val candidateUrls: List<String> = emptyList(),
|
||||
@SerialName("seed_linked_accounts")
|
||||
val seedLinkedAccounts: Boolean = true,
|
||||
@SerialName("search_public_pages")
|
||||
val searchPublicPages: Boolean = true,
|
||||
@SerialName("model_profile_id")
|
||||
val modelProfileId: String? = null,
|
||||
@SerialName("max_candidates")
|
||||
val maxCandidates: Int = 10,
|
||||
@SerialName("extra_requirements")
|
||||
val extraRequirements: String = ""
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class DouyinSimilarCandidate(
|
||||
val id: String = "",
|
||||
@SerialName("candidate_account_id")
|
||||
val candidateAccountId: String? = null,
|
||||
@SerialName("candidate_profile_url")
|
||||
val candidateProfileUrl: String = "",
|
||||
@SerialName("candidate_nickname")
|
||||
val candidateNickname: String = "",
|
||||
@SerialName("heuristic_score")
|
||||
val heuristicScore: Double = 0.0,
|
||||
@SerialName("agent_score")
|
||||
val agentScore: Double = 0.0,
|
||||
@SerialName("rationale_text")
|
||||
val rationaleText: String = "",
|
||||
val dimensions: JsonElement = JsonObject(emptyMap()),
|
||||
@SerialName("rank_index")
|
||||
val rankIndex: Int = 0
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class DouyinSimilaritySearchResult(
|
||||
@SerialName("search_id")
|
||||
val searchId: String = "",
|
||||
@SerialName("source_account")
|
||||
val sourceAccount: DouyinAccountSummary = DouyinAccountSummary(),
|
||||
@SerialName("model_profile")
|
||||
val modelProfile: JsonObject = JsonObject(emptyMap()),
|
||||
@SerialName("raw_model_output")
|
||||
val rawModelOutput: String = "",
|
||||
val candidates: List<DouyinSimilarCandidate> = emptyList()
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class DouyinSimilaritySearchDetail(
|
||||
val id: String = "",
|
||||
@SerialName("source_account_id")
|
||||
val sourceAccountId: String? = null,
|
||||
@SerialName("source_profile_url")
|
||||
val sourceProfileUrl: String = "",
|
||||
val keywords: List<String> = emptyList(),
|
||||
val context: JsonElement = JsonObject(emptyMap()),
|
||||
@SerialName("created_at")
|
||||
val createdAt: String = "",
|
||||
val candidates: List<DouyinSimilarCandidate> = emptyList()
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class DouyinBenchmarkLinkRequest(
|
||||
@SerialName("target_account_ids")
|
||||
val targetAccountIds: List<String> = emptyList(),
|
||||
@SerialName("target_profile_urls")
|
||||
val targetProfileUrls: List<String> = emptyList(),
|
||||
@SerialName("relation_type")
|
||||
val relationType: String = "benchmark",
|
||||
val note: String = "",
|
||||
@SerialName("search_id")
|
||||
val searchId: String = ""
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class DouyinBenchmarkLinkResult(
|
||||
val saved: Int = 0,
|
||||
@SerialName("relation_ids")
|
||||
val relationIds: List<String> = emptyList(),
|
||||
val links: List<DouyinLinkedAccount> = emptyList()
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class DouyinSnapshotField(
|
||||
@SerialName("field_path")
|
||||
val fieldPath: String = "",
|
||||
@SerialName("field_type")
|
||||
val fieldType: String = "",
|
||||
@SerialName("field_value_text")
|
||||
val fieldValueText: String = ""
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class DouyinSnapshotDetail(
|
||||
val id: String = "",
|
||||
@SerialName("snapshot_type")
|
||||
val snapshotType: String = "",
|
||||
@SerialName("source_url")
|
||||
val sourceUrl: String = "",
|
||||
@SerialName("field_count")
|
||||
val fieldCount: Int = 0,
|
||||
@SerialName("collected_at")
|
||||
val collectedAt: String = "",
|
||||
val summary: JsonObject = JsonObject(emptyMap()),
|
||||
@SerialName("raw_payload")
|
||||
val rawPayload: JsonElement = JsonObject(emptyMap()),
|
||||
val fields: List<DouyinSnapshotField> = emptyList()
|
||||
)
|
||||
@@ -157,6 +157,7 @@
|
||||
- 进一步在旧改版隔离实例 `http://127.0.0.1:5682` 上重放了 fresh 图片请求,返回同样的 `403 access denied for invalid user`
|
||||
- 结论因此进一步收敛:当前 blocker 不是 upstream 回归,而是外部图片/视频凭证已失效
|
||||
- 已在 `huobao-drama-upstream` 增加按服务类型的运行时覆盖能力,可用 `HUOBAO_TEXT_* / HUOBAO_IMAGE_* / HUOBAO_VIDEO_*` 环境变量接管数据库中的 AI 配置
|
||||
- 已在 `huobao-drama-upstream` 固化 `scripts/run_storyforge_smoke.sh`,可自动复制旧库配置与数据、起隔离实例并校验 `/health`
|
||||
|
||||
结论更新:`huobao-drama-upstream` 的代码级兼容迁移已经完成,当前剩余 blocker 是外部图片/视频凭证失效,导致无法用“旧配置副本”继续 fresh 生成;但新的运行时 env 覆盖路径已经就位,后续补新 key 不需要再手改 SQLite。
|
||||
|
||||
@@ -175,10 +176,11 @@
|
||||
- `collector-service` 的 live 运行态已回归到 `StoryForge-gitea` 自身源码构建,不再依赖 `/Users/kris/code/Fastgpt/collector-service/app` 的临时 bind mount
|
||||
- `collector-service` 现已在 live `8081` 提供 `/v2/douyin/*` 接口,并保留原有 `real-cut / ai-video / content-source-sync` 路由
|
||||
- Android Explore 页已补上“账号同步”入口,可直接创建内容源账号同步任务,并支持平台、主页链接、账号标识、最大抓取条数、跳过已存在、自动触发分析等参数
|
||||
- Android 工作区缺失的 `com.aiglasses.app.data` 数据层已从同源代码补回,当前 `./gradlew :app:compileDebugKotlin` 与 `:app:assembleDebug` 均已通过,并产出 `app-debug.apk`
|
||||
|
||||
## 当前主要风险
|
||||
|
||||
1. 小红书账号级内容源还未做真实平台验证
|
||||
2. `douyin` public 直抓仍受反爬限制,生产落地还需要补 cookie 或人工页面采集协作链
|
||||
3. `huobao-drama-upstream` 已完成代码迁移并可编译,但 fresh smoke 受外部图片/视频凭证 `403 invalid user` 阻塞
|
||||
4. Android 端完整编译目前仍被既有 `MainViewModel` 缺失依赖阻塞,本轮新增账号同步入口未触发新的 Kotlin 编译错误,但无法在现有工作区拿到全量 APK 构建通过结论
|
||||
4. Android 端目前已能完成 Debug APK 构建,但仍缺少真机安装和功能回归验证
|
||||
|
||||
@@ -212,6 +212,7 @@ docker compose up -d --build
|
||||
- fresh 图片/视频生成请求已能进入 provider 调用,但当前复制出的图片/视频凭证返回 `403 invalid user`
|
||||
- 同样的 fresh 图片请求已在旧改版隔离实例 `http://127.0.0.1:5682` 上重放,结论一致,所以当前不是 upstream 回归问题
|
||||
- `huobao-drama-upstream` 现在支持 `HUOBAO_TEXT_* / HUOBAO_IMAGE_* / HUOBAO_VIDEO_*` 运行时覆盖数据库里的 AI 配置
|
||||
- `huobao-drama-upstream` 已新增 `/Users/kris/code/huobao-drama-upstream/scripts/run_storyforge_smoke.sh`,可自动复制旧目录配置和数据,在默认 `5681` 端口起隔离实例并校验 `/health`
|
||||
- 如果你要重新验证 upstream fresh 生成,优先给 huobao 进程补这些环境变量,再复跑即可
|
||||
|
||||
推荐覆盖字段:
|
||||
@@ -241,3 +242,23 @@ docker compose up -d --build
|
||||
- 脚本会在清理前后校验:
|
||||
- `http://127.0.0.1:8081/healthz`
|
||||
- `http://127.0.0.1:5670/healthz`
|
||||
|
||||
## 11. Android 本地构建
|
||||
|
||||
如果你要在本机重新打 Android 包:
|
||||
|
||||
```bash
|
||||
cd /Users/kris/code/StoryForge-gitea/android-app
|
||||
./gradlew :app:assembleDebug
|
||||
```
|
||||
|
||||
当前已验证结果:
|
||||
|
||||
- `:app:compileDebugKotlin` 通过
|
||||
- `:app:assembleDebug` 通过
|
||||
- APK 输出路径:
|
||||
- `/Users/kris/code/StoryForge-gitea/android-app/app/build/outputs/apk/debug/app-debug.apk`
|
||||
|
||||
补充说明:
|
||||
|
||||
- 工作区根目录的 `.gitignore` 里保留了通用 `data/` 忽略规则,但已对 Android 源码目录 `android-app/app/src/main/java/com/aiglasses/app/data/` 做了白名单放行,避免误伤客户端代码
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
- 文本 / 视频链接 / 上传视频 三类分析任务创建
|
||||
- 内容源账号同步任务创建与子任务派发
|
||||
- Android Explore 页已补上内容源账号同步入口
|
||||
- Android `com.aiglasses.app.data` 数据层已补回,`compileDebugKotlin` 与 `assembleDebug` 已通过
|
||||
- `n8n` 工作流导入、激活与触发接口
|
||||
- 本地下载器调用
|
||||
- 本地 `ffmpeg` / `whisper` 风格入口封装
|
||||
@@ -42,6 +43,8 @@
|
||||
- `douyin` 相似账号搜索:`dysearch_c247b75db0df49429a1d127407fe4486`
|
||||
- `douyin` 对标关系:`dyrel_c8df266341e74237b99c880eb4b572d8`
|
||||
- `huobao-upstream` 隔离 smoke 剧本:`drama_id=11` (`http://127.0.0.1:5681`)
|
||||
- `huobao-upstream` 隔离 smoke 启动脚本:`/Users/kris/code/huobao-drama-upstream/scripts/run_storyforge_smoke.sh`
|
||||
- Android Debug APK:`/Users/kris/code/StoryForge-gitea/android-app/app/build/outputs/apk/debug/app-debug.apk`
|
||||
|
||||
## 尚未完全跑通
|
||||
|
||||
@@ -49,7 +52,7 @@
|
||||
- `douyin` public 主页直抓会命中 `public_profile_anti_bot_challenge`;当前已验证手工 payload 导入、分析、相似账号搜索和对标关系可作为可用兜底路径
|
||||
- `huobao-upstream` 已能全量编译;并且旧改版隔离实例也已重放确认,当前 fresh 生成被外部图片/视频凭证统一返回 `403 invalid user`
|
||||
- `huobao-upstream` 已新增 `HUOBAO_TEXT_* / HUOBAO_IMAGE_* / HUOBAO_VIDEO_*` 运行时覆盖能力,后续补新 key 可直接接管数据库配置
|
||||
- Android 整体 `compileDebugKotlin` 目前仍被工作区既有 `MainViewModel` 缺失依赖阻塞,暂时无法给出 APK 级构建通过结论
|
||||
- Android Debug 包已可本地构建,但尚未完成真机安装验证
|
||||
|
||||
## 下一步优先级
|
||||
|
||||
|
||||
Reference in New Issue
Block a user