- Node.js >= 18
- npm >= 9
- Rust >= 1.77 (安装: https://rustup.rs)
- Android SDK + NDK 28 (仅 Android 构建需要)
- Android Studio(或
sdkmanager)用于模拟器管理
cd amiba
npm install
npm run dev # 启动开发服务器 → http://localhost:5173npm run dev # 启动开发服务器 (localhost:5173)
npm run build # 生产构建 (vue-tsc + vite)
npm run preview # 预览生产构建
npx tauri dev # 启动 Tauri 桌面应用(开发模式)
npx tauri build # 打包 Tauri 桌面应用
npx tauri android dev # Tauri Android 开发构建(需模拟器/设备)
npx tauri android build # Tauri Android 生产构建npm run plugin:create -- my-plugin # 生成本地插件骨架
npm run plugin:validate -- <pluginDir> # 静态校验
npm run plugin:add -- <pluginDir> # 开发期启用
npm run plugin:remove -- my-plugin --purge # 移除并备份
npm run plugin:package -- <pluginDir> # 打包 .amiba-plugin
npm run plugin:verify # 完整性校验
npm run plugin:sync # 重新生成本地插件注册表插件扩展点与开发细节见 docs/plugin/ 与内置 skill plugin-dev。
# 前置条件:Android SDK 28+, NDK 28, 模拟器运行中
# 检查设备连接
adb devices
# 开发构建(自动编译 Kotlin + Rust,安装并启动)
npx tauri android dev
# 生产构建
npx tauri android build注意: tauri android init 会重置 gen/android/ 目录。自定义 Kotlin 代码放在 MainActivity.kt 中(位于 gen/android/app/src/main/java/com/amiba/desktop/),该文件不会被 tauri android dev 覆盖。
# src-tauri/Cargo.toml
tauri-plugin-android-fs = "28.4.0" # SAF Picker + content:// URI 文件操作(替代旧 JNI pick_folder)
[target.'cfg(target_os = "android")'.dependencies]
jni = { version = "0.21", features = ["invocation"] } # read_tombstone 仍需要
ndk-context = "0.1"
libloading = "0.8" # 用于动态加载 libnativehelper.so 获取 JVM- 启动应用,进入 设置 页面
- 填入 API Key
- 默认使用 DeepSeek API,可改为任何 OpenAI 兼容接口:
- Base URL:
https://api.deepseek.com - 对话模型:
deepseek-v4-flash/deepseek-v4-pro
- Base URL:
在「AI 供应商」设置卡片中可配置多个 AI 提供商:
- 每个供应商包含:名称、ID、Base URL、API Key、模型列表(每行一个模型名)
- 支持的供应商类型:DeepSeek / OpenAI / Ollama / 任何 OpenAI 兼容 API
- API Key 按供应商独立存储,优先于全局 API Key
在「自定义 Agent」设置卡片中可创建专属 AI 助手:
- 绑定供应商(下拉选择)→ 模型从供应商的模型列表中下拉选择
- Skill 通过勾选框从已导入的 Skill 列表中多选
- 可选自定义 System Prompt 或关联 Soul 人格文件
- 点击「启用」切换当前使用的 Agent
- 未选择 Agent 时使用默认 API 配置
src/
├── main.ts # Vue 入口(先加载 config/polyfill.ts 兼容旧 WebView),挂载 Router
├── App.vue # 根组件:GlassBackground + EdgeNavHint + router-view(无顶栏)
├── router/index.ts # 7 条路由(含动态服务路由);导出 PAGE_ORDER 页面序列;onError chunk 加载失败自愈刷新
├── types/service.ts # 全部 TypeScript 类型
├── config/config.ts # 统一配置(amiba_settings,reactive + 自动持久化)
├── config/storage.ts # 存储抽象层
├── config/updater.ts # 更新检查 + Rust reqwest 下载
├── config/session-db.ts # SQLite FTS5 数据库封装
├── config/web-bridge.ts # Tauri web_* invoke 封装(fetchPage/clickGetContent/close + captureScreenshot)
├── config/folder-picker.ts # 文件夹选取统一入口(Android→tauri-plugin-android-fs SAF Picker / 桌面→plugin-dialog / 浏览器→prompt)
├── config/logger.ts # 前端日志系统:monkey-patch console → JSON Lines 文件持久化 + 轮转
├── i18n/ # 多语言 (zh-CN / en)
│ ├── index.ts # createI18n + settings.language 同步
│ ├── types.ts # LocalesSchema 类型
│ └── locales/
│ ├── zh-CN.ts # 中文语言包
│ └── en.ts # 英文语言包
├── ai/
│ ├── agent.ts # LLM 流式对话 + 多工具循环
│ ├── system-prompt.ts # System Prompt 两层组装器(stable/volatile)
│ ├── soul.ts # 人格系统
│ ├── session.ts # 多会话管理(创建/切换/删除)
│ ├── memory-store.ts # MEMORY.md / USER.md 记忆引擎
│ ├── packager.ts # 多文件 ServicePackage → 单 HTML 内联
│ ├── catalog.ts # YAML 组件目录
│ ├── skills.ts # Skill 管理 + 导入
│ ├── skill-parser.ts # SKILL.md frontmatter 解析
│ ├── skill-commands.ts # Skill 扫描 + 斜杠命令检测 + 缓存失效
│ ├── skill-packager.ts # SkillPackage 打包/安装引擎
│ ├── skill-zip.ts # Skill ZIP 导入/导出 + URL 导入
│ ├── skill-usage.ts # Skill 使用统计
│ ├── skill-curator.ts # Skill 生命周期管理
│ ├── service-validator.ts # 服务代码校验(localStorage/BroadcastChannel/权限一致性)
│ ├── doc-index.ts # 文档索引/搜索/读取(内置 + 用户)
│ ├── requirement-store.ts # 需求追踪引擎
│ ├── provider-store.ts # AI 供应商管理
│ └── custom-agent-store.ts # 自定义 Agent 管理
├── tools/ # 工具系统(auto-discover via import.meta.glob)
│ ├── tool-registry.ts # ToolRegistry 核心
│ ├── toolsets.ts # 工具集定义(core/service/docs)
│ ├── memory.tool.ts
│ ├── catalog.tool.ts
│ ├── skill.tool.ts / skill-manage.tool.ts
│ ├── service.tool.ts / service-file.tool.ts / service-validate.tool.ts
│ ├── doc.tool.ts
│ ├── requirement.tool.ts
│ ├── session-search.tool.ts
│ ├── soul.tool.ts
│ ├── service-archive.tool.ts # service_archive / service_rollback
│ └── web-browser.tool.ts
├── host/
│ ├── service-container.vue # iframe 沙箱外壳
│ ├── bridge.ts # postMessage JSBridge
│ ├── registry.ts # 服务注册表
│ ├── network-bridge.ts # 网络中枢 + 全局门控
│ ├── network-session.ts
│ ├── service-share.ts # 局域网服务分享引擎
│ ├── skill-share.ts # 局域网技能分享引擎
│ ├── service-archive.ts # 服务版本归档引擎
│ ├── floating-widget-manager.ts
│ ├── floating-widget-container.vue
│ ├── widget-lifecycle.ts
│ ├── background-manager.ts # 后台服务运行时 + widget 全局 API 处理器
│ └── webview-overlay-state.ts # WebView 截图预览状态 + Tauri 事件监听
├── components/
│ └── WebviewOverlay.vue # 可拖拽 WebView 预览悬浮面板
└── pages/
├── HomePage.vue # 功能入口
├── ChatPage.vue # 流式 AI 对话
├── SettingsPage.vue # 标签页:通用 / 技能 & Agent / 数据
├── ServiceBrowsePage.vue # 服务浏览与管理
├── ShareDialog.vue # 局域网服务分享弹窗
├── SkillShareDialog.vue # 局域网技能分享弹窗
└── MemoryPage.vue # MEMORY.md / USER.md 编辑器
src-tauri/
├── Cargo.toml # Rust 依赖配置
├── tauri.conf.json # Tauri 窗口/打包配置
├── capabilities/ # 权限声明
├── src/
│ ├── main.rs # Rust 入口
│ ├── lib.rs # 插件注册 + AndroidJvm 状态缓存
│ ├── db.rs # SQLite FTS5 会话
│ ├── web.rs # WebView 浏览器引擎(三平台)
│ └── picker.rs # read_tombstone(Android JNI 崩溃诊断);pick_folder 已迁移至 tauri-plugin-android-fs
└── gen/android/ # Android 项目(Gradle)
└── app/src/main/java/com/amiba/desktop/
└── MainActivity.kt # Activity + JsCallback + WebViewHelper + FolderPickerHelper(已废弃)
- 在
src/pages/创建 Vue 组件 - 在
src/router/index.ts添加路由 - 在
App.vue的navItems添加入口
在 src/ai/skills.ts 的 builtinSkills 数组中添加:
{
name: '技能名',
description: '简要描述',
keywords: ['关键词1', '关键词2'],
template: `{ "manifest": {...}, "ui": {...}, "logic": "..." }`
}import { exportAndSaveZip } from '../ai/skill-zip'
await exportAndSaveZip('skill-slug')
// 桌面端弹出保存对话框,移动端/浏览器触发下载import { pickAndImportZip } from '../ai/skill-zip'
const slug = await pickAndImportZip()
// 桌面端弹出文件选择器,浏览器通过 <input type="file">
// ZIP 全程在内存中解析,不落盘import { importSkillFromUrl } from '../ai/skill-zip'
const slug = await importSkillFromUrl('https://example.com/skill.zip')
// fetch → ArrayBuffer → JSZip 解析 → 安装写入import { sendSkill, startReceivingSkills, stopReceivingSkills, acceptSkillShare, onSkillShareEvent } from '../host/skill-share'
// 发送技能到指定节点
await sendSkill('skill-slug', peerId)
// 监听接收(复用 service-share 的网络基础设施,service key: "amiba.skill-share")
await startReceivingSkills()
onSkillShareEvent((evt) => {
if (evt.event === 'request') { /* 显示确认对话框 */ }
if (evt.event === 'chunk-progress') { /* 显示进度 evt.percent */ }
if (evt.event === 'complete') { /* 安装完成 */ }
})
// 停止监听
await stopReceivingSkills()协议与局域网服务分享一致:64KB 分块传输,逐块 ACK 确认。接收端自动调用 installSkillPackage(pkg, 'overwrite') 安装。
interface SkillPackage {
formatVersion: 1
slug: string
manifest: SkillFrontmatter // name, description, version, keywords, platforms
body: string // SKILL.md Markdown 正文
files: Record<string, string> // 支持文件(相对路径 → 内容)
exportedAt: string
exportedFrom?: string
}ZIP 文件和局域网传输均使用此格式。导入时同名覆盖,不保留旧版本。
编辑 public/catalog/builtin_catalog.yaml,按现有格式添加。同时在 src/ai/generator.ts 的 renderNodeTree 和 generateStyles 中添加对应的 HTML/CSS 渲染逻辑。
npm test # 单元测试(Vitest)
npm run test:e2e # 端到端测试(Playwright)模板中使用 $t() 函数:
<template>
<p>{{ $t('app.title') }}</p>
<p>{{ $t('chat.stats.roundsLeft', { n: 5 }) }}</p>
</template>脚本中使用 useI18n() composable:
import { useI18n } from 'vue-i18n'
const { t } = useI18n()
alert(t('settings.confirm.deleteProvider', { name: 'example' }))- 在
src/i18n/types.ts的LocalesSchema中添加新 key - 在
src/i18n/locales/zh-CN.ts中添加中文值 - 在
src/i18n/locales/en.ts中添加英文值 - 两个文件的结构和 key 必须一致
settings.language (zh-CN / en) 变更会自动同步到 i18n.global.locale,无需手动干预。syncI18nWithSettings() 在 main.ts bootstrap 阶段通过 watch() 监听。
import { archiveService, rollbackService, listVersions } from '../host/service-archive'
// 归档当前服务状态
const result = await archiveService('user.my_service')
// → { label: 'v_2026-07-05T12-00-00-000Z', fileCount: 4 }
// 存储位置:services/{id}/.versions/{label}/// 回退到最新版本
const result = await rollbackService('user.my_service')
// 或指定版本
const result = await rollbackService('user.my_service', 'v_2026-07-01T08-00-00-000Z')
// → { label: 'v_...', fileCount: 4 }const versions = await listVersions('user.my_service')
// → [{ label, timestamp, fileCount }, ...]AI 通过 service_archive / service_rollback 工具自动调用上述引擎。
import { sendService } from '../host/service-share'
// 主动发送
await sendService('user.my_service', peerId)import { startReceiving, stopReceiving, acceptShare, declineShare, onShareEvent } from '../host/service-share'
// 开始监听(指定 serviceKey = "amiba.service-share")
await startReceiving()
// 订阅事件
onShareEvent((evt) => {
if (evt.event === 'request') {
// 弹出确认框,用户选择后调用 acceptShare() 或 declineShare()
}
if (evt.event === 'chunk-progress') {
// 显示进度 evt.percent
}
if (evt.event === 'complete') {
// 安装完成
}
})
// 停止监听
await stopReceiving()通过 NetworkSession 发送 JSON 消息,固定 serviceKey "amiba.service-share",大文件按 64KB 分块传输并逐块 ACK 确认。接收端自动调用 registerService + storeServicePackage 安装。
ShareDialog.vue 提供 UI,挂载于 ServiceBrowsePage.vue 头部 📡 按钮。包含"发送"和"等待接收"两个标签页。
- 服务 ID: 内置
system.xxx,用户user.yyy - 配置键: 全小写下划线
ai_base_url - API 方法: camelCase
setStorage - Vue 组件: PascalCase
- TS 模块: kebab-case
- Git 提交: 中文,
feat:fix:docs:refactor:
应用通过设置页「🔍 检查更新」按钮触发更新检查:
- 实现方式:纯前端检查 + Rust reqwest 下载,绕过浏览器 CORS
- 仓库地址:硬编码在
src/config/updater.ts的GITHUB_API常量中(当前:belowthetree/amiba) - 版本比较:
tag_name去v前缀后做 semver 三段式比较 - 版本来源:Tauri 运行时走
@tauri-apps/api/app.getVersion()(读tauri.conf.jsonversion),非 Tauri 环境降级到 Vite 注入的__APP_VERSION__(读package.jsonversion) - 平台资产匹配:按当前平台匹配 GitHub Release Assets(Windows →
.exe/.msi,macOS →.dmg,Linux →.AppImage/.deb,Android →.apk) - 下载:调用 Rust
download_file命令(基于 reqwest),走流式下载 +download-progress事件,前端展示进度条 - 安装:下载到临时目录后通过
@tauri-apps/plugin-opener.openPath()拉起系统默认安装程序 - 全平台统一:桌面 / Android / Web 同一套逻辑
tauri.conf.json的version字段为 Tauri 构建版本,getVersion()返回此值package.json的version为前端兜底版本,Vite 构建时注入__APP_VERSION__- 发布前应确保两处版本一致
- ❌ 不做 Flutter 原生 UI 渲染
- ❌ 不发明自定义 UI 协议
- ❌ 不做 Python 后端 AI 服务
- ❌ 不做第三方登录/支付(后续可加)
- ❌ 不做实时协作/多人
- ❌ 不做离线 AI(端侧模型,后续探索)
- 2026-07-07:
scanSkills()在第一次扫描后将结果缓存到模块级变量skillCommands,后续所有调用直接返回缓存而不重新扫描磁盘。导入新技能后调用scanSkills()会静默返回旧结果,导致 UI 不显示新技能。解决方案:导出invalidateSkillCache()函数(设置skillCommands = null; scanPromise = null),在installSkillPackage()和loadUserSkills()中写入/读取前调用。 - 2026-07-07: SettingsPage 的 IIFE 初始化块无条件调用
setVisibility({ lan: true })——每次进入设置页都会触发 UDP 广播重启。initNetworkBridge()已在应用启动时处理好 LAN 可见性初始化,设置页只需同步 UI 开关值,不应调setVisibility。 - 2026-07-05: 更新下载功能中的路径拼接使用了
${tempDir}amiba-update模板字符串,缺少路径分隔符。@tauri-apps/api/path的tempDir()返回不含尾部斜杠的路径,导致 Windows 上生成C:\Users\...\Tempamiba-update而非C:\Users\...\Temp\amiba-update。应始终使用join()函数进行跨平台路径拼接,切勿手动字符串拼接。同时 Rustdownload_file缺少 HTTP 状态码检查,下载 404/403 错误页会导致假成功——需在流式下载前检查response.status().is_success()。 - 2026-07-05: vue-i18n v11 语言切换不生效——
i18n.global.locale是WritableComputedRef<string>,必须用.value = lang赋值,直接对整个 ref 赋值 (= lang) 会丢弃 ref 对象,Vue 响应式无法感知变化。 - 2026-07-05:
getServicePackage会列出服务目录下所有文件(含隐藏目录),归档版本存储在当前服务目录的.versions/子目录下,需在getServicePackage的文件过滤中加入name.startsWith('.versions')避免把历史快照塞入服务包。 - 2026-07-05: ChatPage 输入框
width: 100%配合margin在 flex column 布局中会导致overflow: hidden裁切右侧边距。使用width: calc(100% - Npx)配合margin: auto同时实现居中+间距。 - 2026-07-05: 分享弹窗启动设备发现后,关闭弹窗时必须调用
stopDiscovery('lan')并清除定时器。仅清定时器不会停止 Rust 端 UDP 监听,日志将持续输出。 - 2026-07-07: 后台服务运行时采用隐藏 iframe 模式,复用已有 JSBridge 基础设施。隐藏 iframe 的
postMessage目标为window.parent(即主 window),而非 service-container。为防止后台 iframe 的 API 消息被前台 service-container 错误处理,bridge.ts的createBridge()中增加了event.source !== iframe.contentWindow检查。后台 iframe 的所有 API 调用(storage/notification/network/background)均由BackgroundServiceManager统一处理,使用worker.serviceId而非当前路由的serviceId确定调用上下文。 - 2026-07-08: 悬浮块 (widget) 与后台服务通信需要特殊处理。widget 通过
window.parent.postMessage()发送的消息直接到主 window,不受 service-container 的event.source过滤。解决方案:widget 注入window.__amiba_service_id__,background-manager 添加全局 listener 按serviceId路由。widget ← 后台方向:后台将状态写入storage,widget 通过setInterval轮询读取(postMessage无法直达 widget iframe)。 - 2026-07-08: 悬浮块尺寸应由服务通过
widget.json声明(width/height字段),而非自动检测。自动检测(ResizeObserver + postMessage)存在 body 未就绪、template literal 语法错误、多次注入点维护困难等问题。声明式尺寸更简单可控,宿主只做默认值兜底(width=280, height=200)。 - 2026-07-08: 预置服务统一使用
manifest.json与可导入包保持一致。index.json只做文件清单索引(id+files数组),不再承载元数据。installPrebuiltServices()从manifest.json解析 ServiceManifest。服务目录结构完全统一,不再有两种格式之分。 - 2026-07-17:
index.json中的条目必须与实际目录严格一致。曾因index.json提交了user.remote_control条目但目录未提交,导致每次启动安装器都 fetch 失败并跳过(静默日志)。新增预置服务时必须同提交目录 + index.json 条目;校验方法:遍历 index.json 的files逐一Test-Path。 - 2026-07-17: Windows PowerShell 5.1 校验 JSON 时,
Get-Content对无 BOM 的 UTF-8 文件默认按 GBK 解码,中文内容会乱码导致ConvertFrom-Json误报语法错误。应使用[System.IO.File]::ReadAllText($path, [System.Text.UTF8Encoding]::new($false))显式按 UTF-8 读取后再解析。 - 2026-07-08:
fileAccess授权模型:内存 Map 存储 grant(token → serviceId + path + pattern),不落盘,应用重启即失效。confirm()原生弹框确认,token 绑定 serviceId 实现多服务隔离。Tauri fs 插件权限名称为fs:allow-read(涵盖 read_file/read_dir)、fs:allow-read-file、fs:allow-read-text-file,带连字符的fs:allow-read-dir,注意与直觉的fs:allow-readdir区别。 - 2026-07-09: Android 平台播放器扫描音乐改为直接扫描根目录(
/storage/emulated/0/),无需用户手动选文件夹。实现要点:(1)FileAccessRequest新增silent字段,跳过confirm()弹窗(仅 path 已指定时生效);(2) 前端app.js通过navigator.userAgent检测 Android,自动调用requestAccess({ path, silent: true });(3)_matchesPattern支持**/前缀剥离,使**/{*.mp3,*.flac}可同时触发递归扫描 + 多扩展名匹配;(4) Taurifs:scope扩展/storage/emulated/0/**和/sdcard/**;(5) AndroidManifest 添加READ_EXTERNAL_STORAGE+READ_MEDIA_AUDIO权限。 - 2026-07-09: 多主题系统实现后发现所有页面 CSS 均为硬编码颜色(0 个
var()引用),切换暗色主题只改得了body背景。迁移策略:先在 App.vue:root定义 30 个 CSS 变量(含颜色/圆角/阴影/字体/间距的全集),再逐页将 ~260 处硬编码替换为var(--*)引用。迁移后的关键收益:暗色主题只需一份variables.json覆盖变量值即可全局生效。迁移注意点:shadow 变量要保持语义一致,tint 背景(如#E3F2FD)用--color-*-light系列变量替代而非直接用表面色变量,white在 primary 背景上保留不变但所有卡片/面板的white需迁移为--color-surface。 - 2026-07-17: Android 扫描文件夹失败是两层问题叠加。(1) 系统权限:Manifest 只声明
READ_EXTERNAL_STORAGE/READ_MEDIA_AUDIO不够——危险权限必须运行时请求(requestPermissions),且 Android 11+ Scoped Storage 下直接 POSIX 路径访问共享目录需要MANAGE_EXTERNAL_STORAGE(READ_EXTERNAL_STORAGE在 API 33+ 被系统忽略,READ_MEDIA_AUDIO仅对 MediaStore 有效)。修复:Manifest 加MANAGE_EXTERNAL_STORAGE,MainActivity.onCreate中 API<30 走requestPermissions,API≥30 检查Environment.isExternalStorageManager()未授权则跳Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION系统设置页。注意gen/android会被tauri android init重置,MainActivity 改动需重置后重新写入。(2) 前端递归 bug(全平台):_scanDir(原_listDirRecursive)曾用recursive: false调plugin:fs|read_dir后用!!entry.children判断目录——tauri-plugin-fs 的FileEntry.children仅在 recursive:true 时填充,非递归时目录永远被当文件、永不递归子目录。修复:单次read_dir调用(recursive 由 pattern 是否含**决定)+ 树遍历,避免逐目录多次 IPC。另:FileEntry无size字段,size ?? 0恒为 0(已知限制,获取真实大小需逐文件stat,扫描场景不划算)。 - 2026-07-19: Tauri Android WebView 中
window.__TAURI__全局变量不存在,导致pickFolder()误判为非 Tauri 环境直接返回null。应使用try { await import('@tauri-apps/api/core') }检测 Tauri 环境(与file-access-grants.ts、session-db.ts一致),而非依赖'__TAURI__' in window。(2026-07-20 更新:pickFolder已迁移至tauri-plugin-android-fs,通过动态import('tauri-plugin-android-fs-api')检测 Android 平台,不再依赖__TAURI__检测。) - 2026-07-19: Android 原生文件夹选取通过
ACTION_OPEN_DOCUMENT_TREE实现。架构:前端pickFolder()→ Rustpick_foldercommand(JNI)→ KotlinFolderPickerHelper.pickFolder()→ 主线程startActivityForResult→onActivityResult回调 →DocumentsContract.getTreeDocumentId()解析 content:// URI 为实际路径。路径不可转换时保留 content:// URI。关键细节:(1) 需要MainActivity实例引用(companion object 静态持有,onCreate设、onDestroy清);(2)CountDownLatch风格的synchronized+wait/notifyAll阻塞 Rust 线程等待用户选择;(3)takePersistableUriPermission确保持久权限;(4)find_app_class需通过ActivityThread.currentActivityThread()→getApplication().getClassLoader()加载 app 类(native 线程只有 system class loader)。(2026-07-20 废弃:整套 JNI 方案被tauri-plugin-android-fs替换,保留此记录作为反面教材。根因:同步阻塞 JNI 线程 + deprecatedstartActivityForResult+ Activity 生命周期不感知 → 实机闪退。详见下方 2026-07-20 条目。) - 2026-07-20: 文件夹选取从手写 JNI (
FolderPickerHelper) 迁移至tauri-plugin-android-fs(v28.4.0)。…(详见上方完整条目) - 2026-07-24:
web_fetchRust HTTP 路径(http_fetch)新增raw字段返回原始 HTML。此前只返回extract_text()提取的纯文本,导致RemoteServicesPage.safeFetch下载服务源文件时 HTML/JS 内容被剥离为纯文本。修复:FetchResultstruct 加raw: String,http_fetch存入原始 HTML;前端web-bridge.ts的FetchResult接口同步;RemoteServicesPage.safeFetch改用result.raw。注意 WebView 路径(桌面/移动端)暂不返回raw(值为空串),仅http_fetch有效。 - 2026-07-24: servicehub 目录中的服务从内置服务迁移时格式不规范——index.html 首行有
<!-- AMIBA_BRIDGE -->,packager 在<body>后又注入一个,造成双标记。容器replace只替换首个(DOCTYPE 前),bridge<script>注入到<!DOCTYPE html>之前触发浏览器怪异模式。修复:移除 5 个 index.html 首行标记(widget HTML 保留,不走 packager)。建立新规范:index.html 不得手动添加<!-- AMIBA_BRIDGE -->,统一由 packager 在<body>后注入。|迁移原因:(1) 原实现用synchronized(lock).wait(60000)同步阻塞 tokio worker 线程,旋转屏幕 / Activity 重建时必闪退;(2) deprecatedstartActivityForResult不感知生命周期,重建后回调丢失;(3) 手动unsafeJVM 裸指针 +Handler.post不可靠。新方案:AndroidXActivityResultRegistry异步回调 + Tauri PluginHandle 托管 JVM 生命周期,零阻塞。迁移改动:(a)picker.rs删除pick_foldercommand(-76 行),仅保留read_tombstone;(b)folder-picker.ts用动态import('tauri-plugin-android-fs-api')替代invoke('pick_folder');(c)file-access-grants.ts从基于 POSIX 路径的授权模型改为基于content://URI 的授权模型(+210 行改动)。注意:MainActivity.kt中的FolderPickerHelper代码未删除(避免下次tauri android init重置后被遗漏),但已无调用路径。