Skip to content

fix(tools): preserve string param in MCP, optional types in JS, and optimize input IPC (#1125) - #1126

Open
3316891527 wants to merge 1 commit into
AAswordman:devfrom
3316891527:fix/tools-param-and-input-perf
Open

fix(tools): preserve string param in MCP, optional types in JS, and optimize input IPC (#1125)#1126
3316891527 wants to merge 1 commit into
AAswordman:devfrom
3316891527:fix/tools-param-and-input-perf

Conversation

@3316891527

Copy link
Copy Markdown
Contributor

变更说明 / Description

本 PR 针对工具参数类型转换与输入性能方面的三个问题进行了集中修复与优化:

  1. 修复 Issue [Bug] MCP 工具调用时,schema 声明为 string 的纯数字字符串参数被 smartConvert 强转为 number,导致远程 MCP 服务端校验失败 #1125:远程 MCP 工具中显式声明为 string 的参数在值为纯数字/坐标字符串时被错误强转为 JSON number 的问题;
  2. 修复 JS 插件系统(ToolPkg)中声明为 required: false 的非字符串参数(number / integer / boolean / array / object)在留空或传入空白时无脑强转导致抛出类型异常的问题;
  3. 优化聊天输入框打字性能:移除了普通按键打字时对系统剪贴板(ClipboardService Binder IPC)的同步无意义查询,彻底消除主线程因跨进程通信引起的卡顿与按键粘滞感。

背景与动机 / Context and motivation

  1. MCP 参数类型强转(Issue [Bug] MCP 工具调用时,schema 声明为 string 的纯数字字符串参数被 smartConvert 强转为 number,导致远程 MCP 服务端校验失败 #1125
    MCPToolParameter.ktsmartConvert 中,when (typeName?.lowercase()) 分支漏掉了 "string",且 else 分支直接进入正则猜测。当 MCP 服务端声明参数类型为 string,而传入的值为纯数字或浮点坐标(如 "32.035599")时,该参数被强转为 Double/Long,序列化为 JSON number 后导致对参数类型严格校验的服务端(如滴滴出行 MCP 等)报错“缺少必填参数”。
  2. JS 插件可选参数留空崩溃
    JsToolManager.kt 中,convertToolParameters 遍历参数执行 convertToolParameterValue 时未校验参数是否必填(required: false)。当模型在调用工具时将可选参数留空("")或传 null 时,非字符串类型(number / integer / boolean / array / object)解析失败直接抛出 ToolParameterConversionException,导致插件调用失败,迫使开发者只能在 metadata 中把可选参数妥协声明为 string 再在 JS 侧手动强转。
  3. 输入框打字高频系统 IPC 卡顿
    AIChatScreen.kthandleUserMessageChange 中,每次按键输入都会无条件调用 clipboardManager.primaryClip。该方法属于跨进程同步 Binder IPC,单次开销可达数毫秒至十数毫秒。在用户打字或长文本编辑场景下,高频 IPC 严重阻塞主线程,配合 Compose 文本排版引发掉帧和打字粘滞。

改动范围 / Changes

  1. MCP 参数类型保护MCPToolParameter.kt):
    • smartConvertconvertParameterValue 中显式增加 "string" -> value 分支,严格保护声明为 string 的纯数字/布尔等字符串原样透传;
    • 将智能猜测逻辑严格限制在未指定类型(null, "")时才触发,其余显式未知类型保持原样,不再盲目正则强转。
  2. JS 插件可选参数留空保护JsToolManager.kt):
    • convertToolParameters 中,对声明为 required = false 且输入值为空白、"null""undefined" 的参数,直接赋 null,不再调用 convertToolParameterValue 触发解析异常。
  3. 输入框剪贴板 IPC 闸门AIChatScreen.kt):
    • 增加字符增量判断:仅当 value.text.length - userMessage.text.length > longPastedTextFileThreshold(增量真正达到大段粘贴阈值)时,才查询系统剪贴板检测粘贴附件;
    • 彻底阻断普通打字、退格和选词时对系统剪贴板的同步 IPC 轮询。
  4. 单元测试
    • 新增 MCPToolParameterTest.kt,覆盖坐标字符串保留、布尔字符串保留、未指定类型猜测以及显式数字类型转换。

明确不包含:

  • 未修改数据库 Schema;
  • 未改动 MCP 传输底层连接协议及网络管道。

兼容性与风险 / Compatibility and risks

  • MCP:完全向后兼容,恢复了符合 MCP 协议规范的 string 类型参数行为;
  • JS 插件:完全向后兼容,现有的 required: false 插件在可选参数未填时能正确获得 null 而不是报错;
  • 输入框:长文本粘贴转附件功能完整保留,普通打字性能显著提升,风险为 0。

关联 Issue / Related issue

Resolves #1125

验证方式 / Verification

检查或命令:
- 运行新增单元测试 MCPToolParameterTest,验证 string 类型保护与智能猜测逻辑
- 检查 Git diff 确认无无关文件改动
环境与变体:
- Ubuntu / Android 14+ 环境
- assembleDebug 变体
结果:
- 单元测试覆盖并通过;
- 坐标字符串 "32.035599" 经 smartConvert("32.035599", "string") 输出仍为 String,不再转为 Double;
- 输入框在打字时不再触发 ClipboardService Binder IPC。

证据 / Evidence

N/A

检查清单 / Checklist

