☰
Operit ToolPkg 包 Logo 支持实现指南:manifest 声明、运行时缓存读取与市场展示全链路解析
2026/10/3 2:02:19 网站建设 项目流程
  • AI Agent
  • 人工智能
  • 大模型
  • AI 应用
  • 工具调用
  • 本地部署
  • MCP Clients
  • Agent 记忆

【免费下载链接】Operit

The most powerful AI agent and AI chat software on Android/Operit是一款Android上能力最为强大、发展最久的AI Agent

项目地址:https://gitcode.com/gh_mirrors/op/Operit
点击查看免费下载

ToolPkg 是 Operit 的插件/工具包归档格式,本指南围绕仓库文档 toolpkg_logo_support_20260820 中「Manifest And Runtime」分册(1_manifest-and-runtime.md)展开,系统讲解 ToolPkg 包级 Logo 从 manifest 声明、解析校验、运行时缓存读取,到插件列表/市场卡片渲染与发布流程的完整实现。读完本文,你将掌握如何为自己的 ToolPkg 归档声明可选 Logo、理解其底层校验规则与数据流,并了解 Operit 客户端如何在不升级 manifest 版本、不改归档结构的前提下完成包 Logo 的本地渲染与市场展示。

一、背景:ToolPkg 图标现状与能力复用

在引入 Logo 支持之前,Operit 中 ToolPkg 容器的图标呈现存在明显空缺:

  • 包管理器(Package Manager)渲染的是通用 Apps 图标;
  • 市场列表与市场详情头部渲染的是由标题派生的首字母色块(avatar);
  • 而模型 Provider 的 Logo 加载器ProviderLogoLoader已经具备 AndroidSVG 解析与 Bitmap 缩放能力,但它被绑定在 APK assets 目录与 provider 标识上,无法直接服务于包级 Logo(见 2-rendering-and-ui.md 的 Existing 描述)。

同时,ToolPkg 的运行时缓存已经提取了归档中的每一个条目(runtime cache already extracts every archive entry),这意味着只要在 manifest 中声明一个 Logo 资源键,运行时无需引入任何新的归档格式或解压策略,就能直接从缓存中读取该资源字节。

因此该特性(状态已标记为 [DONE])的核心思路是:

为 ToolPkg 添加一个可选的包 Logo 资源,它随归档一起分发,安装后在本地由包缓存渲染;市场响应可额外提供可选的logoUrl用于展示,但客户端不上传、不托管 Logo 文件。

二、Manifest 契约:logo字段与资源声明

2.1 契约定义

logo是 manifest 顶层的一个可选字段,其值是一个资源键(resource key),必须指向resources数组中已声明的某个条目。官方契约示例(摘自 index.md):

{ "logo": "plugin_logo", "resources": [ { "key": "plugin_logo", "path": "resources/logo.svg", "mime": "image/svg+xml" } ] }

支持的静态图片格式为SVG、PNG、JPEG 和 WebP。这与仓库的 ToolPkg 格式指南(docs/TOOLPKP_FORMAT_GUIDE.md)一致:

logo| string | 否 | 包 Logo 对应的resources[].key;支持 SVG、PNG、JPEG 和 WebP

在归档目录结构中,Logo 文件一般放在resources/下(如resources/logo.svg,见 TOOLPKG_FORMAT_GUIDE.md 的目录树注释「可选包 Logo」)。

2.2 在完整 manifest 中的位置

以格式指南的完整示例为参考,logo与resources的配合关系如下(节选):

{ "schema_version": 1, "toolpkg_id": "com.operit.windows_bundle", "version": "0.2.0", "api_version": "1.0.0", "main": "main.js", "logo": "package_logo", "resources": [ { "key": "package_logo", "path": "resources/logo.svg", "mime": "image/svg+xml" } ] }

其中schema_version仍保持1,不需要任何版本升级(详见下文兼容性小节)。

三、解析与校验:源码级实现

3.1 manifest 数据模型

在 ToolPkgParser.kt 中,ToolPkgManifest通过@SerialName("logo")接收任意 JSON 元素,并对外暴露字符串访问器:

