第一个版本的客户端

This commit is contained in:
2024-09-17 09:24:50 +08:00
parent f6e9283700
commit 01f3d63378
62 changed files with 1961 additions and 0 deletions

View File

@@ -0,0 +1,288 @@
package com.example.flomo_ai
import android.content.Intent
import android.os.Bundle
import android.view.Gravity
import android.widget.Button
import android.widget.EditText
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
import com.google.android.material.tabs.TabLayout
import com.google.gson.Gson
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import okhttp3.Interceptor
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.Response
import okhttp3.RequestBody.Companion.toRequestBody
import com.google.gson.JsonObject
import okhttp3.MediaType.Companion.toMediaType
import com.google.gson.reflect.TypeToken
import android.util.Log
import java.io.IOException
import java.net.UnknownHostException
import org.json.JSONObject
import com.squareup.moshi.Moshi
import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
// 定义请求体数据类
data class ChatRequest(
val model: String,
val messages: List<Message>
)
data class Message(
val role: String,
val content: String
)
// 从返回的 JSON 响应中提取标签列表
fun extractLabels(responseBody: String): List<String>? {
try {
// 假设 responseBody 中包含一个完整的 JSON 对象,我们需要先提取出其中的 content 部分
val fullJsonObject = Gson().fromJson(responseBody, JsonObject::class.java)
val choicesArray = fullJsonObject.getAsJsonArray("choices")
if (choicesArray.size() > 0) {
val firstChoice = choicesArray.get(0).asJsonObject
val messageObject = firstChoice.get("message").asJsonObject
val content = messageObject.get("content").asString
// 从 content 中提取出 labels
val startIndex = content.indexOf("\"labels\": [") + "\"labels\": [".length
val endIndex = content.indexOf("]", startIndex)
val labelsStr = content.substring(startIndex, endIndex)
// 处理引号
//val processedLabelsStr = labelsStr.replace("\"", "")
val labels = labelsStr.split("\", \"")
val processedLabels = mutableListOf<String>()
for (label in labels) {
// 假设 label 是原始字符串
var processedLabel = label
// 去掉单引号和双引号
if (label.contains("'")) {
processedLabel = processedLabel.replace("'", "")
}
if (label.contains("\"")) {
processedLabel = processedLabel.replace("\"", "")
}
// 去掉所有空格
processedLabel = processedLabel.replace(" ", "")
processedLabels.add(processedLabel)
}
return processedLabels
}
} catch (e: Exception) {
e.printStackTrace()
Log.e("ExtractLabels", "Error during extraction: ${e.message}")
}
return null
}
class MainActivity : AppCompatActivity() {
private lateinit var inputEditText: EditText
private lateinit var configButton: Button
private lateinit var submitToAIButton: Button
private lateinit var tabLayout: TabLayout
private lateinit var submitToServerButton: Button
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
inputEditText = findViewById(R.id.inputEditText)
inputEditText.gravity = Gravity.START or Gravity.TOP
configButton = findViewById(R.id.configButton)
submitToAIButton = findViewById(R.id.submitToAIButton)
submitToAIButton.setOnClickListener {
// 创建 OkHttpClient点击智谱AI分析返回标签
val client = OkHttpClient.Builder()
.addInterceptor(object : Interceptor {
override fun intercept(chain: Interceptor.Chain): okhttp3.Response {
val originalRequest = chain.request()
val newRequest = originalRequest.newBuilder().build()
return chain.proceed(newRequest)
}
})
.build()
// 假设这是一个 EditText 元素
val inputEditText = findViewById<EditText>(R.id.inputEditText)
// 获取 EditText 中的文本内容创建request的body
val textFromEditText = inputEditText.text.toString()
val combinedText =
"$textFromEditText。请为以上文章分析并给出 4 个最合理的标签,没有其他内容。以 JSON 格式输出,格式为 labels: [标签 1, 标签 2, 标签 3, 标签 4]"
val moshi = Moshi.Builder().add(KotlinJsonAdapterFactory()).build()
val jsonAdapter = moshi.adapter(ChatRequest::class.java)
val requestBody = ChatRequest(
model = "glm-4-flash",
messages = listOf(Message(role = "user", content = combinedText))
)
val requestBodyJson = jsonAdapter.toJson(requestBody)
val mediaType = "application/json; charset=utf-8".toMediaType()
val body = requestBodyJson.toRequestBody(mediaType)
// 从配置中读取 api_key
val sharedPrefs = getSharedPreferences("APIConfigs", MODE_PRIVATE)
val allConfigsJson = sharedPrefs.getString("configs", null)
var api_key = ""
var api_url = ""
if (allConfigsJson != null) {
val type = object : TypeToken<List<APIConfig>>() {}.type
val allConfigs = Gson().fromJson<List<APIConfig>>(allConfigsJson, type)
val zhipuConfig = allConfigs.find { it.name == "zhipu" }
if (zhipuConfig != null) {
api_key = zhipuConfig.key
api_url = zhipuConfig.url
}
}
// 创建请求
val request = Request.Builder()
.url("$api_url")
.post(body)
.header("Authorization", "Bearer $api_key")
.header("Content-Type", "application/json")
.build()
// 使用协程在后台线程中发送请求
CoroutineScope(Dispatchers.Main).launch {
try {
// 模拟可能出现异常的网络操作,这里需要替换为你的实际网络请求相关代码
// 比如使用 OkHttp 或者其他网络库进行请求
val response = withContext(Dispatchers.IO) {
client.newCall(request).execute()
}
if (response.isSuccessful) {
val responseBody = response.body?.string() // 将响应体转换为字符串
responseBody?.let {
// 处理响应 JSON 数据
print("return message is $responseBody")
val labels = extractLabels(responseBody)
labels?.let {
if (labels != null && labels.size == 4) {
for (i in 0 until 4) {
val tab = tabLayout.getTabAt(i)
if (tab != null) {
tab.text = labels[i]
tab.view.setOnClickListener {
val currentText =
findViewById<EditText>(R.id.inputEditText).text.toString()
val buttonText = tab.text.toString()
findViewById<EditText>(R.id.inputEditText).setText("$currentText\n#$buttonText")
}
}
}
findViewById<TextView>(R.id.statusTextView).text = "标签已经获取并更新"
}
} ?: run {
findViewById<TextView>(R.id.statusTextView).text = "没有更新"
}
}
} else {
findViewById<TextView>(R.id.statusTextView).text = "没有响应,没有更新"
}
} catch (e: UnknownHostException) {
findViewById<TextView>(R.id.statusTextView).text ="UnknownHostException: ${e.message}"
} catch (e: IOException) {
findViewById<TextView>(R.id.statusTextView).text = "IOException: ${e.message}"
}
}
}
tabLayout = findViewById(R.id.tabLayout)
submitToServerButton = findViewById(R.id.submitToServerButton)
configButton.setOnClickListener {
val intent = Intent(this, SecondActivity::class.java)
startActivity(intent)
}
submitToServerButton = findViewById(R.id.submitToServerButton)
inputEditText = findViewById(R.id.inputEditText)
submitToServerButton.setOnClickListener {
val textFromEditText = inputEditText.text.toString()
submitToServer(textFromEditText)
}
// Setup TabLayout using a loop
val tabLayout = findViewById<TabLayout>(R.id.tabLayout)
// 维持原来的创建标签按钮的代码
(1..4).forEach { tabIndex ->
val tabButton = tabLayout.newTab().apply {
text = "标签示例$tabIndex"
tabLayout.addTab(this)
}
}
}
private fun submitToServer(content: String) {
val statusTextView = findViewById<TextView>(R.id.statusTextView)
CoroutineScope(Dispatchers.Main).launch {
statusTextView.text = "提交到flomo服务器..."
val result = withContext(Dispatchers.IO) {
postDataToServer(content)
}
when (result) {
is Result.Success -> {
findViewById<EditText>(R.id.inputEditText).setText("")
statusTextView.text = "提交成功!"
}
is Result.Error -> {
statusTextView.text = "提交失误: ${result.exception.message}"
}
}
}
}
// 提交到笔记服务器flomo服务器
private suspend fun postDataToServer(content: String): Result {
return try {
val client = OkHttpClient()
val mediaType = "application/json".toMediaType()
val json = JSONObject().apply {
put("content", content)
}.toString()
val body = json.toRequestBody(mediaType)
val request = Request.Builder()
.url("https://flomoapp.com/iwh/MTY5NTQy/b671d4930ecd1eae63e50cc0cb8ca4ae/")
.post(body)
.build()
val response = client.newCall(request).execute()
if (response.isSuccessful) {
Result.Success(response)
} else {
Result.Error(Exception("服务器返回错误: ${response.code}"))
}
} catch (e: Exception) {
Result.Error(e)
}
}
sealed class Result {
data class Success(val response: Response) : Result()
data class Error(val exception: Exception) : Result()
}
}

