Cherry Studio 小程序活动日志(Activity Log)机制详解:记录了什么、永不记录什么、如何保留与清理

发布时间:2026/9/20 22:43:31
Cherry Studio 小程序活动日志(Activity Log)机制详解:记录了什么、永不记录什么、如何保留与清理 Cherry Studio 小程序活动日志Activity Log机制详解记录了什么、永不记录什么、如何保留与清理【免费下载链接】cherry-studio Cherry Studio 是一款支持多个 LLM 提供商的桌面客户端项目地址: https://gitcode.com/CherryHQ/cherry-studioCherry Studio 为每个小程序Mini App维护一份独立的活动日志Activity Log并在小程序的详情面板中向用户展示。它是用户了解这个小程序用授予它的能力做了什么的唯一官方记录每一次权限被拒、每一次离开沙箱的调用、每一次权限授予或撤销都会留下痕迹而任何敏感载荷存储键值、消息文本、剪贴板内容、请求响应体则被刻意排除在外。本文以 docs/references/mini-app/activity-log.md 为骨架结合 activityLog.ts、miniAppActivity.ts、MiniAppRuntimeService.ts 与 bridge.ts 等源码完整讲解活动日志的记录分类、隐私边界、保留策略、底层实现与调试实践。活动日志是什么用户视角的可审计账本活动日志是宿主Cherry Studio 主进程针对每个已安装小程序独立维护的 JSONL 文件集展示在应用详情面板上。其设计出发点是一句话它记录小程序做了什么从不记录小程序说了什么。做了什么调用过哪些能力方法、结果成败、耗时、以及必要的元数据目标主机、状态码、数据量说了什么不包含任何存储键名与键值、文件名、消息文本、通知标题、剪贴板文本、请求或响应正文。这份日志既不属于小程序开发者Nothing in it is yours to read or write也不受小程序代码控制——它是宿主单方面写入的用户审计记录也是用户向技术支持分享排障信息时不会泄露个人数据的凭证。从源码结构看整个机制由三个部分协作完成模块职责activityLog.ts日志写入、计数聚合、按日归档、保留与清理的纯逻辑单例miniAppActivity.ts日志行的 Zod Schema 与类型定义调用、决策、计数、截断四类MiniAppRuntimeService.ts决定何时 flush 计数何时忘记日志等时机问题记录什么四种记录类型与完整分类表活动日志共包含四种kind的记录见 miniAppActivity.ts 中的四个 Schemacall调用、grant权限决策、count计数、truncated截断标记。下表对应原文档的分类逻辑类型kind触发时机一行包含的内容call· 拒绝任何cherry.*调用被拒绝公开错误名为PermissionDenied、RateLimited、QuotaExceeded、InvalidArgument、Unavailable、Cancelled、Internal之一方法名、结果、耗时当公开名称无法说明具体失败原因时附加reason字段指明底层错误类名call· 出站调用network.fetch、clipboard.read/write、file.export、notification.show、ai.chat方法名、结果、耗时外加元数据 facetfetch 记录主机、状态码与响应字节数剪贴板记录读写字符数export 记录是否保存成功chat 记录模型槽位、消息条数与提示词大小grant· 权限决策安装、重装、更新、回滚、详情面板中的授予或撤销、新请求权限被授予或暂缓snooze、清除数据决策名称、版本号、涉及的权限叶子permissions / removedcount· 计数其余所有调用——storage.*、file.save/load/list/delete、usage、app.*——按方法聚合调用次数、移动的字节数每分钟 flush 一次分级依据TIER 表方法被精确分成event逐条记录与count聚合计数两档定义在 activityLog.ts 的TIER常量中覆盖全部MiniAppMethodevent档每个调用一条独立日志行ai.chat、file.export、notification.show、clipboard.read、clipboard.write、network.fetch——这些调用都会离开沙箱是审计关注的核心count档每分钟聚合为一行app.getInfo、app.getPermissions、ai.getCapabilities、ai.cancel、storage.get/set/delete/keys/usage、file.save/load/list/delete/usage。源码注释给出了分档理由storage.set单独可能一秒钟跑 20 次一个几分钟就被写满的日志没人会读。所以沙箱内部调用只累计计数但任何被拒绝的调用无论属于哪一档都会强制写成一条独立日志行——小程序探测它没有被授予的能力正是这份日志存在的意义对应测试用例 always writes a refusal as its own line, even for a counted method。出站调用的 facet 明细facetOfactivityLog.ts为出站调用提取纯元数据network.fetch解析 URL 提取host主机名有数字状态码则记录status有响应体则用base64Bytes估算解码后的bytes大小。解析失败的 URL 本身不记录——无法解析的 URL 正是拒绝本身的 reason无需另行归因clipboard.write记录写入文本的字符数charsclipboard.read记录读出文本的字符数file.export记录saved: true/falseai.chat记录model未指定时为default、messages条数、各消息 content 的总字节数bytes。base64Bytes是一个值得一提的细节activityLog.ts桥接层不携带二进制因此函数直接按 base64 字符串长度与尾部填充数估算解码后字节数无需真正解码也就不会在日志中出现二进制内容。永不记录什么强制的隐私边界原文档明确任何类型的载荷都不进入日志——没有存储键或值、没有文件名、没有消息文本、没有通知标题、没有剪贴板文本、没有请求或响应体。因此用户可以把日志分享给技术支持而不必泄露自己在小程序里做了什么。这条边界在测试中是被显式守护的见 activityLog.test.ts写入剪贴板文本my password后日志行只包含facet: { chars: 11 }且JSON.stringify(line)断言不包含passwordnetwork.fetch带?tokenabc的 URL 与 base64 请求/响应体日志只保留{ host: api.example.com, status: 200, bytes: 8 }断言不包含token25 次storage.set键k、值vvvv聚合为一行{ kind: count, name: storage.set, calls: 25, bytes: 100 }断言不包含值文本。reason字段同样受控它只携带底层错误类名如AI_APICallError从不携带错误消息。与之呼应的是 bridge.ts 中的publicErrorOf——未映射的内部错误统一归为Internal并丢弃消息因为宿主 bug 的文本里带有堆栈和绝对路径不能流入不可信代码。存储与保留策略JSONL 格式与文件命名日志以JSONL每行一个 JSON 对象格式存储位于 Cherry 的 logs 目录下路径由 paths.ts 的miniAppLogsPath给出feature.mini_app.logs/appId。每个活动日一个文件命名匹配activity.YYYY-MM-DD.log。每个日志行都带v: 1版本号使未来格式演进时无需重写旧文件即可区分miniAppActivity.ts 的设计注释。文件按行追加写入appendFile每行一次、不持有打开句柄——因此没有每个运行中应用的句柄需要关闭退出时也无需特殊清理activityLog.ts 头部注释。三个保留参数定义在 activityLog.ts/** Activity days, not calendar days: the newest seven files survive however old they are. */ export const ACTIVITY_DAYS_KEPT 7 /** Per app per day. Past it the day gets one truncated line and nothing more. */ export const ACTIVITY_DAY_BYTES 5 * 1024 * 1024 /** The backstop for long-running apps; the runtime also flushes at the moments it knows about. */ export const ACTIVITY_COUNT_FLUSH_MS 60_000保留 7 个活动日注意是有活动的日子而非日历日。sweepactivityLog.ts按数量保留最新 7 个日文件无论它们有多旧——一个月打开一次的小程序依然能看到它的上次会话每日 5 MB 预算budgetFor以文件实际大小计预算activityLog.ts。当新一行会越过预算时当日文件追加一条{ kind: truncated }标记行并停止当日写入之后不再记录任何内容每分钟 flush累计的计数每 60 秒落盘一次后文详述 flush 时机。dayFiles的排序即按文件名日期字符串倒序天然最新优先activityLog.ts。生命周期clear data / clear log / uninstall 三者的区别这是最容易混淆的部分原文档与源码完全一致操作对活动日志的影响实现清除数据Clear data不影响日志——日志放在 logs 目录而非 data 目录两者刻意分离见 paths.ts 注释 Under logs, not dataclear_data只写一条grant决策记录management.ts清除日志Clear log删除整个日志目录下一条记录会重新创建它clear()activityLog.ts卸载Uninstall删除整个日志目录并且之后拒绝任何写入直到新的安装记录到来forget()activityLog.tsforget与clear的差异在测试 refuses a call landing after forget — unlike clear — until a new install 中被精确验证activityLog.test.tsforget会同步把 appId 加入forgotten集合使卸载后仍然在途的异步能力调用例如已发出的network.fetch晚于卸载才返回无法把日志目录mkdir 回来而clear不设此标记下一条记录即可重建日志。只有当新的install决策记录到来时forgotten标记才会被解除activityLog.ts。详情面板展示面板展示通过list(appId, { limit, deniedOnly })实现activityLog.ts返回最新 100 行跨保留的所有日文件、从新到旧读取返回整个日志在磁盘上的总字节数按文件stat大小累加且页面读满后不再读取更多文件内容——保留窗口理论上可达 35 MB而面板每次打开都会调用它deniedOnly: true时可只列出未以ok结束的调用排障利器崩溃可能留下残缺的末尾行解析失败会被跳过而不是致命错误Open log folder 调用openFolderactivityLog.ts先确保目录存在再用shell.openPath打开——文件属于用户可以用任何工具阅读。源码级实现一条日志行如何产生桥接层的统一记录点所有cherry.*调用都经由 bridge.ts 的route函数。在完成调用方身份解析resolveAppIdBySender身份只来自 Electron 的webContentsId绝不信任载荷自报的 appId之后调用从此可归属因此从此可记录调用成功recordCall(appId, method, ok, durationMs, params, value)调用抛出recordCall(appId, method, publicErrorOf(error).name, durationMs, params, undefined, reasonOf(error))——reason合并进 facet 而非替换丢失调用自身的 facet 来携带 reason是用一个盲区换另一个盲区。recordCallactivityLog.ts先检查forgotten标记与 TIER 档位count档且结果为ok的调用只累加calls与bytesbytesOf按方法估算移动的字节数见 activityLog.ts其余情况生成独立行。recordCall永不抛出——日志失败不是小程序的失败。写入串行化与防交错serializeactivityLog.ts按 appId 维护一条 Promise 链保证同一小程序的追加写入与 sweep/删除不会交错。这一点在forget场景中至关重要由于卸载刻意不等待在途调用设计约定 §2.1晚到调用会排在该 app 的删除操作之后若没有forgotten同步标记与写串行化就会把日志目录重新建回来。flush 时机谁来决定计数何时落盘MiniAppActivityLog是纯单例不是生命周期服务——它不拥有任何定时器activityLog.ts。决定权在 MiniAppRuntimeService.ts每分钟兜底第一个 guest 到达时启动setInterval(flush, ACTIVITY_COUNT_FLUSH_MS)并unref()最后一个 guest 离开时停止registerGuest/unregisterGuestMiniAppRuntimeService.ts——空闲的宿主不应为日志空转计时器最后一个 guest 离开时unregisterGuest在检测到该 app 已无存活实例后立即flush(appId)计数现在落盘而不是等下一分钟小程序被下线quiesce前quiesceThenMutate在任何发布类变更卸载、升级、回滚、clear_data 等之前先flush(appId)确保计数在变更产生的 grant 行之前落地MiniAppRuntimeService.ts服务停止时onStop中flush()全量收尾MiniAppRuntimeService.ts。flush(appId)支持单应用定向 flush——运行时在小程序的最后一个 guest 离开时 flush 它其他应用不得因此丢失自己的计数窗口对应测试用例 activityLog.test.ts。权限决策记录的来源recordGrant的调用点分布在各发布路径中对应原文档的决策类型枚举install/reinstall/update/rollback/grant/revoke/grant_pending/snooze_pending/clear_data见 miniAppActivity.ts安装与重装installer.tsinstall/reinstall回滚webInstaller.ts授予 / 撤销 / 批量授予 / 暂缓 / 清除数据management.tsgrant、revoke、grant_pending、snooze_pending、clear_data。测试覆盖行为即契约activityLog.test.ts 共 14 个用例把上述行为固化成了可回归的契约值得在改造时参考出站调用一行、仅元数据、绝不携带载荷文本计数聚合为一行25 次调用 →calls: 25, bytes: 100拒绝调用永远是独立行即使属于 count 档Unavailable拒绝携带reason且与 facet 合并保留 7 个活动日、按数量而非年龄清理非日志文件如notes.txt不受影响5 MB 预算耗尽后只追加一条truncated标记list最新优先、deniedOnly过滤、跳过残缺行bytes/days统计覆盖未读入条目的文件clear后下一条记录重建日志forget后拒绝写入直到新安装openFolder先建目录再打开。给小程序作者的实践建议原文档For authors一节的建议可以结合源码进一步展开用户看到自己授权的小程序反复撞PermissionDenied一定会打开活动日志求证所以被拒绝的调用永远不会是静默的。调用前先查权限通过cherry.app.getPermissions()检查对应能力是否已授予该接口刻意不做门禁——它报告的正是调用者自己的授权状态门禁它只会让小程序对自己的可调用范围失明见 bridge.ts在 UI 中处理拒绝而非盲目重试活动日志会把每次PermissionDenied单独记一行重试只会刷屏日志并暴露小程序在探测未授权能力这一事实。应当提示用户去详情面板授权或优雅降级留意Unavailable的两种含义它同时覆盖提供商暂时不可用与小程序被清空/下线测试 keeps the reason a refusal came with 即为此设计面板借助reason帮助用户区分不要试图绕过或篡改日志日志由宿主主进程独占写入入口在 bridge.ts 的唯一通道上小程序代码拿不到任何读写它的 API。延伸阅读小程序能力总览capabilities.md 与接口定义 cherry.d.ts生命周期与权限lifecycle.md、sandbox.md日志核心实现activityLog.ts、类型契约 miniAppActivity.ts日志读写路径MiniAppRuntimeService.ts、bridge.ts、paths.ts行为测试activityLog.test.ts【免费下载链接】cherry-studio Cherry Studio 是一款支持多个 LLM 提供商的桌面客户端项目地址: https://gitcode.com/CherryHQ/cherry-studio创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考