# 网络请求与数据流向 这份文档说明**本仓库的代码**在什么条件下发出什么网络请求、内容是什么、去哪里、默认是否开启,以及在哪一行代码里。 它不是隐私政策。产品的隐私政策在 ,设置页「关于」里也有入口(`packages/ui/src/settings/pages/about-page.tsx`),各平台都打开它。两者冲突时以隐私政策为准。这里提供的是可以对着代码核对的技术细节,给审阅者和贡献者用。 输入法是能看到你输入的一切的软件,所以这个问题值得一个能逐条验证的答案,而不是一句「我们重视你的隐私」。 ## 一句话结论 新装的默认配置下,**没有任何功能会把你输入的内容发出设备**。会这样做的两项都要你自己打开:云联想把当前正在组的拼音串发给 Google 输入工具,新装默认关闭,Windows、Linux 与 macOS 在第一次使用时说明并让你选择,默认选项是不启用(见[云联想](#云联想新装默认关闭));候选翻译要你在设置里选择一个翻译服务(自己的凭据,或显式选择「水杉账号」,后者把当前页的中文候选词发到 `api.msime.app`,见[候选翻译](#候选翻译默认不联网))。其余所有联网功能——语音识别、语音润色、AI 联想、账号同步——默认凭据为空,你不填自己的密钥它们就不会发出任何请求。从旧版本升级时沿用配置文件里已存的值,不会替你改。 另有一条不携带输入内容的自有上报路径,端点是本项目自己的 `https://api.msime.app`:六个平台都会发送匿名使用统计(每天一条活跃记录、每个输入法进程一条会话记录、崩溃时的错误摘要与调用栈),**默认开启**,可以在设置里用「匿名使用统计」(偏好字段 `usage_reporting`)关闭,关闭后不再发送并清空本机队列,见[使用统计与崩溃上报](#使用统计与崩溃上报默认开启可关闭)。设置窗口或 App 首页打开时会拉取一次不带凭据的[服务公告](#服务公告);社区里只有你主动提交时才会发出[举报](#社区举报与审核)。六个平台还会在安装后首次启动时向同一端点注册一个本机匿名水杉账号,只发送本机随机生成的标识与口令,见[账号与同步](#账号与同步需要登录)。仓库不接入任何第三方统计或崩溃上报 SDK。 ## 逐项说明 ### 云联想(新装默认关闭) | | | | --- | --- | | 偏好字段 | `cloud_candidates`,新装与「恢复默认设置」为 `false`;配置文件里缺这个字段时按 `true` 读,升级沿用原来的行为 | | 目的地 | `https://inputtools.google.com/request` | | 发送内容 | 当前正在组的拼音串,作为 `text` 查询参数 | | 需要凭据 | 否 | | 代码 | `crates/client-core/src/cloud/candidates.rs` 的 `build_google_url`,经 `crates/input-runtime/src/providers.rs` 调用 | URL 的完整形态可以在同文件的测试里看到,是断言过的字面量: ``` https://inputtools.google.com/request?text=ni%20hao&itc=zh-t-i0-pinyin&num=1&ie=utf-8&oe=utf-8 ``` 日文输入方案走 `ja-t-i0-und`,其余相同。这是仓库里唯一的云候选来源,没有第二个提供方。 发出前有几道硬性限制,定义在共享层(`candidates.rs` 顶部的常量):输入为空、超过 256 字节、或含控制字符时不发;响应超过 256 KiB 或单条候选超过 512 字节即丢弃。请求按代次绑定,失焦和换代次会使迟到的结果作废,不会写进新会话。共享层的云候选代码路径不含任何日志调用,查询和响应不经由它落盘。 防抖间隔**由各宿主自己决定**,不是共享常量:共享层的 `spawn_with_debounce` 接受调用方给的时长,`spawn` 默认为零。已在代码中固定取值的是 Android(`OnlineCandidatePolicy.QUIET_INTERVAL_MILLIS = 350`)和 HarmonyOS(`OnlineCandidatePolicy.QUIET_INTERVAL_MS = 350`),两者都是组字停顿 350 ms 后才发一次,并用请求身份保证同一组合只问一次。其余宿主的取值请以各自代码为准。 **默认关闭,桌面平台首次使用时会问**:新装时 `cloud_candidates` 是 `false`(`crates/client-core/src/preferences.rs` 的 `Default`),六个平台都一样。三个桌面平台在第一次使用时先说明这项功能再让你选,默认选项都是不启用:Windows 安装器在全新安装时显示「联网功能」页(`platforms/windows/installer/msime_setup.iss`),勾选框默认不勾,勾上时把选择记在数据目录的 `installer-choices.json`,Server 首次启动准备状态时写进共享偏好(`platforms/windows/src/system/FirstRun.h`),之后删掉这个文件;Linux 的首次配置页(`packages/ui/src/account/linux-setup-page.tsx`)在同一处说明并提供同样默认不勾的选择;macOS 在全新配置下由输入法第一次激活时弹出「联网功能」对话框(`platforms/macos/src/settings/AppearancePreferences.mm` 的 `MSIMEClientCloudCandidatesConsent`),回车对应「不启用」,回答之前不发任何云候选请求。Android、HarmonyOS 与 iOS 不弹询问,在设置里打开(iOS 另有自己的开关,见 `platforms/ios/SharedUI/candidate/CloudCandidatePreference.swift`)。升级都不问,沿用配置文件里已存的值。 **打开或关掉它**:设置页「云联想」开关,或把配置里的 `cloud_candidates` 设为 `true` / `false`。关闭后宿主不再发起云候选请求,并拒绝任何返回的云来源候选。 这与常见中文输入法的云输入功能是同一类能力。它会把你正在输入的内容交给第三方,所以由你决定是否打开;既然仓库公开,端点和发送内容也应当白纸黑字写在这里,而不是让人去读源码才能知道。 ### 候选翻译(默认不联网) `candidate_translations` 默认 `true`,支持腾讯机器翻译(`https://tmt.tencentcloudapi.com`)、小牛翻译(`https://api.niutrans.com/v2/text/translate`)和自定义端点。三者都要求你在设置里填入自己的 API 凭据,默认全为空字符串——**没有凭据就不会发出请求**,开关为真也一样。发送内容是待翻译的候选词。代码在 `crates/client-core/src/credential/translation.rs`。自定义端点的主机名恰好是 `api.deepl.com` 或 `api-free.deepl.com`(https、默认端口)时,按 DeepL v2 格式带上你填的密钥发给 DeepL:密钥以 `:fx` 结尾的发往 `https://api-free.deepl.com/v2/translate`,其余发往 `https://api.deepl.com/v2/translate`,与你填的是两个 DeepL 主机中的哪一个无关;这种地址没有密钥时不发。其他自定义端点原样使用你填的地址。请求由 `crates/client-core/src/translation.rs` 的 `custom_translation_request` 构造,Linux provider(`platforms/linux/scripts/msime-linux-online-provider`)照写的一份由 `shared/contracts/custom-translation/cases.json` 的共享用例约束。 Android 也支持这三种服务(含按主机名识别的 DeepL),在「设置 → 输入 → 翻译 → 翻译服务」里选择和填写凭据。选了它们之后,键盘把当前一页的中文候选词直接发给你配置的第三方服务翻译,不经过水杉服务器,候选词按该服务自己的数据政策处理:每页最多 10 个词,每个不超过 40 字,跳过表情和颜文字候选;腾讯每批最多 9 个词,小牛和自定义每个词单独请求一次;设置了第二目标语言时,每种语言各请求一次。离线释义查得到的语言仍然先用离线释义,但请求不因此省掉。自定义端点在 Android 上只接受 https,或指向本机(`localhost`、`127.0.0.1`、`[::1]`)的 http,局域网 http 不接受。同一设置下已经回答过的词(包括回答「没有译文」的)不会重复请求;失败时同一个词 30 秒内不再请求,整个服务连不上时 30 秒内不再发任何请求。凭据只保存在本机的设置文件里,不随设置同步上传,也不写进日志。代码在 `platforms/android/java/app/msime/android/candidate/UserTranslationClient.java` 和 `TranslationHttpTransport.java`。 macOS、iOS、Android 和 Linux 另提供「水杉账号」(`translation_account`):选择它后,会把当前页的中文候选词(包括本地已有释义的)连同目标语言代码 POST 到 `https://api.msime.app/v1/translate`。请求带账号令牌:macOS 与 Android 在你已登录时用登录的账号,否则(以及 iOS 上始终)用安装后首次启动时注册的本机匿名账号(见[账号与同步](#账号与同步需要登录))。所有平台新装和「恢复默认设置」都不选它,只有你显式选择才发送;已有配置文件里存着选择的,升级沿用,缺这个字段的按未选择处理。不想发送,在设置的翻译服务里改选别的服务或「不使用在线翻译」,或关掉候选翻译。你自己的服务优先:候选翻译关闭、小牛或自定义服务已启用、或腾讯已启用且两项凭据都可用时,都不走水杉账号。共享层把这个判定算成翻译查询里的 `translation_account` 字段(`crates/host-api/src/ffi/providers.rs`),各宿主只在它为真时发请求。Windows、HarmonyOS 没有这条路径。没有选择任何服务时不发出候选翻译请求。 macOS 26 及以上在没有选择任何服务时(候选翻译开启,小牛、自定义、腾讯都未启用,也没有选择水杉账号,新装就是这个状态),会用 Apple 系统自带的离线翻译模型为随包词典答不上的中文候选补一行释义。翻译在本机完成,候选词不离开这台 Mac;只用你已经在「系统设置 → 通用 → 语言与地区 → 翻译语言」里下载好的语言对,输入法不会触发下载,没下载就不补。代码在 `platforms/macos/src/backend/translation/BackendOnDeviceGloss.swift`,判定在 `InputController.mm` 的 `currentOnDeviceGlossRequest`。 ### 语音输入(默认凭据为空) `voice_input.enabled` 默认 `true`,但这只表示功能可用,录音要你主动触发。默认识别服务是豆包(`wss://openspeech.bytedance.com/...`),**`asr_token` 默认为空字符串**,不填就无法使用。可选的识别服务还有 SiliconFlow、OpenAI、Groq、EveryAPI、Mistral Voxtral、阿里云百炼(`bailian`,默认 `https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions`,模型 `qwen3-asr-flash`,录音以 Base64 放进 Chat Completions 请求),以及两种不出设备的选项: - `local`:设备上的识别模型,`asr_model_path` 是一个绝对路径,指向设置页下载的 sherpa-onnx 模型目录(包含 `msime-model.json`)。不需要 Token,也没有端点。 - `system`:调用操作系统自带的识别(macOS / iOS / HarmonyOS),数据流向由操作系统决定。macOS 与 iOS 26 起使用 SpeechAnalyzer(设备端);更早的系统在识别器支持时设置 `requiresOnDeviceRecognition`,不支持时由系统决定是否上传。 选择云端服务时,发送的是录制的音频。代码在 `crates/client-core/src/credential/asr.rs`,服务选择逻辑在 `crates/client-core/src/preferences.rs`。 **本地模型**全程在设备上运行:录音、识别结果和热词都不离开设备,识别期间不发出任何网络请求。macOS 与 Linux 的输入法进程不自己加载模型,而是拉起本机的 `msime-voice-local` 辅助进程,经标准输入输出交换音频和文本(协议见 `shared/voice/README.md`),不经过网络套接字;Windows 在本机的 `msime-client-server` 进程内识别,Android、iOS 与 HarmonyOS 在应用进程内识别。热词取自你的个人词库(只取用户自己添加的拼音词条),在本机交给识别器,或在识别后于本机做近音替换(`crates/client-core/src/voice/hotwords.rs`)。 唯一的联网发生在**下载模型**时,且只在你在设置页点「下载」后发生。用「从文件导入」安装时完全不联网:选中的本地文件只在本机按长度和 SHA-256 校验后解压进模型目录;设置页里每个文件的下载链接用系统浏览器打开,输入法本身不发请求(Android 的 full 与拼音版在打开「离线识别」而本机还没有语音运行库时会下载它,见[资源与更新下载](#资源与更新下载)): | | | | --- | --- | | 目的地 | GitHub Releases(`https://github.com/k2-fsa/sherpa-onnx/releases/download/...`,下载时会被重定向到 GitHub 的文件存储域名);配置了镜像时改为镜像地址 | | 发送内容 | 对模型归档的 HTTPS GET 请求,User-Agent 为 `msime/<版本号>`,不携带任何输入内容、音频、账号或设备标识 | | 需要凭据 | 否 | | 偏好字段 | `voice_input.asr_model_mirror`,默认空字符串,表示直接访问 GitHub | | 代码 | `crates/client-core/src/voice/local_models.rs`;地址、长度和 SHA-256 固定在 `resources/local-asr-models.json` | 镜像必须是 `https://` 地址,请求 URL 是镜像前缀加上原始 GitHub 地址,所以**镜像运营方能看到你的 IP 和你下载的是哪个模型**。下载内容按目录里固定的 SHA-256 校验,镜像无法替换文件;校验不过的下载会被丢弃。重定向离开 HTTPS 时请求被拒绝。模型装好之后,识别不再需要网络。 ### 语音润色(不填 Token 不发生) 开启后把识别出的文本发给一个 Chat Completions 服务做整理。默认值按平台不同:Windows 与 macOS 跟随来源模板,默认服务是 DeepSeek(`https://api.deepseek.com/chat/completions`,模型 `deepseek-v4-flash`),`voice_input.polish_text` 默认开启;其余宿主默认服务是 SiliconFlow,`polish_text` 默认关闭。`voice_input.polish_enabled` 在所有宿主上默认 `false`。无论哪个平台,`polish_token` 默认都为空,宿主在 Token 为空时不发起润色请求,所以默认情况下不会发送任何文本。 ### AI 联想(自带端点与密钥) 配置完整才会发起请求,发送内容是当前查询。默认值按平台不同:Windows 与 macOS 跟随来源模板,`ai_assistant.enabled` 默认开启,并预填 DeepSeek 的接口(`https://api.deepseek.com/chat/completions`)与模型(`deepseek-v4-flash`);Token 默认为空,`chat_completion_http_request`(`crates/client-core/src/ai.rs`)在没有可用密钥时拒绝构造请求,因此不会发出任何请求。其余宿主默认关闭,接口为空,需要显式启用并自己填写。端点可以改成任意服务,代码对它做校验:必须是 http/https、不得内嵌用户名密码、长度不超过 2048、不含控制字符(`apps/desktop/src-tauri/src/ai.rs`)。常见的 DeepSeek、OpenAI、Groq、SiliconFlow、OpenRouter、EveryAPI 只是文档和测试里出现的示例,仓库不预置任何一家的密钥。 ### 账号与同步(需要登录) `https://api.msime.app`,定义在 `crates/client-core/src/account.rs` 的 `ACCOUNT_ORIGIN`。普通账号不登录不发生。 匿名账号在六个平台上都是安装后自动注册的:本机随机生成一个标识(`msime-` 加 16 位)和一个 48 位口令,先保存在本机,再向 `/v1/auth/challenges` 与 `/v1/auth/login` 换取令牌。只发送这两个随机值,不含输入内容、设备信息或系统账号。网络失败不影响输入,下次启动重试;已有登录或匿名会话时只读本机文件,不再发请求。各平台的时机: - Linux:Debian `postinst` 为可联系的登录用户注册;手工安装或当时网络不可用时,`msime-linux-setup` 会在首次配置时重试。身份和令牌由 `msime-linux-online-provider` 保存在用户配置目录的 `anonymous-account.json` 与 `anonymous-session.json`,两个文件均为当前用户专用权限,不进入设置页或输入法进程。 - Windows:Server 首次以 `--production` 启动时(安装程序完成页会拉起它),按 Windows 用户分别保存在 `%LOCALAPPDATA%\MSIME\account`,不放进全机共用、所有用户都可写的数据目录。 - macOS:输入法首次激活时(`MSIMEEnsureAnonymousAccount`);iOS:应用首次启动时,已存有登录或匿名会话(即使已过期)就不发请求。两者都存于 App Group 容器,键盘与应用共用。 - Android:应用首次打开时(`AccountIdentity.register`),已存有匿名会话(即使已过期)就不发请求,存于应用私有存储。 - HarmonyOS:应用首次启动或键盘首次加载时,存于应用的 `files/state` 目录。 Windows 与 HarmonyOS 除了你主动提交的[社区举报](#社区举报与审核)之外,不用这个账号发送任何内容。候选翻译在你选择了「水杉账号」时才发送,见[候选翻译](#候选翻译默认不联网);Android 浏览社区皮肤与词库目录时,也会带上匿名账号的令牌(`platforms/android/java/app/msime/android/community/CommunityCatalog.java`),取不到照常列出目录。凭据的存放:iOS 用 Keychain(`crates/tauri-mobile-platform/ios/Sources/MobilePlatformPlugin.swift`),Android 用 Keystore 加密后落盘;macOS、Windows 与 Linux 桌面端存在当前用户私有的 `account-session.json` 里(`crates/client-core/src/account/file_storage.rs`),只有本人可读写,不加密。 ### 云剪贴板(需要登录或自建中继) 用来在手机和电脑之间传文字:一台设备上把文字放进云剪贴板,另一台设备上从云剪贴板里点选。条目存在 `https://api.msime.app` 你的账号下(改走自建中继时存在你自己的中继上,见下文),最多 50 条,单条不超过 4000 个 UTF-16 单元。 - **端到端加密**:文字在你的设备上加密后才上传,只在你的设备上解密,服务端只保存密文,读不到内容,也不能替你搜索(搜索在设备上进行)。不需要设置口令:每台设备第一次使用时自己生成一对密钥,私钥只留在这台设备上;账号的剪贴板密钥由第一台开启云剪贴板的设备生成,服务端只保存分别用每台设备的公钥封装过的副本。协议公开在 [`docs/cloud-clipboard-e2e.md`](docs/cloud-clipboard-e2e.md),实现在 `crates/client-core/src/cloud/clipboard_e2e.rs`。 - **新设备要由已有设备批准**:在新设备上登录后,要在一台已经能用云剪贴板的设备上点「允许」,它才拿得到密钥;两台设备上会显示同一组 16 位核对码,核对一致再允许。服务端因此没办法悄悄加一台设备来读你的剪贴板。移除设备或「重置云剪贴板」会换一把新密钥并清空云端条目,其余设备都要重新批准。所有设备都丢了时,在新设备上重置即可,旧条目无法找回。 - **服务端仍然看得到的**:条目数量、写入时间、是否置顶、写入设备的名称、两条内容是否相同(用于去重),以及每台设备的名称、公钥和登记时间。 - **本机密钥放在哪里**:与账号凭据放在一起——iOS 钥匙串、Android 用 Keystore 加密后落盘、HarmonyOS 应用私有目录、macOS/Windows/Linux 当前用户私有的 `cloud-clipboard-keystore` 文件;退出登录时与凭据一起删除。同一台设备上的输入法和设置应用共用这一份,所以在服务端只算一台设备。 - **改走自建中继**:Linux 设置页「剪贴板」和 Android「我的 → 云剪贴板」页的「云剪贴板同步方式」可以选「自建中继」,填中继地址和管理员发的令牌(偏好字段 `cloud_clipboard_relay`)。选了以后,云剪贴板的请求只发往你填的地址、带中继令牌,不再经过 `api.msime.app`,也不需要登录水杉账号;加密和设备批准都和上面一样,上面说的元数据留在你的中继上(中继不记录写入设备的名称)。地址只接受 `https://`(`http://` 只用于本机回环地址),证书必须是公网可信的。令牌和语音、翻译服务的凭据一样明文存在本机的设置文件里,不随设置文件导出、不随账号同步,诊断快照里换成 ``。本机密钥另存为当前用户私有的 `cloud-clipboard-relay-keystore`(桌面端与账号会话同一目录;Android 在应用私有的 `no_backup/cloud-clipboard-relay/` 下,和账号那份一样用 AndroidKeyStore 里不能导出的密钥加密,不进云备份,也不随设备间迁移带走),退出水杉账号不删除它,换了中继地址或令牌后不再使用旧的那份。Android 上配置了中继时,设置页和键盘的「云端」都只连中继,账号页不再为显示条数去问水杉账号;还不能经中继收发的页面(Android、iOS 的 Tauri 设置页和云剪贴板面板)直接拒绝,不改走水杉账号。协议和参考实现见 [`docs/cloud-clipboard-relay.md`](docs/cloud-clipboard-relay.md),客户端在 `crates/client-core/src/clipboard_relay.rs`。 - **只上传你明确选择的文字**:在云剪贴板面板里输入或粘贴后点上传,或者在本地剪贴板历史的某一条上点「发到云剪贴板」。复制时不会自动上传到云端。(Android 在你开启本机剪贴板历史后会自动把复制的文字记在本机,见下文「默认值」,这些记录同样不会上传;你在键盘里把某一条「添加到常用语」后,它就成了一条常用语,开着常用语同步时会随常用语一起同步。) - **只在打开面板时读取**:设置页、键盘里的「云端」栏和 macOS 输入法菜单里的「云剪贴板…」在打开和点刷新时各拉取一次列表,没有轮询或推送。 - **密码框里不出现**:Android、iOS、HarmonyOS 键盘和 Linux Fcitx5 在密码类输入框里不显示云端条目,也不发请求;macOS 在安全输入期间不打开云剪贴板面板。 - **关闭即删除**:把云剪贴板关掉会删除云端全部条目、剪贴板密钥的封装和设备登记,共享设置页在关闭前会先请你确认。 - **键盘怎么拿到账号**:键盘和设置应用是不同的进程,账号令牌不会被复制成明文文件。Android 键盘向主进程里一个不导出的 ContentProvider 取令牌,只有主进程会刷新令牌;iOS 的会话存放在 App 与键盘扩展共有的 App Group 钥匙串访问组里,键盘需要「允许完全访问」才会联网;HarmonyOS 键盘读取设置应用在应用私有目录里保存的同一份会话;macOS 的输入法与设置应用读写同一个 `~/Library/Application Support/app.msime.macos/account-session.json`。iOS、HarmonyOS 与 macOS 刷新令牌时持有跨进程文件锁,防止两个进程用同一个刷新令牌而让会话被服务端吊销。 - **从电脑上屏**:Windows 与 Linux 的设置窗口里,云剪贴板面板只有在这次打开时记下了另一个应用的输入窗口才能直接上屏,否则只能复制;它不会沿用以前记下的窗口。 ### 资源与更新下载 首次准备词库时从 GitHub Releases 拉取固定版本的资源,地址、长度和 SHA-256 全部写死在 `resources/desktop-dictionary.lock.json` 里,逐一校验,全部成功才发布到内容标识目录。下载的是公开发布物,不上传任何东西。检查更新只在点击「检查更新」时进行,由应用本身(不经网页)向 `https://api.github.com/repos/metasequoiaime/msime/releases` 发起 GET 请求,在本地按平台标签前缀、版本和架构筛选;请求除 IP 地址和固定的 User-Agent `msime-client` 外不携带标识,适用 GitHub 隐私条款。 桌面和 Android 发布包不内置的几个资源包由设置应用在首次用到时下载,之后从本机读取。哪些平台下载哪些: - 日文词典、粤拼注音与笔画词库:macOS 和 Android full 版下载,它们的发布包只内置打中文所需的核心词库;Android 拼音版只下载日文词典(给临时日语用),它不提供粤拼、注音和笔画方案,所以不下载语言词库;Windows 和 Linux 的安装包带着它们,Android 日文版的 APK 带着日文词典。 - 非英文离线释义(法、日、西、俄、德、韩)与本地语音运行库(sherpa-onnx 与 ONNX Runtime 两个原生库):只有 Android 的 full 与拼音版下载,其他平台和 Android 的其他版本照旧随包。 - 手写模型:macOS 下载;Windows 只在系统没有中文 Windows Ink 手写识别器、安装目录里也没有模型时下载;Linux 只在安装前缀里没有随包模型时下载(我们自己发布的 deb/rpm 不带它,各发行版仓库的包继续内置)。 - 桌面神经联想模型(约 25 MB):三个桌面平台都只在打开「桌面神经联想」时下载,安装布局里已经带着它时不下载。 | | | | --- | --- | | 触发 | 桌面上只在这几种情况下发生:在设置里选日文、粤拼、注音或笔画方案(macOS);第一次打开手写面板(宿主需要下载手写模型时);打开「桌面神经联想」;设置应用启动时发现已保存的方案(或上一次的中文方案)需要的词库、或已打开的桌面神经联想需要的模型还没装;在资源包那一行点「下载」或「重试」。Android 上只在你在「添加语言」里添加日语、粤语、注音或笔画,打开离线释义,打开「离线识别」(本机还没有语音运行库时),或在资源包那一行点「下载」「重试」时发生,应用启动时从不自动下载;使用按流量计费的网络时先告诉你下载大小、等你确认。从内置这些文件的旧版 Android 升级时,已经解压在本机的文件按固定的长度和 SHA-256 校验后直接转成资源包,不联网 | | 目的地 | GitHub Releases(`https://github.com/metasequoiaime/msime-dictionary/releases/download/dict-v.../` 的词库,`https://github.com/metasequoiaime/chinese-ime-lm/releases/download/model-v1/` 的桌面神经联想模型,`https://github.com/metasequoiaime/chinese-ime-lm/releases/download/offline-glosses-.../` 的离线释义,`https://github.com/k2-fsa/sherpa-onnx/releases/download/v.../` 的 Android 语音运行库归档,下载时会被重定向到 GitHub 的文件存储域名)与 `https://raw.githubusercontent.com/metasequoiaime/msime-engine/<固定提交>/...`(手写模型);配置了镜像时先从镜像地址下载,镜像失败再回到上面的原地址 | | 发送内容 | 对固定文件的 HTTPS GET 请求,User-Agent 为 `msime/<版本号>`;中断后接着下载时带 `Range` 头,只说明从第几个字节开始。不携带任何输入内容、账号或设备标识 | | 需要凭据 | 否 | | 偏好字段 | 沿用 `voice_input.asr_model_mirror`,默认空字符串,表示直接访问 GitHub;下载失败时资源包那一行和手写面板都提供「设置下载镜像」 | | 存放位置 | 偏好目录下的 `resource-packs/<资源包>/`:macOS 是 `~/Library/Application Support/app.msime.macos/resource-packs/`,Windows 是输入法服务的数据目录,Linux 是运行时选项记录的状态目录,Android 是应用私有目录 `files/bootstrap/state/resource-packs/`(没下完的部分留在同一目录下的 `.partial-<资源包>/`,下次接着下载);许可证文本放在数据旁边 | | 代码 | `crates/client-core/src/resource_packs.rs`、`apps/desktop/src-tauri/src/platform/desktop/desktop_resource_packs.rs`,Android 经 `crates/host-api/src/ffi/pack_downloads.rs` 调用同一份实现;地址、长度和 SHA-256 固定在 `resources/desktop-dictionary.lock.json`、`resources/language-dictionaries.lock.json`、`resources/offline-glosses.lock.json`、`resources/voice-runtime.lock.json` 与 `resources/voice-runtime-android.lock.json`(语音运行库归档和从中取出的两个库)、`resources/handwriting-model.lock.json` 和 `resources/settled-model.lock.json` | 镜像规则和下面本地语音模型的相同:必须是 `https://` 地址,镜像运营方能看到你的 IP 和你下载的是哪个资源包,但下载内容按固定的 SHA-256 校验,镜像无法替换文件,校验不过就换下一个来源。不需要这些功能就不会发生这些请求;缺少资源包时对应的方案或手写面板显示为不可用,桌面神经联想保持现有候选;Android 上缺少的语言不出现在键盘里,离线释义只有英文,本地语音识别不可用,中文输入不受影响。 Android 的手写识别使用 ML Kit,**首次使用需要联网下载识别模型**,之后在设备上离线识别。Windows 先用系统的 Windows Ink 识别器,没有中文识别器时用 Engine 的离线 Zinnia 模型;Linux 与其余桌面端都用这个 Zinnia 模型。发行版仓库的 Linux 包随附该模型,从安装路径读取,全程不联网;macOS、Windows 和我们自己发布的 Linux deb/rpm 按上一段在需要时下载,之后同样离线识别。 ### 使用统计与崩溃上报(默认开启,可关闭) 六个平台都通过同一份共享实现向 `https://api.msime.app/v1/telemetry/events` POST 事件:`crates/client-core/src/telemetry.rs`(队列、安装 id、事件规则、发送与重试),原生宿主经 `crates/host-api` 的 `msime_client_telemetry_{begin,end,record_crash,flush,clear}`(`crates/host-api/src/ffi/reporting.rs`,声明在 `crates/host-api/include/msime_client.h`)调用它。**事件里没有任何输入内容、候选、剪贴板、账号标识或硬件信息**。 | | | | --- | --- | | 目的地 | `https://api.msime.app/v1/telemetry/events`,不带任何凭据(服务端和任何请求一样能看到来源 IP) | | 偏好开关 | `usage_reporting`,默认 `true`。设置页「关于 → 隐私 → 匿名使用统计」在所有平台都显示(`packages/ui/src/settings/telemetry-section.tsx`);Windows 原生设置在「关于」里另有同一开关(`platforms/windows/settings/main.cpp`),iOS 在「关于水杉 → 发送匿名使用统计」,Android 在「词库与输入 → 需要留意的 → 匿名使用统计」。旧字段 `telemetry_enabled` 不再读取 | | 每条事件的字段 | 随机事件 id、`kind`、`platform`(`windows`/`macos`/`linux`/`android`/`ios`/`harmony`)、`version`(应用的真实版本号)、`install_id`;只有 `crash` 另带 `message` 与 `stack` | | `install_id` | 每个安装首次使用时随机生成一次,存在本机的 `telemetry-state.json` 里,不由硬件、账号或任何用户数据派生 | | 需要凭据 | 否 | 事件只有四种,客户端不再发送 `download`(GitHub Release 的下载数由服务端统计,装机数由 `active` 统计): - `active`:每个安装每个 UTC 日最多一条,id 固定为 `active--`,重复发送也只记一次。 - `session`:输入法宿主进程正常结束时记一条(iOS 键盘是每次键盘弹出到收起),在下一次发送时送出。 - `session_crash`:上一个会话没有正常结束**并且**崩溃处理程序为它写下了崩溃记录时才记。只留下会话标记(注销、关机、系统杀进程、内存不足)不算崩溃,不产生任何事件。 - `crash`:崩溃处理程序在崩溃时**只写本地文件、不联网**,下次启动时才转成事件发送。`message` 是异常或信号的摘要(最多 1000 个字符),`stack` 是调用栈帧(最多 16000 个字符、12 KiB,在行边界截断)。共享层在入队前去掉每一行路径里的目录部分,只留文件名,所以用户名和文件夹名不会离开设备。每次启动最多转换 8 条崩溃记录。 发送与保留:事件先写进本机队列再发送,`202` 后移除;`400` 类拒绝直接丢弃不再重试;`429`(遵守 `Retry-After`)、`5xx` 和网络错误保留到下次用同一个 id 重发。队列最多 64 条,超出时丢弃最旧的。关闭开关后什么都不发送,并立即删除队列、会话标记和崩溃记录;`install_id` 文件保留,这样重新打开时不会把同一个安装算成两次。队列里格式无效的事件在读取时丢弃。 各平台的会话边界、崩溃来源和文件位置: - **Windows**(`platforms/windows/src/entrypoints/server_main.cpp`,包装层 `platforms/common/Telemetry.cpp`):会话是 Server 进程从启动到消息循环结束;开关读自偏好里的 `usage_reporting`(`platforms/windows/src/system/TelemetryConsent.h`:缺省算开启,显式 `false` 算关闭,读不出来的值或文件算关闭),设置页保存后立即生效。启动时发送一次,之后每 30 分钟一次。崩溃来源:`std::terminate` 记录异常类型和 `what()` 的第一行(JSON 解析异常只记类型和编号,因为其文本可能引用用户文件的内容);未处理的结构化异常由 `SetUnhandledExceptionFilter` 记录异常名与代码、出错模块文件名+偏移和逐帧的模块+偏移(例如 `EXCEPTION_ACCESS_VIOLATION (0xc0000005) in module.dll+0x…`)。文件在 `%LOCALAPPDATA%\MSIME\` 下:`telemetry.json`、`telemetry-state.json`、`telemetry-session.json`、`telemetry-crashes\`。安装器的「联网功能」页说明了这项统计默认开启(`platforms/windows/installer/msime_setup.iss`)。 - **Linux**:IBus 宿主(`platforms/linux/src/entrypoints/ibus_main.cpp`)的会话是 `msime-linux-ibus` 从启动到主循环返回,崩溃守护以 `--recovered` 重启的进程同样如此;Fcitx5 插件(`platforms/linux/fcitx5/FcitxEngine.cpp`)的会话是插件实例的生存期,发送在后台任务里进行,不占用事件循环。崩溃来源:`std::terminate`(规则同 Windows),以及 SIGSEGV、SIGBUS、SIGILL、SIGFPE、SIGABRT 的信号处理程序,写下 `SIGSEGV: segmentation fault (code N)` 这样的摘要和 `backtrace_symbols_fd` 的栈帧,然后交回原先的处理程序(Fcitx5 自己的崩溃日志照常工作)。启动时发送一次,之后每 30 分钟一次。开关是共享偏好 `preferences.json` 里的 `usage_reporting`。文件在 `$XDG_STATE_HOME/msime/`(未设时为 `~/.local/state/msime/`)下,文件名同 Windows;Fcitx5 用自己的子目录 `$XDG_STATE_HOME/msime/fcitx5/`,因此有自己的 `install_id`,同一台机器上两种框架都用会被算成两个安装。 - **macOS**(`platforms/macos/src/core/UsageReporting.mm`,由 `input_method_main.mm` 调用):会话是输入法进程的生存期,单独打开的设置窗口不算。崩溃来源:`NSSetUncaughtExceptionHandler`(异常名、原因和 `callStackSymbols`)和 SIGSEGV、SIGBUS、SIGILL、SIGFPE、SIGABRT、SIGTRAP 的信号处理程序(`backtrace_symbols_fd`:二进制文件名、地址、符号+偏移)。启动时发送一次,之后每 3 小时一次。开关是共享设置页的「匿名使用统计」。文件在 `~/Library/Application Support/MSIME/telemetry/`;水杉五笔、水杉拼音等其他版本各用 `~/Library/Application Support/MSIME/<版本 id>/telemetry/`,同时安装的版本互不共享 `install_id`。 - **iOS**(`platforms/ios/SharedUI/core/UsageReporting.swift`):会话是键盘的一次弹出到收起(`KeyboardExtension/Sources/core/KeyboardUsageReporting.swift`),App 本身不产生会话。崩溃来源:键盘里的信号处理程序(`CrashSignalRecorder.c`,信号名和 `backtrace_symbols_fd` 的栈帧)和未捕获异常处理程序(异常名、原因和 `callStackSymbols`);App 的崩溃来自系统的 MetricKit 诊断(`CrashDiagnostics.swift`),只取异常类型、代码、信号、ObjC 异常名、系统给出的终止原因和「二进制名+偏移」形式的栈帧,从不取 ObjC 的 `composedMessage`。键盘只有在「允许完全访问」打开时才发送,最多每 15 分钟一次;否则由 App 在回到前台时发送。文件在 App Group 容器的 `MSIME/telemetry/` 下,App 与键盘共用一个 `install_id`。`PrivacyInfo.xcprivacy` 声明了 CrashData、ProductInteraction 与 DeviceID(随机安装 id),均为不关联身份、不用于跟踪。 - **Android**(`platforms/android/java/app/msime/android/core/Telemetry.java`,经 `platforms/android/native/client_jni.cpp` 调用共享层):会话只在 `:ime` 进程里,是 `MSIMEInputService` 从 `onCreate` 到 `onDestroy`。崩溃来源:两个进程的未捕获异常处理程序,记录 `Throwable.toString()` 的第一行(**可能包含异常自带的消息文本**)和 Java 栈帧(类、方法、源文件名与行号,最多 4 层 Caused by);App 进程的崩溃在键盘下次启动时作为单独的 `crash` 发送。原生(信号)崩溃目前不捕获。App 打开时发送,键盘弹出时最多每 6 小时发送一次。文件在应用私有目录 `files/telemetry/`。 - **HarmonyOS**(`platforms/harmony/entry/src/main/ets/telemetry/Telemetry.ets`,经 `platforms/harmony/native/client_napi.cpp` 调用共享层):会话是键盘进程从 `KeyboardExtensionAbility.onCreate` 到 `onDestroy`,设置应用只发送不产生会话。崩溃来源只有系统在下次启动时投递的 HiAppEvent `APP_CRASH`:JS 崩溃取错误名、消息第一行和引擎调用栈;原生崩溃取信号名与代码和「文件名+pc+符号」形式的栈帧,不含故障地址和目录。键盘启动时与之后每 6 小时(打字时检查)发送一次,设置应用打开时也发送。文件在 `files/state/telemetry/` 下,另有只存键盘进程号的 `harmony-keyboard-process.json`。 ### 服务公告 设置窗口或 App 首页打开时,向 `GET https://api.msime.app/v1/notices?channel=app&platform=<平台>` 拉取一次公告列表,不带凭据,也不带安装 id;服务端缓存一分钟,客户端最快一分钟问一次,输入法进程从不在后台拉取。共享实现在 `crates/client-core/src/notices.rs`。拉取的地方: - Windows、macOS、Linux 的共享设置窗口,以及 Tauri 版 Android/iOS 设置(`apps/desktop/src-tauri/src/notices.rs`,在设置页挂载时读取一次);缓存与已关闭的 id(最多 200 个)存在共享状态目录的 `notices.json`。 - iOS App 的「设置」首页打开或回到前台时(`platforms/ios/SharedUI/core/AppNotices.swift`),存在 `Application Support/MSIME/notices`;键盘不拉取。 - Android App 的「设置」页出现时(`platforms/android/java/app/msime/android/home/NoticeBanner.java`),存在 `files/notices`;`:ime` 进程不拉取。 - HarmonyOS 设置窗口打开或回到前台时(`platforms/harmony/entry/src/main/ets/pages/Settings.ets`),存在 `files/state/notices`;键盘不拉取。 公告正文是简单 Markdown,在本机渲染,原始 HTML 不执行,不加载图片;正文里的链接只有在你点击时才用系统浏览器(或邮件应用)打开,且只接受 `http`、`https` 和 `mailto`。关闭某条公告只记在本机。 ### 社区举报与审核 社区里看别人发布的皮肤、候选窗口皮肤、插件、词库和回复模板时,每一项都有「举报」入口。只有你选择理由并提交时,才会向 `POST https://api.msime.app/v1/community/reports` 发送 `{kind, item_id, reason, detail}`:`reason` 是固定的六个理由之一(侵权/抄袭、色情低俗、违法违规、垃圾广告、恶意插件、其他),`detail` 是你自己填写的可选说明(最多 1000 字)。请求带账号令牌,已登录用登录账号,否则用本机匿名账号。共享实现在 `crates/client-core/src/community/report.rs`。 社区采用先发布后审核:发布的内容立即公开,管理员可以下架。查看「我的作品」或自己作品详情时,请求带 `fields=moderation`,只用来在被下架的作品上显示「已下架」;不会显示下架理由,也没有待审核状态。 ## 留在本地的东西 - **输入历史与学习数据**由输入引擎(`crates/engine`)管理,写在宿主提供的用户目录里,不上传。 - **剪贴板历史默认关闭**(`clipboard_history` 默认 `false`)。开启后写入状态目录下的 `clipboard_history.json`,仅本地;关闭时会清空该文件。Android 上开启后,水杉作为当前输入法时会在你每次复制文字后自动记下一条(隐私模式、密码类输入框和系统标记为敏感的内容除外),不再需要手动点「保存当前」。 - **@ 名单**:用户在设置页「插件」里手动添加的名字和地点写在状态目录下的 `plugins/mentions.json`,仅本地,不读取通讯录或位置。它不在 `preferences.json` 里,所以宿主拷贝偏好和账号同步都不会带上它。「@ 名字与地点」模式默认关闭(`local_modes.mention` 默认 `false`);从这个模式以及 V、/ 模式上屏的文字不进学习数据,也不计入打字统计。 - **导入的辅助码表**:Android 在「输入 › 辅助码方案」里导入的 `.txt` 码表只在本机校验后复制到状态目录下的 `plugins/helpcode/`,不联网、不上传;应用只读你在系统文件选择器里选中的那一个文件,不申请存储权限。 - **设置**保存在应用数据目录的 `preferences.json`,可用绝对路径环境变量 `MSIME_CLIENT_STATE_DIR` 指向隔离目录。 ## 没有的东西 没有第三方统计、用户行为分析或崩溃上报 SDK:Sentry、Mixpanel、Amplitude、Crashlytics、Google Analytics 及其等价物既不在依赖里,也不在代码里。全仓库唯一的上报路径是上面那条自己实现的[使用统计与崩溃上报](#使用统计与崩溃上报默认开启可关闭)。下面的命令命中的文件应当只有这几类: - 共享实现与 C ABI:`crates/client-core/src/telemetry.rs`、`crates/client-core/src/telemetry/tests.rs`、`crates/client-core/src/lib.rs`、`crates/client-core/src/account/client.rs`(发送用的 HTTP 客户端)、`crates/client-core/src/notices.rs`(文档注释里提到它)、`crates/client-core/src/preferences.rs`(`usage_reporting` 开关)、`crates/host-api/include/msime_client.h`、`crates/host-api/src/ffi/reporting.rs`、`crates/host-api/src/tests.rs`。 - 设置页开关:`packages/ui/src/index.tsx`、`packages/ui/src/settings/telemetry-section.tsx`、`packages/ui/src/settings/about-settings-actions.ts`、`packages/ui/src/settings/pages/about-page.tsx`、`apps/desktop/tests/settings/settings.test.tsx`。 - Windows 与 Linux:`platforms/common/Telemetry.{h,cpp}`、`platforms/common/tests/telemetry.cpp`、`platforms/windows/CMakeLists.txt`、`platforms/windows/settings/main.cpp`、`platforms/windows/src/entrypoints/server_main.cpp`、`platforms/linux/CMakeLists.txt`、`platforms/linux/README.md`、`platforms/linux/fcitx5/CMakeLists.txt`、`platforms/linux/fcitx5/FcitxEngine.cpp`、`platforms/linux/src/entrypoints/ibus_main.cpp`、`platforms/linux/tests/core/ibus_startup_telemetry.py`。 - Apple:`platforms/macos/src/core/UsageReporting.{h,mm}`、`platforms/ios/SharedUI/core/UsageReporting.swift`、`platforms/ios/tests/core/UsageReportingTests.swift`。 - Android:`platforms/android/README.md`、`platforms/android/java/app/msime/android/core/{Telemetry,MSIMEInputService}.java`、`platforms/android/java/app/msime/android/home/{HomeActivity,KeyboardSheets}.java`、`platforms/android/tests/core/TelemetryHandlerSmoke.java`。 - HarmonyOS:`platforms/harmony/README.md`、`platforms/harmony/native/client_napi.cpp`、`platforms/harmony/entry/src/main/ets/telemetry/Telemetry.ets`、`platforms/harmony/entry/src/main/ets/entryability/EntryAbility.ets`、`platforms/harmony/entry/src/main/ets/inputmethodextability/KeyboardExtensionAbility.ets`、`platforms/harmony/entry/src/main/ets/pages/Settings.ets`、`platforms/harmony/tests/keyboard-logic.test.ts`、`platforms/harmony/tests/tsconfig.json`。 ```sh git grep -ilwE '(telemetry|analytics|sentry|mixpanel|crashlytics)' -- crates apps packages platforms shared ``` `-w` 是必须的:不加的话 `PROCESSENTRY32W` 会匹配上 `sentry`,触感振幅和波形振幅的 `amplitude` 会匹配上 `amplitude`,全是误报。但别改写成 `\b…\b` 的形式——`git grep -E` 走的是 POSIX 扩展正则,`\b` 在这里不表示词边界,那样写的结果是命令永远返回零命中,看着像通过,其实什么都没查到。 依赖侧再核一遍锁文件: ```sh grep -niE 'opentelemetry|sentry|mixpanel|crashlytics|google-analytics' pnpm-lock.yaml Cargo.lock ``` 唯一的命中是 `@opentelemetry/api`,它是测试框架 vitest 的**可选 peer 依赖**,没有被安装,也不进入任何产物。 ## 自己核对 ```sh # 所有出现在代码里的外部地址 git grep -nIoE 'https?://[a-zA-Z0-9.-]+\.[a-z]{2,}' -- crates apps packages platforms shared | sort -u # 云候选的完整 URL 构造与其边界检查 sed -n '/fn build_google_url/,/^}/p' crates/client-core/src/cloud/candidates.rs # 所有默认开启的偏好 grep -n 'enabled_by_default' crates/client-core/src/preferences.rs ``` 发现本文档与代码不符,请按[贡献指南](.github/CONTRIBUTING.md)提 issue;若涉及数据泄露或凭据问题,走[安全策略](.github/SECURITY.md)的私下报告入口,不要开公开 issue。