View File

@@ -0,0 +1,189 @@
package com.example.flomo_ai
import android.os.Bundle
import android.widget.Button
import android.widget.EditText
import android.widget.LinearLayout
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
import com.google.gson.Gson
import com.google.gson.reflect.TypeToken
import java.util.UUID
class SecondActivity : AppCompatActivity() {
private lateinit var etApiName: EditText
private lateinit var etApiUrl: EditText
private lateinit var etApiKey: EditText
private lateinit var etApiSecretKey: EditText
private lateinit var btnSave: Button
private lateinit var llConfigList: LinearLayout
private var configs = mutableListOf<APIConfig>()
private var editingId: Long? = null
override fun onCreate(savedInstanceState: Bundle?) {
setTheme(androidx.appcompat.R.style.Theme_AppCompat)
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_second)
initViews()
loadConfigs()
displayConfigs()
val btnGoBack: Button = findViewById(R.id.btnGoBack)
btnGoBack.setOnClickListener {
finish()
}
btnSave.setOnClickListener {
if (editingId != null) {
updateConfig()
} else {
addConfig()
}
}
}
private fun initViews() {
etApiName = findViewById(R.id.etApiName)
etApiUrl = findViewById(R.id.etApiUrl)
etApiKey = findViewById(R.id.etApiKey)
etApiSecretKey = findViewById(R.id.etApiSecretKey)
btnSave = findViewById(R.id.btnSave)
llConfigList = findViewById(R.id.llConfigList)
}
private fun loadConfigs() {
val sharedPrefs = getSharedPreferences("APIConfigs", MODE_PRIVATE)
val json = sharedPrefs.getString("configs", null)
if (json != null) {
val type = object : TypeToken<List<APIConfig>>() {}.type
configs = Gson().fromJson(json, type)
}
}
private fun saveConfigs() {
val sharedPrefs = getSharedPreferences("APIConfigs", MODE_PRIVATE)
val json = Gson().toJson(configs)
sharedPrefs.edit().putString("configs", json).apply()
}
private fun addConfig() {
val name = etApiName.text.toString()
val url = etApiUrl.text.toString()
val key = etApiKey.text.toString()
val secretKey = etApiSecretKey.text.toString()
// 生成唯一的 id
val id = System.currentTimeMillis()
// 创建新的配置项
val newConfig = APIConfig(id, name, url, key, secretKey)
// 添加配置项
configs.add(newConfig)
// 保存配置
saveConfigs()
// 显示配置
displayConfigs()
// 清空输入框
clearInputs()
}
private fun updateConfig() {
val name = etApiName.text.toString()
val url = etApiUrl.text.toString()
val key = etApiKey.text.toString()
val secretKey = etApiSecretKey.text.toString()
// 获取编辑的配置项 id
val id = editingId ?: return
// 更新配置项
val updatedConfig = APIConfig(id, name, url, key, secretKey)
val existingConfigIndex = configs.indexOfFirst { it.id == id }
if (existingConfigIndex != -1) {
configs[existingConfigIndex] = updatedConfig
}
// 保存配置
saveConfigs()
// 显示配置
displayConfigs()
// 清空输入框
clearInputs()
// 重置编辑状态
editingId = null
}
private fun displayConfigs() {
llConfigList.removeAllViews()
for (config in configs) {
// 为每个配置项加载对应的布局文件
val configView = layoutInflater.inflate(R.layout.item_api_config, null)
// 设置各项文本信息
// 获取并设置 Name 的 TextView 前景色和背景色
val tvName = configView.findViewById<TextView>(R.id.tvName)
tvName.setTextColor(resources.getColor(R.color.background_color))
tvName.text = "Name: ${config.name}"
// 获取并设置 URL 的 TextView 前景色和背景色
val tvUrl = configView.findViewById<TextView>(R.id.tvUrl)
tvUrl.setTextColor(resources.getColor(R.color.background_color))
tvUrl.text = "URL: ${config.url}"
// 获取并设置 Key 的 TextView 前景色和背景色
val tvKey = configView.findViewById<TextView>(R.id.tvKey).also {
it.setTextColor(resources.getColor(R.color.background_color))
}
tvKey.text = "Key: ${config.key.take(4)}..."
// 获取并设置 SecretKey 的 TextView 前景色和背景色
val tvSecretKey = configView.findViewById<TextView>(R.id.tvSecretKey)
tvSecretKey.setTextColor(resources.getColor(R.color.background_color))
tvSecretKey.text = "Secret Key: ${config.secretKey.take(4)}..."
// 设置编辑按钮点击事件
configView.findViewById<Button>(R.id.btnEdit).setOnClickListener {
editConfig(config)
}
// 设置删除按钮点击事件
configView.findViewById<Button>(R.id.btnDelete).setOnClickListener {
deleteConfig(config.id)
}
// 将包含配置信息的视图添加到父布局中
llConfigList.addView(configView)
}
}
private fun editConfig(config: APIConfig) {
etApiName.setText(config.name)
etApiUrl.setText(config.url)
etApiKey.setText(config.key)
etApiSecretKey.setText(config.secretKey)
// 设置编辑状态
editingId = config.id
btnSave.text = "更新配置"
}
private fun deleteConfig(id: Long) {
configs.removeAll { it.id == id }
saveConfigs()
displayConfigs()
}
private fun clearInputs() {
etApiName.text.clear()
etApiUrl.text.clear()
etApiKey.text.clear()
etApiSecretKey.text.clear()
btnSave.text = "保存配置"
}
}
data class APIConfig(
val id: Long,
val name: String,
val url: String,
val key: String,
val secretKey: String
)