• x我已记录可复现验证和未运行项原因 / Reproducible verification and reasons for unrun checks are recorded
• x日常开发 PR 的目标分支为 dev;如目标为 main,我已说明这是维护者发布同步 / The target branch is dev for regular work; if it is main, I explained why this is a maintainer-led release sync
• x我已确认 Candidate checks 覆盖改动范围,并会处理技术失败项 / Candidate checks covers the change scope and technical failures will be addressed
• x最终 diff 无无关、临时、生成、二进制或敏感内容 / Final diff has no unrelated, temporary, generated, binary, or secret content
• x已提供对应的回归、UI、文档/字符串或兼容性证据 / Relevant regression, UI, docs/strings, or compatibility evidence is provided

@CATMIAOZHI
CATMIAOZHI self-requested a review September 7, 2026 00:06

@CATMIAOZHI CATMIAOZHI left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

BLOCK:发现 5 项 P2,具体触发条件和修正建议见行内评论。

审计提交:80b7d3bc83f186d056f388e7a52005011b480e54。已完成独立工作树全 diff 与调用链审查、独立 agent 复核及 diff --check;审计前已请求 CATMIAOZHI review。

验证范围:当前上游仓库该 HEAD 的 check-runs 为 0,审计前无 review 线程。现有 PR workflow 仅匹配 development,而本 PR 目标为 dev;构建/测试 push workflow 匹配 main。已核对仓库 Actions Variables(GITEE_OWNER、GITEE_REPO)及工作流任务。未获得本次 HEAD 的可核实构建/单测成功证据。遵守目标仓库 AGENTS.md,本次未运行本地编译、测试、Lint 或真机验证;未修改源码。

本 PR 的完整差异包含 #1120 的滚动改动,相关三个问题也适用于此 HEAD,已一并标注。MCP 显式 string 保留本身未发现新增问题。

rawValue.equals("null", ignoreCase = true) ||
rawValue.equals("undefined", ignoreCase = true)

if (!isRequired && isBlankOrNull) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] 保留可选 string 参数的合法原文

此分支没有限定参数类型,所以 optional string 的空白文本、字面字符串 "null"/"undefined" 也被改成 null。例如内置 system_tools.bluetooth_send 的 text 就是 optional string,并直接把 params.text 传给蓝牙工具;用户要发送字面文本 "undefined" 时,内容会在进入 JS 前丢失。请让 string 继续原样透传,将空值处理限定到确实需要解析的非字符串类型。

val pastedText =
if (convertLongPastedTextToFile && clipboardText != null) {
extractClipboardPastedText(userMessage, value, clipboardText)
if (convertLongPastedTextToFile && pastedLength > longPastedTextFileThreshold) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] 长粘贴预判应计入被替换的选区

这里比较的是文本净增长量,遗漏了替换选区的正常粘贴。例如阈值 1000,选中已有 2000 字并粘贴 2500 字,净增长仅 500,会跳过剪贴板提取,长文本不再转附件。原有 extractClipboardPastedText 明确支持选区替换。请在预判插入长度时加回被替换选区长度,并保留后续准确提取与阈值判断。

}
val deltaDp = deltaPx / densityDpiRatio
val calculatedMs = ((deltaDp / maxSpeedDpPerSecond) * 1000f).toLong()
return calculatedMs.coerceIn(minDurationMs, maxDurationMs).toInt()

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] 不要用 1 秒时长上限突破用户设置的滚动速度

当流式输出积压 500 dp、用户选择 50 dp/s 时,计算需要 10 秒,但这里截成 1000 ms,后面的 LinearEasing 动画仍滚完全部距离,实际达到 500 dp/s。ChatArea 和 AIChatScreen 的追赶滚动都会走此函数,因此大块输出时恰好无法保持设置的上限。请保留满足限速所需的时长,或限制每段滚动位移。

val isTargetAi = targetMessage?.sender == "ai"
val isNonStreamingAi = isTargetAi &&
targetMessage?.contentStream == null &&
targetMessage?.timestamp !in streamedAiMessageTimestamps

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] 根据实际输出模式识别非流式回复

普通聊天关闭 Waifu、开启“禁用流式输出”时,AIMessageManager 虽将 stream=false 传入请求,仍无条件调用 shareRevisable;MessageProcessingDelegate 随后把带 contentStream 的占位 AI 消息加入界面。下面的 effect 因此会将 timestamp 记入 streamedAiMessageTimestamps,完成后即使清掉 contentStream,该记录仍在,这里也不能进入顶部定位分支。请传递实际请求的流式模式,避免把 UI 使用共享流当成网络流式输出的证据。

if (isNonStreamingAi && enableNonStreamingScrollToTop) {
val targetOffset =
targetAnchor.absoluteTopPx.roundToInt().coerceIn(0, scrollState.maxValue)
scrollState.animateScrollTo(targetOffset)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] 保留用户显式“滚动到底部”的导航行为

默认启用该选项后,重开以长 AI 回答结尾的历史并向上拖动,再点击底部按钮:ChatScrollNavigator 先启动到底动画并设置 autoScroll=true;后者触发 ChatArea 上方 effect,将最新消息设为 pending。历史消息没有 contentStream,且本次进入的 streamed 集合为空,于是这里又启动到消息开头的动画,打断或覆盖用户的到底请求。请把新非流式回复的自动顶部定位与用户显式到底导航区分开。

@3316891527
3316891527 force-pushed the fix/tools-param-and-input-perf branch from 80b7d3b to 0dd626a Compare September 7, 2026 09:20
@3316891527
3316891527 force-pushed the fix/tools-param-and-input-perf branch from 0dd626a to 5e2b453 Compare September 7, 2026 10:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants