Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 3 additions & 18 deletions .github/workflows/build-agent.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -30,27 +30,12 @@ jobs:
distribution: temurin
java-version: 21

- name: Restore stable Agent debug signing key
uses: actions/cache@v5
with:
path: ~/.android/debug.keystore
key: clash-meta-agent-debug-keystore-v1

- name: Ensure stable Agent debug signing key
- name: Restore stable Agent debug signing key from secret
shell: bash
run: |
mkdir -p "$HOME/.android"
if [ ! -f "$HOME/.android/debug.keystore" ]; then
keytool -genkeypair -v \
-keystore "$HOME/.android/debug.keystore" \
-storepass android \
-alias androiddebugkey \
-keypass android \
-keyalg RSA \
-keysize 2048 \
-validity 10000 \
-dname "CN=Clash Meta AI Debug,O=Clash Meta AI,C=CN"
fi
echo "${{ secrets.AGENT_DEBUG_KEYSTORE }}" | base64 -d > "$HOME/.android/debug.keystore"
test -s "$HOME/.android/debug.keystore"

- name: Set up Gradle
uses: gradle/actions/setup-gradle@v5
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -212,8 +212,12 @@ class AgentEngine(
10. Do not call nonexistent tools or ask the user to manually edit files that an exposed tool can handle.
11. App-aware routing has two layers: use YAML rules for policy selection and access_control_replace only when the
user wants Android to include/exclude entire apps from the VPN. Call installed_apps first and use exact packages.
12. Before changing app overrides or Android VPN settings, read their current complete state and preserve fields the
user did not ask to change. Prefer runtime_set_mode for a temporary mode switch.
12. Before changing app overrides or Android VPN settings, read their current complete state and preserve fields the
user did not ask to change. Prefer runtime_set_mode for a temporary mode switch.
13. Before any modification of a profile, override, or DNS/TUN setting, call runtime.status and note core_version.
Only write fields supported by that mihomo core version: never emit YAML options the running core does not
support. If the user asks for a feature that depends on a newer core, say so and propose the closest supported
alternative instead of writing an invalid field.
""".trimIndent()
}
}
8 changes: 7 additions & 1 deletion app/src/agent/java/com/github/kr328/clash/AgentActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,13 @@ class AgentActivity : BaseActivity<AgentScreenDesign>() {
smoothStream.finish(finalText)
adapter.replace(assistantPosition, assistantMessage.copy(content = finalText))
} catch (_: CancellationException) {
adapter.replace(assistantPosition, assistantMessage.copy(content = "已停止本次操作。"))
val preserved = smoothStream.currentText().trimEnd()
val content = if (preserved.isBlank()) {
"已停止本次操作。"
} else {
"$preserved\n\n> 已停止本次操作。"
}
adapter.replace(assistantPosition, assistantMessage.copy(content = content))
} catch (error: Throwable) {
val detail = error.message?.take(1200) ?: error.javaClass.simpleName
adapter.replace(
Expand Down
107 changes: 106 additions & 1 deletion app/src/agent/java/com/github/kr328/clash/agent/AgentChatAdapter.kt
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
package com.github.kr328.clash.agent

import android.content.Context
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Paint
import android.text.Layout
import android.text.NoCopySpan
import android.text.Spannable
import android.text.SpannableString
import android.text.SpannableStringBuilder
import android.text.Spanned
import android.text.style.LeadingMarginSpan
import android.text.style.LineBackgroundSpan
import android.util.TypedValue
import android.view.Gravity
import android.view.LayoutInflater
Expand All @@ -20,7 +25,12 @@ import com.github.kr328.clash.R
import com.github.kr328.clash.agent.model.AgentConversationMessage
import com.github.kr328.clash.agent.model.AgentMessageRole
import com.google.android.material.card.MaterialCardView
import io.noties.markwon.AbstractMarkwonPlugin
import io.noties.markwon.Markwon
import io.noties.markwon.MarkwonSpansFactory
import io.noties.markwon.core.MarkwonTheme
import org.commonmark.node.FencedCodeBlock
import org.commonmark.node.IndentedCodeBlock
import java.io.Closeable
import java.util.UUID
import java.util.concurrent.Executors
Expand All @@ -40,7 +50,7 @@ class AgentChatAdapter(
val messages: MutableList<AgentConversationMessage>,
private val onContentHeightChanged: (String) -> Unit = {},
) : RecyclerView.Adapter<AgentChatAdapter.Holder>(), Closeable {
private val markwon = Markwon.create(context)
private val markwon = createMarkwon(context)
private val markdownExecutor = Executors.newSingleThreadExecutor()
private val attachedHolders = mutableSetOf<Holder>()

Expand Down Expand Up @@ -251,6 +261,101 @@ class AgentChatAdapter(
return if (context.theme.resolveAttribute(attribute, value, true)) value.data else fallback
}

private fun createMarkwon(context: Context): Markwon {
val primary = resolve(com.google.android.material.R.attr.colorPrimary, Color.rgb(25, 118, 210))
val codeBackground = resolve(
com.google.android.material.R.attr.colorSurfaceVariant,
Color.rgb(0xE6, 0xE6, 0xE9)
)
val codeText = resolve(
com.google.android.material.R.attr.colorOnSurfaceVariant,
Color.rgb(0x44, 0x44, 0x44)
)
val quoteColor = resolve(
com.google.android.material.R.attr.colorPrimary,
Color.rgb(25, 118, 210)
)
val radius = 8 * context.resources.displayMetrics.density

return Markwon.builder(context)
.usePlugin(object : AbstractMarkwonPlugin() {
override fun configureTheme(builder: MarkwonTheme.Builder) {
builder
.linkColor(primary)
.codeTextColor(codeText)
.codeBackgroundColor(codeBackground)
.codeBlockTextColor(codeText)
.codeBlockBackgroundColor(codeBackground)
.blockQuoteColor(quoteColor)
.blockQuoteWidth((3 * context.resources.displayMetrics.density).toInt())
}

override fun configureSpansFactory(builder: MarkwonSpansFactory.Builder) {
builder
.setFactory(FencedCodeBlock::class.java) { _, _ ->
arrayOf(RoundedCodeBlockSpan(codeBackground, radius))
}
.setFactory(IndentedCodeBlock::class.java) { _, _ ->
arrayOf(RoundedCodeBlockSpan(codeBackground, radius))
}
}
})
.build()
}

/**
* Rounded background behind code blocks, replacing Markwon's flat gray rectangle.
* A [LeadingMarginSpan] so indentation of wrapped lines stays aligned.
*/
private class RoundedCodeBlockSpan(
private val backgroundColor: Int,
private val cornerRadius: Float,
) : LeadingMarginSpan, LineBackgroundSpan {
private val paint = Paint().apply { isAntiAlias = true }

override fun getLeadingMargin(first: Boolean): Int = 0

override fun drawLeadingMargin(
c: Canvas,
p: Paint,
x: Int,
dir: Int,
top: Int,
baseline: Int,
bottom: Int,
text: CharSequence,
start: Int,
end: Int,
first: Boolean,
layout: Layout,
) = Unit

override fun drawBackground(
c: Canvas,
p: Paint,
left: Int,
right: Int,
top: Int,
baseline: Int,
bottom: Int,
text: CharSequence,
start: Int,
end: Int,
lineNumber: Int,
) {
paint.color = backgroundColor
c.drawRoundRect(
left.toFloat(),
top.toFloat(),
right.toFloat(),
bottom.toFloat(),
cornerRadius,
cornerRadius,
paint,
)
}
}

private fun String.toStableLong(): Long = runCatching {
UUID.fromString(this).let { it.mostSignificantBits xor it.leastSignificantBits }
}.getOrElse { hashCode().toLong() }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import android.net.NetworkCapabilities
import android.os.Build
import android.os.ParcelFileDescriptor
import androidx.core.content.getSystemService
import com.github.kr328.clash.BuildConfig
import com.github.kr328.clash.core.Clash
import com.github.kr328.clash.agent.model.AgentToolExecutionResult
import com.github.kr328.clash.agent.runtime.AgentToolExecutor
Expand Down Expand Up @@ -433,6 +434,8 @@ class AndroidAgentToolExecutor(
put("active_profile", active?.name ?: "")
put("active_profile_id", active?.uuid?.toString() ?: "")
put("mode", state.mode.name)
put("core_version", com.github.kr328.clash.core.bridge.Bridge.nativeCoreVersion())
put("app_version", BuildConfig.VERSION_NAME)
put("traffic_total", queryTrafficTotal())
put("selectable_groups", buildJsonArray { groups.forEach { add(kotlinx.serialization.json.JsonPrimitive(it)) } })
put("provider_count", queryProviders().size)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,9 @@ class SmoothMarkdownStream(
finishContinuation = null
}

/** The text currently committed to the UI. Safe to call from any thread. */
fun currentText(): String = visible

override fun doFrame(frameTimeNanos: Long) {
scheduled = false
if (cancelled) return
Expand Down Expand Up @@ -164,14 +167,14 @@ class SmoothMarkdownStream(
const val NANOS_PER_SECOND = 1_000_000_000.0
const val START_DELAY_NANOS = 24_000_000L
const val MAX_DELTA_NANOS = 100_000_000L
const val MIN_COMMIT_INTERVAL_NANOS = 33_000_000L
const val MIN_CHARS_PER_SECOND = 60.0
const val MAX_CHARS_PER_SECOND = 1_600.0
const val MIN_COMMIT_INTERVAL_NANOS = 66_000_000L
const val MIN_CHARS_PER_SECOND = 80.0
const val MAX_CHARS_PER_SECOND = 1_400.0
const val TARGET_LATENCY_SECONDS = 0.42
const val FINISH_LATENCY_SECONDS = 0.16
const val CATCH_UP_LATENCY_SECONDS = 0.24
const val CATCH_UP_THRESHOLD = 560
const val MAX_CHARS_PER_COMMIT = 96
const val MAX_CHARS_PER_COMMIT = 120
const val SPEED_EASING = 0.2
const val ZERO_WIDTH_JOINER = 0x200D
}
Expand Down
1 change: 1 addition & 0 deletions app/src/agent/res/layout/item_agent_message.xml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
android:id="@+id/agent_message_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:includeFontPadding="false"
android:lineSpacingExtra="2dp"
android:minWidth="48dp"
android:paddingHorizontal="14dp"
Expand Down
15 changes: 0 additions & 15 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -182,27 +182,12 @@
android:configChanges="uiMode"
android:exported="false"
android:label="@string/help" />
<activity
android:name=".DiagnosticsActivity"
android:configChanges="uiMode"
android:exported="false"
android:label="@string/diagnostics" />
<activity
android:name=".FilesActivity"
android:configChanges="uiMode"
android:exported="false"
android:label="@string/files" />

<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.fileprovider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths" />
</provider>

<service
android:name=".LogcatService"
android:exported="false"
Expand Down
Loading
Loading