View File

@@ -0,0 +1,49 @@
import java.util.*
import com.nimbusds.jose.JWSAlgorithm
import com.nimbusds.jose.JWSHeader
import com.nimbusds.jose.crypto.MACSigner
import com.nimbusds.jwt.JWTClaimsSet
import com.nimbusds.jwt.SignedJWT
class JWTGenerator {
companion object {
fun generateJWT(apiKeyId: String, secretStr: String): SignedJWT {
// 确保密钥长度至少为 32 字节256 位)
val secret = generateValidSecret(secretStr)
// Prepare JWT header
val header = JWSHeader.Builder(JWSAlgorithm.HS256)
.customParam("sign_type", "SIGN")
.build()
// Prepare JWT payload
val currentTimeMillis = System.currentTimeMillis()
val claimsSet = JWTClaimsSet.Builder()
.claim("api_key", apiKeyId)
.expirationTime(Date(currentTimeMillis + 60000))
.claim("timestamp", currentTimeMillis)
.build()
// Create HMAC signer
val signer = MACSigner(secret)
// Create signed JWT
val signedJWT = SignedJWT(header, claimsSet)
signedJWT.sign(signer)
return signedJWT
}
private fun generateValidSecret(secretStr: String): ByteArray {
val originalSecret = secretStr.toByteArray()
val desiredLength = 32
if (originalSecret.size >= desiredLength) {
return originalSecret.copyOfRange(0, desiredLength)
}
val paddedSecret = ByteArray(desiredLength)
for (i in originalSecret.indices) {
paddedSecret[i] = originalSecret[i]
}
return paddedSecret
}
}
}

