feat: restore android build path and update status docs

This commit is contained in:
kris
2026-03-20 14:17:33 +08:00
parent ac6a8a82df
commit 7070c3aa85
8 changed files with 1050 additions and 2 deletions

2
.gitignore vendored
View File

@@ -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

View File

@@ -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>()
}
}

View File

@@ -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>
}

View File

@@ -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
}
}

View 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()
)

View File

@@ -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 构建,但仍缺少真机安装和功能回归验证

View File

@@ -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/` 做了白名单放行,避免误伤客户端代码

View File

@@ -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 包已可本地构建,但尚未完成真机安装验证
## 下一步优先级