diff --git a/.gitignore b/.gitignore index 1e8a554c..0237fa1a 100644 --- a/.gitignore +++ b/.gitignore @@ -43,3 +43,7 @@ app/src/main/assets/model-catalog.json # ---- 打包产物(tools/package-module.py,W7) ---- agentos-*.zip + +# 发布目录(tools/package-release.py、tools/package-samples.py 的默认输出):签名后的 APK、zip、SHA256SUMS 都不进仓库 +dist-release/ +dist-samples/ diff --git a/README.en.md b/README.en.md index 3790cd34..0d421b45 100644 --- a/README.en.md +++ b/README.en.md @@ -140,7 +140,9 @@ Key frames, left to right: the instruction typed before sending; the Thursday 15 > [!IMPORTANT] > You need a **rooted** Android 15–17 phone with Magisk or KernelSU installed. The combination verified on a real device is a Pixel 8 (Magisk 30.7, Android 15). Android 15 / 16 / 17 have run on emulators. KernelSU and Android 16 / 17 real devices are not verified yet. -**1. Build the module zip.** There is no published release yet, so build it from source (JDK 21, Android SDK and Node 22.19+; see the [development guide](docs/development.md), in Chinese): +**Shortest path:** download **all files** from [Releases](https://github.com/HanZijie/agentos-android/releases) into one folder and run `sh install.sh`. It verifies the files, flashes the module, reboots, and installs AgentOS and the sample apps. **What the script cannot do** (unlocking / rooting, USB debugging, granting root, the model key, enabling plugins, the SMS permission) and **what to do when something goes wrong** are listed step by step in [docs/install.md](docs/install.md) (Chinese first, English below). To package it yourself from source: + +**1. Build the module zip.** Build it from source (JDK 21, Android SDK and Node 22.19+; see the [development guide](docs/development.md), in Chinese): ```bash python3 tools/package-module.py --variant debug # output: build/module/agentos-.zip @@ -335,6 +337,7 @@ Work packages start by dependency and do not wait for the previous milestone to | [docs/development.md](docs/development.md) | Development guide: environment, common commands, packaging the module zip, modules and source layout, sample apps, the release certificate | | [docs/implementation-plan.md](docs/implementation-plan.md) | File-level layout, build and artifacts (with pinned dependency versions), 8 validations, dependency graph, 28 work packages and M1–M6 exit criteria, migration from agenroid, risks | | [docs/m1-acceptance.md](docs/m1-acceptance.md) | Acceptance checklist: real-device and emulator verification records, defects found and fixed, what is not verified | +| [docs/install.md](docs/install.md) | Install guide: what the script cannot do (root, USB debugging, grants, key, plugins, SMS permission), upgrading, troubleshooting | | [docs/sample-apps.md](docs/sample-apps.md) | The alarm, calendar and notes sample apps: tool lists, data, build | | [docs/spikes/](docs/spikes/) | Conclusions of each validation: S1 (module installs an APK), S2 (keep-alive and root supervision), S3 (ACP over Binder), S8 (Pi Agent core in QuickJS). The experiment projects live in `spikes/` and are not part of the main build | | [core/protocol/acp-profile-v1.md](core/protocol/acp-profile-v1.md) | The external protocol; this is the source of truth | diff --git a/README.md b/README.md index aaf50ded..fea2a4a5 100644 --- a/README.md +++ b/README.md @@ -138,7 +138,9 @@ Agent 通过三个示例 App 提供的 MCP 工具,分别创建备忘录、日 > [!IMPORTANT] > 需要一台**已 root** 的 Android 15–17 手机,装好 Magisk 或 KernelSU。真机验证过的组合是 Pixel 8(Magisk 30.7,Android 15);模拟器上跑过 Android 15 / 16 / 17;KernelSU 和 Android 16 / 17 真机还没验证过。 -**1. 构建模块 zip。** 目前还没有发布版本,需要从源码打包(JDK 21、Android SDK、Node 22.19+,详见[开发指南](docs/development.md)): +**最短路径:** 从 [Releases](https://github.com/HanZijie/agentos-android/releases) 下载**所有文件**到同一个文件夹,运行 `sh install.sh`。它会校验文件、刷入模块、重启、装好 AgentOS 和示例 App。**脚本做不了的部分**(解锁 / root、USB 调试、授予 root、模型 key、启用插件、短信权限)和**出问题怎么办**,逐条写在 [docs/install.md](docs/install.md)。下面是从源码自己打包的方式: + +**1. 构建模块 zip。** 从源码打包(JDK 21、Android SDK、Node 22.19+,详见[开发指南](docs/development.md)): ```bash python3 tools/package-module.py --variant debug # 产物在 build/module/agentos-.zip @@ -331,6 +333,7 @@ AgentOS 目前是用于验证产品形态与技术路径的原型。 | [docs/development.md](docs/development.md) | 开发指南:环境、常用命令、打包模块 zip、模块与源码位置、示例 App、项目发布证书 | | [docs/implementation-plan.md](docs/implementation-plan.md) | 文件级目录、构建与产物(含依赖版本锁定)、8 项验证、依赖顺序图、28 个工作包与 M1–M6 出口条件、从 agenroid 迁移、风险 | | [docs/m1-acceptance.md](docs/m1-acceptance.md) | 验收清单:真机与模拟器的验证记录、发现并修好的缺陷、没验证的部分 | +| [docs/install.md](docs/install.md) | 安装指南:脚本解决不了的部分(root、USB 调试、授权、key、插件、短信权限)、升级、故障排查 | | [docs/sample-apps.md](docs/sample-apps.md) | 闹钟、日历、备忘录三个示例 App:工具清单、数据、构建 | | [docs/spikes/](docs/spikes/) | 各项验证的结论:S1(模块安装 APK)、S2(保活与 root 监督)、S3(ACP over Binder)、S8(Pi Agent core 在 QuickJS 里);实验工程在 `spikes/`,不参与主构建 | | [core/protocol/acp-profile-v1.md](core/protocol/acp-profile-v1.md) | 对外协议,以此为准 | diff --git a/app/src/debug/java/org/agentos/app/agent/ConsentDebugReceiver.kt b/app/src/debug/java/org/agentos/app/agent/ConsentDebugReceiver.kt index 20ab026a..b2a85e40 100644 --- a/app/src/debug/java/org/agentos/app/agent/ConsentDebugReceiver.kt +++ b/app/src/debug/java/org/agentos/app/agent/ConsentDebugReceiver.kt @@ -8,6 +8,8 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.launch import org.agentos.app.agent.consent.ConsentWire +import org.agentos.app.i18n.AndroidStrings +import org.agentos.app.i18n.Strings import org.agentos.runtime.consent.AutoConsentResponder import org.agentos.runtime.ports.CallerIdentity import org.agentos.runtime.ports.CallerKind @@ -17,6 +19,7 @@ import org.agentos.runtime.ports.ToolRisk import org.agentos.runtime.ports.ToolSource import org.json.JSONArray import org.json.JSONObject +import java.util.Locale import java.util.concurrent.ConcurrentHashMap /** @@ -43,6 +46,9 @@ import java.util.concurrent.ConcurrentHashMap class ConsentDebugReceiver : BroadcastReceiver() { override fun onReceive(context: Context, intent: Intent) { val process = AgentProcess.get(context) + // 设备测试脚本(tests/device/acp-channel/*.py)按中文整句比对 title / initiatorLine / source 等字段:这里固定用中文渲染, + // 不跟界面语言(界面切到英文时脚本的判据不能变)。线上的 wire 格式是 key + 参数,这里只是把它在测试出口展开。 + val zh: Strings = AndroidStrings.forLocale(context, Locale.SIMPLIFIED_CHINESE) val op = intent.getStringExtra("op") val responder = process.autoConsent val result = try { @@ -70,6 +76,9 @@ class ConsentDebugReceiver : BroadcastReceiver() { JSONArray( process.consent.pending.value.map { JSONObject(ConsentWire.encodeViewString(it)).put("remainingMs", it.deadlineMillis - System.currentTimeMillis()) + .put("title", zh.get(it.title)) + .put("initiatorLine", zh.get(it.initiatorLine)) + .put("sourceLine", it.sourceLine?.let { ref -> zh.get(ref) } ?: JSONObject.NULL) }, ), ) @@ -102,12 +111,12 @@ class ConsentDebugReceiver : BroadcastReceiver() { .put("requestId", e.requestId) .put("tool", e.toolName) .put("risk", e.risk.name) - .put("source", e.sourceLine ?: JSONObject.NULL) + .put("source", e.sourceLine?.let { zh.get(it) } ?: JSONObject.NULL) .put("args", e.argumentsSummary) .put("options", JSONArray(e.options.map { it.name })) .put("answeredWith", e.answeredWith?.name ?: JSONObject.NULL) .put("end", e.end?.name ?: JSONObject.NULL) - .put("notice", e.notice ?: JSONObject.NULL) + .put("notice", e.notice?.let { zh.get(it) } ?: JSONObject.NULL) }, ), ) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index dbae0a41..043e73a9 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -46,6 +46,7 @@ android:name=".AgentOSApp" android:allowBackup="false" android:label="AgentOS" + android:localeConfig="@xml/locales_config" android:networkSecurityConfig="@xml/network_security_config"> diff --git a/app/src/main/aidl/org/agentos/internal/IExtensionHost.aidl b/app/src/main/aidl/org/agentos/internal/IExtensionHost.aidl index 6c95e581..9a84c875 100644 --- a/app/src/main/aidl/org/agentos/internal/IExtensionHost.aidl +++ b/app/src/main/aidl/org/agentos/internal/IExtensionHost.aidl @@ -27,12 +27,13 @@ import org.agentos.internal.IExtensionCallback; * "status":"ready"|"unavailable"|"signature_changed"|"signature_unconfirmed"(PluginStatus), * "unavailableReason":"assets_missing"|"manifest_rejected"|"no_usable_server"|"name_conflict"|null, * "builtin": bool, "enabled": bool(插件级是否启用), "approval":"ask"|"always"|null(插件级审批方式,null = 没设,按 ask), - * "problems":[{"code","message"}](中文,给插件页显示), "unsupported":[{"kind","location","detail"}](PluginManifest.unsupported), + * "problems":[{"code","message":{"key","args":[…]},"location"|null}](message 是文案 key + 参数,ExtMessages;界面按自己的语言渲染,不是文字), + * "unsupported":[{"kind","location","detail":{"key","args":[…]}}](PluginManifest.unsupported;detail 同 message), * "servers":[{"name", "service"(完整类名,Binder 服务器;否则 null), "url"(https,远端服务器;否则 null), * "state":"idle"|"connected"|"unreachable"|"disabled", "error"(或 null), "toolCount"}], * "rejectedServers":[{"name","service","reason":"not_in_package"|"not_exported"|"missing_permission"}], * "toolCount", "skillCount", - * "skillProblems":[{"code","message"}](ExtensionSkillPort.problems:frontmatter 缺失 / 非法、SKILL.md 读不到等,A10)} + * "skillProblems":[{"code","message":{"key","args"},"location"}](ExtensionSkillPort.problems:frontmatter 缺失 / 非法、SKILL.md 读不到等,A10;location 是 Skill 的目录名)} * 签名变化(signature_changed)或记忆丢失(signature_unconfirmed)时已停用,要用户先 confirmSignature 再启用。 * * 已知工具(listTools 的一项;setToolEnabled / setToolApproval / setToolApprovalBySource 的返回值):与 core:extensions 的 diff --git a/app/src/main/java/org/agentos/app/agent/AgentService.kt b/app/src/main/java/org/agentos/app/agent/AgentService.kt index 8fc03b8b..547c1b05 100644 --- a/app/src/main/java/org/agentos/app/agent/AgentService.kt +++ b/app/src/main/java/org/agentos/app/agent/AgentService.kt @@ -10,6 +10,9 @@ import android.content.pm.ServiceInfo import android.graphics.drawable.Icon import android.os.IBinder import android.util.Log +import org.agentos.app.R +import org.agentos.app.i18n.AndroidStrings +import org.agentos.app.i18n.Strings import org.agentos.app.settings.Desktop import org.agentos.app.settings.SettingsActivity @@ -30,6 +33,7 @@ import org.agentos.app.settings.SettingsActivity */ class AgentService : Service() { private lateinit var runtime: AgentProcess + private val strings: Strings by lazy { AndroidStrings(this) } override fun onCreate() { super.onCreate() @@ -86,15 +90,15 @@ class AgentService : Service() { private fun buildNotification(desktopOn: Boolean, busy: Boolean): Notification { val nm = getSystemService(NotificationManager::class.java) if (!desktopOn) { - ensureChannel(nm, CHANNEL, "Agent 运行时") + ensureChannel(nm, CHANNEL, strings.get(R.string.runtime_notification_channel)) return Notification.Builder(this, CHANNEL) - .setContentTitle("AgentOS 正在运行任务") + .setContentTitle(strings.get(R.string.runtime_notification_title)) .setSmallIcon(android.R.drawable.stat_notify_sync) .setOngoing(true) .build() } // 用词与设置页(D,SettingsActivity 的“电脑端接入”一节和关闭确认)一致 - ensureChannel(nm, CHANNEL_DESKTOP, DESKTOP_CHANNEL_NAME) + ensureChannel(nm, CHANNEL_DESKTOP, strings.get(R.string.desktop_channel_name)) val settings = PendingIntent.getActivity( this, 0, Intent(this, SettingsActivity::class.java).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK), PendingIntent.FLAG_IMMUTABLE, ) @@ -102,23 +106,25 @@ class AgentService : Service() { this, 0, Intent(this, DesktopAccessOffReceiver::class.java), PendingIntent.FLAG_IMMUTABLE, ) return Notification.Builder(this, CHANNEL_DESKTOP) - .setContentTitle(DESKTOP_TITLE) - .setContentText(DESKTOP_TEXT) - .setSubText(if (busy) DESKTOP_BUSY else null) + .setContentTitle(Desktop.notificationTitle(strings)) + .setContentText(strings.get(R.string.desktop_notification_text)) + // the one-line form cuts this sentence at a large font size (English is longer): the expanded form shows all of it + .setStyle(Notification.BigTextStyle().bigText(strings.get(R.string.desktop_notification_text))) + .setSubText(if (busy) strings.get(R.string.desktop_notification_busy) else null) .setSmallIcon(android.R.drawable.stat_notify_sync) .setOngoing(true) .setShowWhen(false) .setCategory(Notification.CATEGORY_SERVICE) .setForegroundServiceBehavior(Notification.FOREGROUND_SERVICE_IMMEDIATE) .setContentIntent(settings) - .addAction(Notification.Action.Builder(null as Icon?, DESKTOP_OFF, off).build()) + .addAction(Notification.Action.Builder(null as Icon?, Desktop.notificationAction(strings), off).build()) .build() } private fun ensureChannel(nm: NotificationManager, id: String, name: String) { - if (nm.getNotificationChannel(id) == null) { - nm.createNotificationChannel(NotificationChannel(id, name, NotificationManager.IMPORTANCE_LOW)) - } + // createNotificationChannel on an existing id only updates the name (and description): the channel list in the + // system settings follows the app language, and the user's own importance choice is kept + nm.createNotificationChannel(NotificationChannel(id, name, NotificationManager.IMPORTANCE_LOW)) } override fun onDestroy() { @@ -143,14 +149,9 @@ class AgentService : Service() { private const val NOTIFICATION_ID = 1 /** - * 电脑端接入打开期间的通知(F11 第 4 点)。标题和按钮用设置页(D,Desktop.kt)的常量,开关旁的说明引用的就是它们; - * 正文沿用设置页的开关文案(“允许电脑经 adb 连接”)和关闭确认框的说法。 + * 电脑端接入打开期间的通知(F11 第 4 点)。标题和按钮用设置页(D,Desktop.kt)的文案,开关旁的说明引用的就是它们; + * 正文沿用设置页的开关文案(“允许电脑经 adb 连接”)和关闭确认框的说法。文字都在 strings_p2.xml(desktop_*、runtime_*)。 */ private const val CHANNEL_DESKTOP = "desktop_access" - const val DESKTOP_CHANNEL_NAME = "电脑端接入" - const val DESKTOP_TITLE = Desktop.NOTIFICATION_TITLE - const val DESKTOP_TEXT = "允许电脑经 adb 连接。关闭会断开连接,并作废已配对的电脑。" - const val DESKTOP_OFF = Desktop.NOTIFICATION_ACTION - const val DESKTOP_BUSY = "正在运行任务" } } diff --git a/app/src/main/java/org/agentos/app/agent/consent/ConsentBridge.kt b/app/src/main/java/org/agentos/app/agent/consent/ConsentBridge.kt index c986fd89..263ff40a 100644 --- a/app/src/main/java/org/agentos/app/agent/consent/ConsentBridge.kt +++ b/app/src/main/java/org/agentos/app/agent/consent/ConsentBridge.kt @@ -10,6 +10,9 @@ import android.graphics.drawable.Icon import android.os.IBinder import android.os.RemoteCallbackList import android.util.Log +import org.agentos.app.R +import org.agentos.app.i18n.AndroidStrings +import org.agentos.app.i18n.Strings import org.agentos.app.ui.consent.AuthorizationLabels import org.agentos.app.ui.consent.ConsentActivity import org.agentos.internal.IConsentListener @@ -35,6 +38,8 @@ import java.util.concurrent.ConcurrentHashMap */ class ConsentBridge(private val context: Context, private val log: (String) -> Unit = { Log.w(TAG, it) }) : ConsentSurface, AuthorizationSurface { @Volatile private var coordinator: ConsentCoordinator? = null + /** 通知文字按当前界面语言(`:agent` 进程的 Resources 跟随应用语言);每次现取,不缓存文字。 */ + private val strings: Strings = AndroidStrings(context) /** 监听者所在的主进程死了(被杀、崩溃)时,还没答复的请求转成通知:用户不会因为界面没了就收不到确认。 */ private val listeners = object : RemoteCallbackList() { override fun onCallbackDied(callback: IConsentListener?) { @@ -168,9 +173,9 @@ class ConsentBridge(private val context: Context, private val log: (String) -> U val deny = action(id, view.requestId, ConsentChoice.DENY) val b = Notification.Builder(context, channel) .setSmallIcon(android.R.drawable.ic_dialog_alert) - .setContentTitle(card.title) - .setContentText(listOfNotNull(card.initiatorLine, card.sourceLine).joinToString(" · ")) - .setStyle(Notification.BigTextStyle().bigText(listOfNotNull(card.initiatorLine, card.sourceLine, card.riskDescription.ifEmpty { null }, card.argumentsPreview.ifEmpty { null }).joinToString("\n"))) + .setContentTitle(strings.get(card.title)) + .setContentText(listOfNotNull(strings.get(card.initiatorLine), card.sourceLine?.let { strings.get(it) }).joinToString(" · ")) + .setStyle(Notification.BigTextStyle().bigText(listOfNotNull(strings.get(card.initiatorLine), card.sourceLine?.let { strings.get(it) }, strings.get(card.riskDescription).ifEmpty { null }, card.argumentsPreview.ifEmpty { null }).joinToString("\n"))) .setCategory(Notification.CATEGORY_ALARM) .setContentIntent(open) .setOngoing(true) @@ -205,11 +210,11 @@ class ConsentBridge(private val context: Context, private val log: (String) -> U .putExtra(ConsentActionReceiver.EXTRA_AUTH_DENY, true) .setPackage(context.packageName) val denyPi = PendingIntent.getBroadcast(context, id * 4 + 3, deny, PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT) - val text = AuthorizationLabels.notificationText(req) - val big = listOfNotNull(text, AuthorizationLabels.signatureChangedLine(req), AuthorizationLabels.EXPLANATION).joinToString("\n") + val text = AuthorizationLabels.notificationText(req, strings) + val big = listOfNotNull(text, AuthorizationLabels.signatureChangedLine(req, strings), AuthorizationLabels.explanation(strings)).joinToString("\n") val b = Notification.Builder(context, CHANNEL) .setSmallIcon(android.R.drawable.ic_dialog_alert) - .setContentTitle(AuthorizationLabels.title(req, label)) + .setContentTitle(AuthorizationLabels.title(req, label, strings)) .setContentText(text) .setStyle(Notification.BigTextStyle().bigText(big)) .setCategory(Notification.CATEGORY_ALARM) @@ -220,7 +225,7 @@ class ConsentBridge(private val context: Context, private val log: (String) -> U .setWhen(req.arrivalMillis) .setTimeoutAfter((req.deadlineMillis - System.currentTimeMillis()).coerceAtLeast(1_000)) // 只有“拒绝”;允许要点开通知,在对话框里看清包名和签名 - b.addAction(Notification.Action.Builder(null as Icon?, AuthorizationLabels.DENY, denyPi).build()) + b.addAction(Notification.Action.Builder(null as Icon?, strings.get(R.string.auth_deny), denyPi).build()) nm().notify(TAG, id, b.build()) } catch (e: Exception) { log("authorization notification failed: ${e.javaClass.simpleName}") @@ -235,7 +240,7 @@ class ConsentBridge(private val context: Context, private val log: (String) -> U val pi = PendingIntent.getBroadcast( context, id * 4 + choice.ordinal, intent, PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT, ) - val label = if (choice == ConsentChoice.DENY) "拒绝" else "允许一次" + val label = strings.get(if (choice == ConsentChoice.DENY) R.string.consent_option_deny else R.string.consent_option_allow_once) return Notification.Action.Builder(null as Icon?, label, pi).build() } @@ -247,10 +252,10 @@ class ConsentBridge(private val context: Context, private val log: (String) -> U private fun ensureChannels() { val m = nm() if (m.getNotificationChannel(CHANNEL) == null) { - m.createNotificationChannel(NotificationChannel(CHANNEL, "工具确认", NotificationManager.IMPORTANCE_HIGH)) + m.createNotificationChannel(NotificationChannel(CHANNEL, strings.get(R.string.consent_channel_normal), NotificationManager.IMPORTANCE_HIGH)) } if (m.getNotificationChannel(CHANNEL_HIGH) == null) { - m.createNotificationChannel(NotificationChannel(CHANNEL_HIGH, "高风险工具确认", NotificationManager.IMPORTANCE_HIGH)) + m.createNotificationChannel(NotificationChannel(CHANNEL_HIGH, strings.get(R.string.consent_channel_high), NotificationManager.IMPORTANCE_HIGH)) } } diff --git a/app/src/main/java/org/agentos/app/agent/consent/ConsentWire.kt b/app/src/main/java/org/agentos/app/agent/consent/ConsentWire.kt index 3e8c9860..360180cc 100644 --- a/app/src/main/java/org/agentos/app/agent/consent/ConsentWire.kt +++ b/app/src/main/java/org/agentos/app/agent/consent/ConsentWire.kt @@ -2,6 +2,7 @@ package org.agentos.app.agent.consent import kotlinx.serialization.json.Json import kotlinx.serialization.json.JsonArray +import kotlinx.serialization.json.JsonElement import kotlinx.serialization.json.JsonNull import kotlinx.serialization.json.JsonObject import kotlinx.serialization.json.JsonPrimitive @@ -13,15 +14,20 @@ import kotlinx.serialization.json.long import kotlinx.serialization.json.put import org.agentos.runtime.consent.ConsentChoice import org.agentos.runtime.consent.ConsentEnd +import org.agentos.runtime.consent.ConsentMessages import org.agentos.runtime.consent.ConsentResolution import org.agentos.runtime.consent.ConsentSeverity import org.agentos.runtime.consent.ConsentView +import org.agentos.runtime.i18n.MessageRef /** * `:agent` ↔ 主进程之间的确认数据([org.agentos.internal.IConsentListener]):纯 Kotlin,可在电脑上单测(ConsentWireTest)。 * * [ConsentView] 里所有第三方文字已由协调器清理;这里只搬运,不再解释。主进程收到的 JSON 当不可信输入读:缺字段、类型不对、 * 选项名不认识都按“这条请求不显示”处理([parseCard] 返回 null),不会崩。 + * + * 核心层给的文案是 [MessageRef](key + 参数),原样过 IPC(`{"key":"…","args":["…"]}`),主进程按自己的界面语言渲染(`Strings.get(ref)`); + * 这里不生成任何自然语言。 */ object ConsentWire { private val json = Json { ignoreUnknownKeys = true } @@ -57,18 +63,18 @@ object ConsentWire { /** 主进程显示用的一条待确认请求。和 [ConsentView] 一一对应,只保留界面要的字段。 */ data class Card( override val requestId: String, - val title: String, - val initiatorLine: String, + val title: MessageRef, + val initiatorLine: MessageRef, val callerKind: String, val callerPackage: String?, - val sourceLine: String?, + val sourceLine: MessageRef?, val toolDisplayName: String, val argumentsPreview: String, val argumentsTruncated: Boolean, val risk: String, val severity: ConsentSeverity, - val riskLabel: String, - val riskDescription: String, + val riskLabel: MessageRef, + val riskDescription: MessageRef, val options: List