From 7070c3aa8536a037c52983f2f8e8ba702ed70adb Mon Sep 17 00:00:00 2001 From: kris Date: Fri, 20 Mar 2026 14:17:33 +0800 Subject: [PATCH] feat: restore android build path and update status docs --- .gitignore | 2 + .../java/com/aiglasses/app/data/ApiClient.kt | 50 ++ .../java/com/aiglasses/app/data/ApiService.kt | 154 +++++ .../aiglasses/app/data/BackendRepository.kt | 276 +++++++++ .../java/com/aiglasses/app/data/Models.kt | 540 ++++++++++++++++++ docs/AUDIT_2026-03-18.md | 4 +- docs/LAN_E2E_GUIDE_2026-03-18.md | 21 + docs/MVP_STATUS_2026-03-18.md | 5 +- 8 files changed, 1050 insertions(+), 2 deletions(-) create mode 100644 android-app/app/src/main/java/com/aiglasses/app/data/ApiClient.kt create mode 100644 android-app/app/src/main/java/com/aiglasses/app/data/ApiService.kt create mode 100644 android-app/app/src/main/java/com/aiglasses/app/data/BackendRepository.kt create mode 100644 android-app/app/src/main/java/com/aiglasses/app/data/Models.kt diff --git a/.gitignore b/.gitignore index 9c15fa1..c53c85f 100644 --- a/.gitignore +++ b/.gitignore @@ -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 diff --git a/android-app/app/src/main/java/com/aiglasses/app/data/ApiClient.kt b/android-app/app/src/main/java/com/aiglasses/app/data/ApiClient.kt new file mode 100644 index 0000000..46702cd --- /dev/null +++ b/android-app/app/src/main/java/com/aiglasses/app/data/ApiClient.kt @@ -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 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() + } +} diff --git a/android-app/app/src/main/java/com/aiglasses/app/data/ApiService.kt b/android-app/app/src/main/java/com/aiglasses/app/data/ApiService.kt new file mode 100644 index 0000000..ee50332 --- /dev/null +++ b/android-app/app/src/main/java/com/aiglasses/app/data/ApiService.kt @@ -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 + + @POST("/api/v1/devices/bind-confirm") + suspend fun bindConfirm( + @Body request: BindConfirmRequest + ): ApiEnvelope + + @POST("/api/v1/ai/sessions") + suspend fun createSession( + @Header("Idempotency-Key") idempotencyKey: String?, + @Body request: CreateSessionRequest + ): ApiEnvelope + + @POST("/api/v1/ai/sessions/{sessionId}/stop") + suspend fun stopSession( + @Path("sessionId") sessionId: String, + @Body request: StopSessionRequest + ): ApiEnvelope + + @POST("/api/v1/ai/sessions/{sessionId}/heartbeat") + suspend fun heartbeat( + @Path("sessionId") sessionId: String, + @Body request: HeartbeatRequest + ): ApiEnvelope + + @GET("/api/v1/devices/{deviceId}/status") + suspend fun getDeviceStatus( + @Path("deviceId") deviceId: String + ): ApiEnvelope + + @POST("/api/v1/events") + suspend fun postEvent( + @Body request: ClientEventRequest + ): ApiEnvelope + + @POST("/api/v1/events/batch") + suspend fun postEventsBatch( + @Body request: ClientEventBatchRequest + ): ApiEnvelope + + @POST("/api/v1/ai/sessions/{sessionId}/messages") + suspend fun sendMessage( + @Path("sessionId") sessionId: String, + @Body request: SessionMessageRequest + ): ApiEnvelope + + @POST("/api/v1/ai/sessions/{sessionId}/scene-role") + suspend fun switchRole( + @Path("sessionId") sessionId: String, + @Body request: SwitchRoleRequest + ): ApiEnvelope + + @POST("/api/v1/ai/sessions/{sessionId}/interrupt") + suspend fun interruptSession( + @Path("sessionId") sessionId: String, + @Body request: SessionInterruptRequest + ): ApiEnvelope + + @GET("/api/v1/baidu/activation/query") + suspend fun activationQuery( + @Query("deviceId") deviceId: String, + @Query("appId") appId: String? = null + ): ApiEnvelope + + @POST("/api/v1/licenses/reload") + suspend fun reloadLicenses(): ApiEnvelope + + @GET("/api/v1/admin/overview") + suspend fun adminOverview(): ApiEnvelope + + @GET("/api/v1/app/update/latest") + suspend fun appUpdateLatest( + @Query("platform") platform: String = "android", + @Query("channel") channel: String = "stable", + @Query("currentVersionCode") currentVersionCode: Int + ): ApiEnvelope + + @GET("/v2/douyin/accounts") + suspend fun listDouyinAccounts(): ApiEnvelope> + + @POST("/v2/douyin/accounts/sync") + suspend fun syncDouyinAccount( + @Body request: DouyinAccountSyncRequest + ): ApiEnvelope + + @GET("/v2/douyin/accounts/{accountId}") + suspend fun getDouyinAccount( + @Path("accountId") accountId: String + ): ApiEnvelope + + @GET("/v2/douyin/accounts/{accountId}/workspace") + suspend fun getDouyinWorkspace( + @Path("accountId") accountId: String + ): ApiEnvelope + + @GET("/v2/douyin/accounts/{accountId}/snapshots") + suspend fun listDouyinSnapshots( + @Path("accountId") accountId: String + ): ApiEnvelope> + + @GET("/v2/douyin/accounts/{accountId}/snapshots/{snapshotId}") + suspend fun getDouyinSnapshot( + @Path("accountId") accountId: String, + @Path("snapshotId") snapshotId: String + ): ApiEnvelope + + @GET("/v2/douyin/accounts/{accountId}/creator-fields") + suspend fun getDouyinCreatorFields( + @Path("accountId") accountId: String + ): ApiEnvelope + + @POST("/v2/douyin/accounts/{accountId}/analysis") + suspend fun analyzeDouyinAccount( + @Path("accountId") accountId: String, + @Body request: DouyinAccountAnalysisRequest + ): ApiEnvelope + + @GET("/v2/douyin/accounts/{accountId}/analysis-reports") + suspend fun listDouyinAnalysisReports( + @Path("accountId") accountId: String + ): ApiEnvelope> + + @POST("/v2/douyin/similar-searches") + suspend fun createDouyinSimilarSearch( + @Body request: DouyinSimilarSearchRequest + ): ApiEnvelope + + @GET("/v2/douyin/similar-searches/{searchId}") + suspend fun getDouyinSimilarSearch( + @Path("searchId") searchId: String + ): ApiEnvelope + + @GET("/v2/douyin/accounts/{accountId}/benchmark-links") + suspend fun listDouyinBenchmarkLinks( + @Path("accountId") accountId: String + ): ApiEnvelope> + + @POST("/v2/douyin/accounts/{accountId}/benchmark-links") + suspend fun createDouyinBenchmarkLinks( + @Path("accountId") accountId: String, + @Body request: DouyinBenchmarkLinkRequest + ): ApiEnvelope +} diff --git a/android-app/app/src/main/java/com/aiglasses/app/data/BackendRepository.kt b/android-app/app/src/main/java/com/aiglasses/app/data/BackendRepository.kt new file mode 100644 index 0000000..f1b77f1 --- /dev/null +++ b/android-app/app/src/main/java/com/aiglasses/app/data/BackendRepository.kt @@ -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 = 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): 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 = 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 { + 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 { + 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 { + 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 { + 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 + } +} diff --git a/android-app/app/src/main/java/com/aiglasses/app/data/Models.kt b/android-app/app/src/main/java/com/aiglasses/app/data/Models.kt new file mode 100644 index 0000000..74f6c2b --- /dev/null +++ b/android-app/app/src/main/java/com/aiglasses/app/data/Models.kt @@ -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( + 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 = emptyMap(), + val ts: Long? = null +) + +@Serializable +data class ClientEventBatchRequest( + val events: List = 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 = 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 = emptyMap() +) + +@Serializable +data class SessionInterruptRequest( + val interrupt: Boolean = true, + val extra: Map = 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 = emptyList(), + @SerialName("manual_profile_payload") + val manualProfilePayload: JsonObject? = null, + @SerialName("manual_creator_pages") + val manualCreatorPages: List = emptyList(), + @SerialName("manual_work_payloads") + val manualWorkPayloads: List = 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 = 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 = 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 = 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 = emptyList(), + val keywords: List = 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 = emptyList(), + @SerialName("linked_account_ids") + val linkedAccountIds: List = emptyList(), + @SerialName("created_at") + val createdAt: String = "", + val suggestions: List = emptyList() +) + +@Serializable +data class DouyinSimilaritySearchPreview( + val id: String = "", + val keywords: List = 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 = 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 = emptyList(), + @SerialName("recent_reports") + val recentReports: List = emptyList(), + @SerialName("recent_similarity_searches") + val recentSimilaritySearches: List = emptyList(), + @SerialName("available_model_profiles") + val availableModelProfiles: List = emptyList(), + @SerialName("sync_errors") + val syncErrors: List = emptyList() +) + +@Serializable +data class DouyinAccountAnalysisRequest( + @SerialName("model_profile_ids") + val modelProfileIds: List = emptyList(), + @SerialName("linked_account_ids") + val linkedAccountIds: List = 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 = 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 = 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 = 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 = emptyList(), + val context: JsonElement = JsonObject(emptyMap()), + @SerialName("created_at") + val createdAt: String = "", + val candidates: List = emptyList() +) + +@Serializable +data class DouyinBenchmarkLinkRequest( + @SerialName("target_account_ids") + val targetAccountIds: List = emptyList(), + @SerialName("target_profile_urls") + val targetProfileUrls: List = 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 = emptyList(), + val links: List = 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 = emptyList() +) diff --git a/docs/AUDIT_2026-03-18.md b/docs/AUDIT_2026-03-18.md index 0c3191d..8f46b3b 100644 --- a/docs/AUDIT_2026-03-18.md +++ b/docs/AUDIT_2026-03-18.md @@ -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 构建,但仍缺少真机安装和功能回归验证 diff --git a/docs/LAN_E2E_GUIDE_2026-03-18.md b/docs/LAN_E2E_GUIDE_2026-03-18.md index 7e983a3..7a7f4ca 100644 --- a/docs/LAN_E2E_GUIDE_2026-03-18.md +++ b/docs/LAN_E2E_GUIDE_2026-03-18.md @@ -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/` 做了白名单放行,避免误伤客户端代码 diff --git a/docs/MVP_STATUS_2026-03-18.md b/docs/MVP_STATUS_2026-03-18.md index 372e3fa..afd5585 100644 --- a/docs/MVP_STATUS_2026-03-18.md +++ b/docs/MVP_STATUS_2026-03-18.md @@ -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 包已可本地构建,但尚未完成真机安装验证 ## 下一步优先级