- AI Agent
- 人工智能
- 大模型
- AI 应用
- 工具调用
- 本地部署
- MCP Clients
- Agent 记忆
【免费下载链接】Operit
The most powerful AI agent and AI chat software on Android/Operit是一款Android上能力最为强大、发展最久的AI Agent
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回调 |
| 插件页 Tab | PluginTabContent.kt#L141 | rememberLogoPainter渲染插件 Logo |
| 包详情对话框 | PackageDetailsDialog.kt#L103-L111 | IO 线程读取字节后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
相关推荐
Operit ToolPkg 市场 API 版本贯通:从 manifest `api_version` 到发布链路与市场展示的实现指南
Operit ToolPkg 市场 API 版本贯通:从 manifest api_version 到发布链路与市场展示的实现指南 本文以 Operit 开源仓
AI Agent人工智能大模型AI 应用工具调用本地部署MCP ClientsAgent 记忆GUI 自动化Operit ToolPkg Logo 支持指南:为插件包声明与渲染可选包 Logo 的完整实现方案
Operit ToolPkg Logo 支持指南:为插件包声明与渲染可选包 Logo 的完整实现方案 本文以 Operit 开源仓库中 docs/TODO/to
AI Agent人工智能大模型AI 应用工具调用本地部署MCP ClientsAgent 记忆GUI 自动化Operit ToolPkg 市场发布中的 Logo 支持:manifest.logo 本地资源与 logoUrl 远端展示的分工
Operit ToolPkg 市场发布中的 Logo 支持:manifest.logo 本地资源与 logoUrl 远端展示的分工 本篇技术指南围绕 Operi
AI Agent人工智能大模型AI 应用工具调用本地部署MCP ClientsAgent 记忆GUI 自动化
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考