Skip to content

Commit 3bcb23d

Browse files
committed
1.8.1
1 parent a0b260e commit 3bcb23d

14 files changed

Lines changed: 892 additions & 117 deletions

File tree

app/build.gradle.kts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ android {
5454
minSdk = 26
5555
targetSdk = 34
5656
versionCode = 39
57-
versionName = "1.8.0+8"
57+
versionName = "1.8.1"
5858

5959
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
6060
vectorDrawables {
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
/* METADATA
2+
{
3+
name: "vflow_trigger"
4+
description: {
5+
zh: "触发vflow app的工作流。"
6+
en: "Trigger VFlow app workflows."
7+
}
8+
enabledByDefault: false
9+
tools: [
10+
{
11+
name: "trigger_vflow_workflow"
12+
description: {
13+
zh: "根据 workflow_id 触发 VFlow 工作流(需安装 com.chaomixian.vflow)。"
14+
en: "Trigger a VFlow workflow by workflow_id (requires com.chaomixian.vflow installed)."
15+
}
16+
parameters: [
17+
{ name: "workflow_id", description: { zh: "工作流 ID", en: "Workflow ID" }, type: "string", required: true }
18+
]
19+
}
20+
]
21+
}
22+
*/
23+
/// <reference path="./types/index.d.ts" />
24+
const VFlowTrigger = (function () {
25+
const DEFAULT_ACTION = "com.chaomixian.vflow.EXECUTE_WORKFLOW_SHORTCUT";
26+
const DEFAULT_COMPONENT = "com.chaomixian.vflow/.ui.common.ShortcutExecutorActivity";
27+
async function trigger_vflow_workflow(params) {
28+
if (!params || !params.workflow_id) {
29+
return { success: false, message: "workflow_id 不能为空" };
30+
}
31+
const result = await Tools.System.intent({
32+
type: "activity",
33+
action: DEFAULT_ACTION,
34+
component: DEFAULT_COMPONENT,
35+
extras: {
36+
workflow_id: params.workflow_id
37+
}
38+
});
39+
return {
40+
success: true,
41+
message: "已触发 VFlow 工作流",
42+
data: result
43+
};
44+
}
45+
async function wrapToolExecution(func, params) {
46+
try {
47+
const result = await func(params);
48+
complete(result);
49+
}
50+
catch (error) {
51+
console.error(`Tool ${func.name} failed unexpectedly`, error);
52+
complete({ success: false, message: String(error && error.message ? error.message : error) });
53+
}
54+
}
55+
return {
56+
trigger_vflow_workflow: (params) => wrapToolExecution(trigger_vflow_workflow, params)
57+
};
58+
})();
59+
exports.trigger_vflow_workflow = VFlowTrigger.trigger_vflow_workflow;

app/src/main/java/com/ai/assistance/operit/api/chat/llmprovider/AIServiceFactory.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -220,7 +220,7 @@ object AIServiceFactory {
220220
enableToolCall = enableToolCall
221221
)
222222
ApiProviderType.MOONSHOT ->
223-
OpenAIProvider(
223+
KimiProvider(
224224
apiEndpoint = config.apiEndpoint,
225225
apiKeyProvider = apiKeyProvider,
226226
modelName = config.modelName,
Lines changed: 290 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,290 @@
1+
package com.ai.assistance.operit.api.chat.llmprovider
2+
3+
import android.content.Context
4+
import com.ai.assistance.operit.data.model.ApiProviderType
5+
import com.ai.assistance.operit.data.model.ModelParameter
6+
import com.ai.assistance.operit.data.model.ToolPrompt
7+
import com.ai.assistance.operit.util.AppLogger
8+
import com.ai.assistance.operit.util.ChatUtils
9+
import com.ai.assistance.operit.util.stream.Stream
10+
import okhttp3.OkHttpClient
11+
import okhttp3.RequestBody
12+
import okhttp3.RequestBody.Companion.toRequestBody
13+
import org.json.JSONArray
14+
import org.json.JSONObject
15+
16+
/**
17+
* Kimi K2.5 Provider (Moonshot API).
18+
* Mirrors DeepseekProvider behavior for reasoning_content handling when thinking is enabled.
19+
*/
20+
class KimiProvider(
21+
apiEndpoint: String,
22+
apiKeyProvider: ApiKeyProvider,
23+
modelName: String,
24+
client: OkHttpClient,
25+
customHeaders: Map<String, String> = emptyMap(),
26+
providerType: ApiProviderType = ApiProviderType.MOONSHOT,
27+
supportsVision: Boolean = false,
28+
supportsAudio: Boolean = false,
29+
supportsVideo: Boolean = false,
30+
enableToolCall: Boolean = false
31+
) : OpenAIProvider(
32+
apiEndpoint = apiEndpoint,
33+
apiKeyProvider = apiKeyProvider,
34+
modelName = modelName,
35+
client = client,
36+
customHeaders = customHeaders,
37+
providerType = providerType,
38+
supportsVision = supportsVision,
39+
supportsAudio = supportsAudio,
40+
supportsVideo = supportsVideo,
41+
enableToolCall = enableToolCall
42+
) {
43+
44+
override fun createRequestBody(
45+
context: Context,
46+
message: String,
47+
chatHistory: List<Pair<String, String>>,
48+
modelParameters: List<ModelParameter<*>>,
49+
enableThinking: Boolean,
50+
stream: Boolean,
51+
availableTools: List<ToolPrompt>?,
52+
preserveThinkInHistory: Boolean
53+
): RequestBody {
54+
fun applyThinkingParams(jsonObject: JSONObject) {
55+
jsonObject.put(
56+
"thinking",
57+
JSONObject().apply {
58+
put("type", if (enableThinking) "enabled" else "disabled")
59+
}
60+
)
61+
}
62+
63+
if (!enableThinking) {
64+
val baseRequestBodyJson =
65+
super.createRequestBodyInternal(context, message, chatHistory, modelParameters, stream, availableTools, preserveThinkInHistory)
66+
val jsonObject = JSONObject(baseRequestBodyJson)
67+
applyThinkingParams(jsonObject)
68+
return jsonObject.toString().toRequestBody(JSON)
69+
}
70+
71+
val jsonObject = JSONObject()
72+
jsonObject.put("model", modelName)
73+
jsonObject.put("stream", stream)
74+
applyThinkingParams(jsonObject)
75+
76+
for (param in modelParameters) {
77+
if (param.isEnabled) {
78+
when (param.valueType) {
79+
com.ai.assistance.operit.data.model.ParameterValueType.INT ->
80+
jsonObject.put(param.apiName, param.currentValue as Int)
81+
com.ai.assistance.operit.data.model.ParameterValueType.FLOAT ->
82+
jsonObject.put(param.apiName, param.currentValue as Float)
83+
com.ai.assistance.operit.data.model.ParameterValueType.STRING ->
84+
jsonObject.put(param.apiName, param.currentValue as String)
85+
com.ai.assistance.operit.data.model.ParameterValueType.BOOLEAN ->
86+
jsonObject.put(param.apiName, param.currentValue as Boolean)
87+
com.ai.assistance.operit.data.model.ParameterValueType.OBJECT -> {
88+
val raw = param.currentValue.toString().trim()
89+
val parsed: Any? = try {
90+
when {
91+
raw.startsWith("{") -> JSONObject(raw)
92+
raw.startsWith("[") -> JSONArray(raw)
93+
else -> null
94+
}
95+
} catch (e: Exception) {
96+
AppLogger.w("KimiProvider", "OBJECT参数解析失败: ${param.apiName}", e)
97+
null
98+
}
99+
if (parsed != null) {
100+
jsonObject.put(param.apiName, parsed)
101+
} else {
102+
jsonObject.put(param.apiName, raw)
103+
}
104+
}
105+
}
106+
}
107+
}
108+
109+
val effectiveEnableToolCall = enableToolCall && availableTools != null && availableTools.isNotEmpty()
110+
111+
var toolsJson: String? = null
112+
if (effectiveEnableToolCall) {
113+
val tools = buildToolDefinitions(availableTools!!)
114+
if (tools.length() > 0) {
115+
jsonObject.put("tools", tools)
116+
jsonObject.put("tool_choice", "auto")
117+
toolsJson = tools.toString()
118+
}
119+
}
120+
121+
val messagesArray = buildMessagesWithReasoning(context, message, chatHistory, effectiveEnableToolCall)
122+
jsonObject.put("messages", messagesArray)
123+
124+
tokenCacheManager.calculateInputTokens(message, chatHistory, toolsJson)
125+
126+
val logJson = JSONObject(jsonObject.toString())
127+
if (logJson.has("tools")) {
128+
val toolsArray = logJson.getJSONArray("tools")
129+
logJson.put("tools", "[${toolsArray.length()} tools omitted for brevity]")
130+
}
131+
val sanitizedLogJson = sanitizeImageDataForLogging(logJson)
132+
logLargeString("KimiProvider", sanitizedLogJson.toString(4), "Final Kimi K2.5 request body: ")
133+
134+
return jsonObject.toString().toRequestBody(JSON)
135+
}
136+
137+
private fun buildMessagesWithReasoning(
138+
context: Context,
139+
message: String,
140+
chatHistory: List<Pair<String, String>>,
141+
useToolCall: Boolean
142+
): JSONArray {
143+
val messagesArray = JSONArray()
144+
145+
val isMessageInHistory = chatHistory.isNotEmpty() && chatHistory.last().second == message
146+
val effectiveHistory = if (isMessageInHistory) {
147+
chatHistory
148+
} else {
149+
chatHistory + ("user" to message)
150+
}
151+
152+
val lastToolCallIds = mutableListOf<String>()
153+
154+
if (effectiveHistory.isNotEmpty()) {
155+
val standardizedHistory = ChatUtils.mapChatHistoryToStandardRoles(effectiveHistory, extractThinking = true)
156+
val mergedHistory = mutableListOf<Pair<String, String>>()
157+
158+
for ((role, content) in standardizedHistory) {
159+
if (mergedHistory.isNotEmpty() && role == mergedHistory.last().first && role != "system") {
160+
val lastMessage = mergedHistory.last()
161+
mergedHistory[mergedHistory.size - 1] =
162+
Pair(lastMessage.first, lastMessage.second + "\n" + content)
163+
} else {
164+
mergedHistory.add(Pair(role, content))
165+
}
166+
}
167+
168+
for ((role, originalContent) in mergedHistory) {
169+
if (role == "assistant") {
170+
val (content, reasoningContent) = ChatUtils.extractThinkingContent(originalContent)
171+
172+
if (useToolCall) {
173+
val (textContent, toolCalls) = parseXmlToolCalls(content)
174+
val historyMessage = JSONObject()
175+
historyMessage.put("role", role)
176+
historyMessage.put("reasoning_content", reasoningContent)
177+
178+
val effectiveContent = if (content.isBlank()) {
179+
"[Empty]"
180+
} else if (textContent.isNotEmpty()) {
181+
textContent
182+
} else {
183+
null
184+
}
185+
186+
if (effectiveContent != null) {
187+
historyMessage.put("content", buildContentField(context, effectiveContent))
188+
} else {
189+
historyMessage.put("content", null)
190+
}
191+
192+
if (toolCalls != null && toolCalls.length() > 0) {
193+
historyMessage.put("tool_calls", toolCalls)
194+
lastToolCallIds.clear()
195+
for (i in 0 until toolCalls.length()) {
196+
lastToolCallIds.add(toolCalls.getJSONObject(i).getString("id"))
197+
}
198+
}
199+
200+
messagesArray.put(historyMessage)
201+
} else {
202+
val historyMessage = JSONObject()
203+
historyMessage.put("role", role)
204+
historyMessage.put("reasoning_content", reasoningContent)
205+
historyMessage.put("content", buildContentField(context, content.ifBlank { "[Empty]" }))
206+
messagesArray.put(historyMessage)
207+
}
208+
} else {
209+
if (useToolCall && role == "user") {
210+
val (textContent, toolResults) = parseXmlToolResults(originalContent)
211+
var hasHandledToolCalls = false
212+
213+
if (lastToolCallIds.isNotEmpty()) {
214+
val resultsList = toolResults ?: emptyList()
215+
val resultCount = resultsList.size
216+
val callCount = lastToolCallIds.size
217+
218+
for (i in 0 until callCount) {
219+
val toolCallId = lastToolCallIds[i]
220+
val toolMessage = JSONObject()
221+
toolMessage.put("role", "tool")
222+
toolMessage.put("tool_call_id", toolCallId)
223+
224+
if (i < resultCount) {
225+
val (_, resultContent) = resultsList[i]
226+
toolMessage.put("content", resultContent)
227+
} else {
228+
toolMessage.put("content", "User cancelled")
229+
}
230+
messagesArray.put(toolMessage)
231+
}
232+
233+
hasHandledToolCalls = true
234+
if (resultCount > callCount) {
235+
AppLogger.w("KimiProvider", "发现多余的tool_result: $resultCount results vs $callCount tool_calls")
236+
}
237+
lastToolCallIds.clear()
238+
}
239+
240+
if (textContent.isNotEmpty()) {
241+
val userMessage = JSONObject()
242+
userMessage.put("role", "user")
243+
userMessage.put("content", buildContentField(context, textContent))
244+
messagesArray.put(userMessage)
245+
} else if (!hasHandledToolCalls) {
246+
val historyMessage = JSONObject()
247+
historyMessage.put("role", role)
248+
historyMessage.put("content", buildContentField(context, originalContent))
249+
messagesArray.put(historyMessage)
250+
} else {
251+
}
252+
} else {
253+
val historyMessage = JSONObject()
254+
historyMessage.put("role", role)
255+
historyMessage.put("content", buildContentField(context, originalContent))
256+
messagesArray.put(historyMessage)
257+
}
258+
}
259+
}
260+
}
261+
262+
return messagesArray
263+
}
264+
265+
override suspend fun sendMessage(
266+
context: Context,
267+
message: String,
268+
chatHistory: List<Pair<String, String>>,
269+
modelParameters: List<ModelParameter<*>>,
270+
enableThinking: Boolean,
271+
stream: Boolean,
272+
availableTools: List<ToolPrompt>?,
273+
preserveThinkInHistory: Boolean,
274+
onTokensUpdated: suspend (input: Int, cachedInput: Int, output: Int) -> Unit,
275+
onNonFatalError: suspend (error: String) -> Unit
276+
): Stream<String> {
277+
return super.sendMessage(
278+
context,
279+
message,
280+
chatHistory,
281+
modelParameters,
282+
enableThinking,
283+
stream,
284+
availableTools,
285+
preserveThinkInHistory,
286+
onTokensUpdated,
287+
onNonFatalError
288+
)
289+
}
290+
}

0 commit comments

Comments
 (0)