View File

@@ -0,0 +1,11 @@
package com.example.flomo_ai.ui.theme
import androidx.compose.ui.graphics.Color
val Purple80 = Color(0xFFD0BCFF)
val PurpleGrey80 = Color(0xFFCCC2DC)
val Pink80 = Color(0xFFEFB8C8)
val Purple40 = Color(0xFF6650a4)
val PurpleGrey40 = Color(0xFF625b71)
val Pink40 = Color(0xFF7D5260)

View File

@@ -0,0 +1,58 @@
package com.example.flomo_ai.ui.theme
import android.app.Activity
import android.os.Build
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.dynamicDarkColorScheme
import androidx.compose.material3.dynamicLightColorScheme
import androidx.compose.material3.lightColorScheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.platform.LocalContext
private val DarkColorScheme = darkColorScheme(
primary = Purple80,
secondary = PurpleGrey80,
tertiary = Pink80
)
private val LightColorScheme = lightColorScheme(
primary = Purple40,
secondary = PurpleGrey40,
tertiary = Pink40
/* Other default colors to override
background = Color(0xFFFFFBFE),
surface = Color(0xFFFFFBFE),
onPrimary = Color.White,
onSecondary = Color.White,
onTertiary = Color.White,
onBackground = Color(0xFF1C1B1F),
onSurface = Color(0xFF1C1B1F),
*/
)
@Composable
fun FlomoaiTheme(
darkTheme: Boolean = isSystemInDarkTheme(),
// Dynamic color is available on Android 12+
dynamicColor: Boolean = true,
content: @Composable () -> Unit
) {
val colorScheme = when {
dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
val context = LocalContext.current
if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
}
darkTheme -> DarkColorScheme
else -> LightColorScheme
}
MaterialTheme(
colorScheme = colorScheme,
typography = Typography,
content = content
)
}

View File

@@ -0,0 +1,34 @@
package com.example.flomo_ai.ui.theme
import androidx.compose.material3.Typography
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.sp
// Set of Material typography styles to start with
val Typography = Typography(
bodyLarge = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Normal,
fontSize = 16.sp,
lineHeight = 24.sp,
letterSpacing = 0.5.sp
)
/* Other default text styles to override
titleLarge = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Normal,
fontSize = 22.sp,
lineHeight = 28.sp,
letterSpacing = 0.sp
),
labelSmall = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Medium,
fontSize = 11.sp,
lineHeight = 16.sp,
letterSpacing = 0.5.sp
)
*/
)