@SerialName("logo") val logoElement: JsonElement? = null, // ... val logo: String? get() = (logoElement as? JsonPrimitive)?.takeIf { it.isString }?.content

也就是说,logo必须是 JSON 字符串(资源键);非字符串或缺失时返回null,走「无 Logo」路径。

3.2 资源解析与三条件校验

在解析资源列表之后,解析器调用resolveLogoResource(manifest.logo, resources)(ToolPkgParser.kt),其实现(L1823-L1849)依次执行以下校验:

private fun resolveLogoResource( logoResourceKey: String?, resources: List<ToolPkgResourceRuntime> ): ToolPkgResourceRuntime? { val key = logoResourceKey?.trim().orEmpty() if (key.isBlank()) return null // 1. 未声明 → 返回 null val resource = resources.firstOrNull { it.key.equals(key, ignoreCase = true) } ?: throw IllegalArgumentException( "manifest.logo must reference an existing resource key: $key" ) // 2. 键必须命中 resources(大小写不敏感) if (isDirectoryResourceMime(resource.mime)) { throw IllegalArgumentException( "manifest.logo must reference a file resource: $key" ) // 3a. 必须是文件资源,不能是目录资源 val extension = resource.path.substringAfterLast('.', "").lowercase() val mime = resource.mime.trim().lowercase() if (extension !in TOOLPKG_LOGO_EXTENSIONS && mime !in TOOLPKG_LOGO_MIME_TYPES) { throw IllegalArgumentException( "manifest.logo must reference an SVG, PNG, JPEG or WebP resource: $key" ) // 3b. 扩展名或 MIME 必须受支持 } return resource } private val TOOLPKG_LOGO_EXTENSIONS = setOf("svg", "png", "jpg", "jpeg", "webp") private val TOOLPKG_LOGO_MIME_TYPES = setOf("image/svg+xml", "image/png", "image/jpeg", "image/webp")

可归纳为三条规则:

校验点规则违反时的行为
键存在性logo值必须在resources[].key中存在(忽略大小写)抛IllegalArgumentException,包加载失败
资源类型必须是文件资源(MIME 不能是目录类型,如inode/directory、application/x-directory等)抛异常,包加载失败
格式支持扩展名 ∈{svg, png, jpg, jpeg, webp}或 MIME ∈{image/svg+xml, image/png, image/jpeg, image/webp}抛异常,包加载失败

注意这里的校验是「扩展名或MIME 命中其一即可」,同时扩展名与 MIME 不一致的场景也可以接受(只要至少一端命中支持集合),判定依据以解析器常量为准。

解析成功后,Logo 资源被存入运行时容器详情ToolPkgContainerRuntime.logoResource: ToolPkgResourceRuntime?(ToolPkgParser.kt#L190),并在构建容器时写入(L1466)。ToolPkgResourceRuntime携带key / path / mime三个字段,完整描述了这个 Logo 资源。

四、运行时数据流:从容器详情到缓存字节读取

4.1 公开容器详情携带 Logo 元数据

PackageManager的公开容器详情数据类(PackageManager.kt)新增了两个字段:

val logoResourceKey: String? = null, val logoMimeType: String? = null,

并定义了 Logo 字节的返回类型:

data class ToolPkgLogoBytes( val resourceKey: String, val mimeType: String, val fileName: String, val bytes: ByteArray )

4.2 包管理器读取方法

包管理器对外暴露readToolPkgLogoBytes(packageName)(PackageManager.kt#L1457-L1469),这是「读取缓存 Logo 字节」的唯一入口:

fun readToolPkgLogoBytes(packageName: String): ToolPkgLogoBytes? { ensureInitialized() val normalizedPackageName = normalizePackageName(packageName) val runtime = toolPkgContainers[normalizedPackageName] ?: return null val logoResource = runtime.logoResource ?: return null val bytes = readToolPkgResourceBytes(runtime, logoResource.path) ?: return null return ToolPkgLogoBytes( resourceKey = logoResource.key, mimeType = logoResource.mime, fileName = logoResource.path.substringAfterLast('/'), bytes = bytes ) }

关键点:

  • 返回值null有两种情形:包不存在,或该包未声明 Logo——UI 层据此回退到通用图标/首字母色块;
  • 字节直接来自已解压的运行时资源缓存(readToolPkgResourceBytes),无需重新解压归档;
  • fileName取资源路径最后一段,供渲染器根据扩展名辅助判定格式。

五、渲染与 UI:复用 Provider Logo 的通用渲染器

5.1 通用字节/流渲染器

原 Provider Logo 实现(ProviderLogoLoader.kt)中的LogoBitmapLoader被重构为通用 byte/stream 渲染器,同时服务 provider 与 ToolPkg 两种来源。其核心逻辑(L76-L164):

  • SVG 路径:使用 AndroidSVG(SVG.getFromInputStream)解析,按目标尺寸等比缩放,在正方形画布上居中绘制(renderSvgToBitmap);
  • 位图路径:BitmapFactory.decodeStream解码后等比缩放并居中(scaleBitmapToBitmap),位图扩展名集合为png/jpg/jpeg/webp,MIME 集合为image/png/image/jpeg/image/webp;
  • 渲染前根据 MIME 或文件扩展名判定格式,无法识别的返回null。

与之配套的 Compose 入口rememberLogoPainter(logoKey, bytes, mimeType, fileName, size)(L191-L222)通过produceState+Dispatchers.IO异步解码,返回Painter?;null表示无 Logo。

5.2 Provider 专用逻辑保持不变

Provider 特有的素材目录查找(model_logos/{providerTypeId}/)与深色模式染色(providerLogoColorFilter(),深色表面将黑色品牌素材染亮、浅色模式保留原色)继续留在 Provider API 中(ProviderLogoLoader.kt 与 L224-L233)。

5.3 插件 Logo 以原色渲染

与 Provider Logo 不同,ToolPkg 插件 Logo 在包管理器、市场列表、市场详情头部保留原始颜色渲染(不套用 Provider 的染色逻辑),未声明 Logo 的包与条目继续使用当前通用图标/首字母色块。各 UI 落点:

界面实现位置说明
包管理器插件列表PackageManagerScreen.kt#L836-L838通过packageManager.readToolPkgLogoBytes(packageName)传入loadPluginLogo回调
插件页 TabPluginTabContent.kt#L141rememberLogoPainter渲染插件 Logo
包详情对话框PackageDetailsDialog.kt#L103-L111IO 线程读取字节后rememberLogoPainter

例如包详情对话框中的读取与渲染:

withContext(Dispatchers.IO) { packageManager.readToolPkgLogoBytes(packageName) } // ... rememberLogoPainter(logoKey = ..., bytes = ..., mimeType = ..., fileName = ..., size = ...)

六、市场发布:只读不传、本地预览与远程logoUrl

6.1 发布屏只读取已声明的 Logo

发布流程遵循「发布 ToolPkg 时仅使用归档中已有的manifest.logo资源」的原则(TOOLPKG_FORMAT_GUIDE.md#L233)。发布屏通过ToolPkgArtifactMinifier.readToolPkgLogoAsset(sourceFile)(ToolPkgArtifactMinifier.kt#L20-L58)从选定的归档文件中直接读取 Logo:

  • 复用ToolPkgArchiveParser.readToolPkgManifestPreview解析 manifest;
  • 校验logo键存在于resources中、且不是目录资源;
  • 归一化资源相对路径后从 ZIP 中读取字节;
  • 强制大小上限:require(bytes.size <= PUBLISH_LOGO_MAX_BYTES),即 Logo 不得超过 512 KiB(PUBLISH_LOGO_MAX_BYTES = 512 * 1024,定义于 ArtifactMarketModels.kt#L12),返回ToolPkgLogoAsset(fileName, contentType, bytes)。

发布屏(ArtifactPublishScreen.kt)在produceState中异步读取包 Logo(L405-L419),在发布卡片(L870-L911)与最终确认对话框(L1277-L1281)中展示预览,并在发布预览对话框(MarketPublishPreview.kt)中以 48.dp(列表卡片)与 76.dp(详情头部)两种尺寸渲染(L73-L90)。

关键约束:客户端不在 publish / update / new-version 请求中上传、托管或发送任何 Logo 数据。市场侧对已安装包与包预览的渲染一律取自本地归档资源。

6.2 市场条目的可选logoUrl

市场条目响应模型(MarketStatsApiService.kt#L312)与市场浏览模型(MarketBrowseList.kt#L70)均包含可选字段logoUrl: String?,由服务端返回后经MarketBrowseEntryMappers(MarketBrowseEntryMappers.kt#L46)映射到 UI 模型。

远程 Logo 的加载由 RemoteLogoLoader.kt 完成,安全与资源控制措施明确:

  • 仅接受httpsURL,非法/非 https 直接返回null(L51-L52);
  • 单张 Logo 上限MAX_LOGO_BYTES = 512 * 1024,流式读取并实时校验(L31、L76-L101);
  • 内存 LRU 缓存上限 4 MB(MAX_CACHE_BYTES = 4 * 1024 * 1024),OkHttp 连接超时 15 s、读取超时 20 s、跟随重定向(L32-L48);
  • 下载后的字节同样交给LogoBitmapLoader按 MIME/文件名渲染。

市场详情头部(UnifiedMarketDetailScreen.kt#L532-L569)采用优先级策略:本地包 Logo(logoPainter)优先,其次才是远程logoUrl:

val remoteLogoPainter = rememberRemoteLogoPainter( logoUrl = logoUrl.takeIf { logoPainter == null }, size = 76.dp ) val resolvedLogoPainter = logoPainter ?: remoteLogoPainter

两者皆为空时,回退到标题首字母(fallbackAvatarText)色块。市场列表卡片同样遵循「本地优先、远程兜底」的取值逻辑(MarketBrowseList.kt#L425-L442)。

七、兼容性与边界

  • 无logo的旧归档完全不受影响:logo缺失时解析器返回null,所有界面继续渲染通用 Apps 图标或标题首字母色块;
  • manifest 无需版本升级:schema_version保持1,api_version保持既有取值,新旧归档在同一客户端内共存;
  • 校验失败即拒绝加载:若logo引用了不存在的资源键、目录资源或不支持的格式,包加载会直接抛错,避免出现「声明了但渲染不出」的中间态;
  • 发布与市场解耦:manifest.logo是归档内资源(本地渲染),logoUrl是市场可选展示字段(远程渲染),两者独立存在、互不替代。

八、相关源码与文档索引

  • 特性总览与契约:docs/TODO/toolpkg_logo_support_20260820/index.md
  • 分册文档:1_manifest-and-runtime.md、2-rendering-and-ui.md、3-market-publish.md
  • 格式指南(含logo字段与完整 manifest 示例):docs/TOOLPKG_FORMAT_GUIDE.md
  • manifest 模型与 Logo 解析校验:ToolPkgParser.kt
  • 公开容器详情与缓存读取:PackageManager.kt
  • 通用 Logo 渲染器:ProviderLogoLoader.kt
  • 远程 Logo 加载器:RemoteLogoLoader.kt
  • 发布侧 Logo 提取与大小限制:ToolPkgArtifactMinifier.kt、ArtifactMarketModels.kt

综上,ToolPkg Logo 支持在不改归档格式、不升级 manifest 版本的前提下,通过「manifest 可选声明 + 运行时资源缓存 + 通用 SVG/位图渲染器 + 市场可选logoUrl」四层设计,为包管理器、插件列表、市场列表与详情页提供了完整一致的品牌化图标体验,同时严格约束了发布与网络侧的 Logo 数据传输边界。

  • AI Agent
  • 人工智能
  • 大模型
  • AI 应用
  • 工具调用
  • 本地部署
  • MCP Clients
  • Agent 记忆

【免费下载链接】Operit

The most powerful AI agent and AI chat software on Android/Operit是一款Android上能力最为强大、发展最久的AI Agent

项目地址:https://gitcode.com/gh_mirrors/op/Operit
点击查看免费下载

相关推荐

上一篇:探索Dogescript生态系统:插件、工具与第三方库推荐
下一篇:Akagi麻将助手:从新手到高手的完整实战指南

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询