收紧原生壳桥接权限边界

移动壳 HostBridge 协议名和版本改为共享契约常量

移动壳配置检查锁定主动导航和 deep link 宿主上下文补写

桌面壳主窗口 capability 移除 Tauri core 默认权限

桌面壳配置检查拒绝 core 默认权限和插件权限外露

更新原生壳方案与共享决策记录
This commit is contained in:
2026-06-18 23:52:40 +08:00
parent 04d2498579
commit 9eee997731
10 changed files with 173 additions and 16 deletions
+46 -3
View File
@@ -1063,9 +1063,40 @@ for (const capability of sdkBackedCapabilities) {
}
const allowedPermissions = [
'core:default',
'allow-host-bridge-request',
];
const blockedCoreDefaultPermissions = [
'core:default',
'core:app:default',
'core:event:default',
'core:image:default',
'core:menu:default',
'core:path:default',
'core:resources:default',
'core:tray:default',
'core:webview:default',
'core:window:default',
];
const blockedCorePermissionPrefixes = [
'core:app:',
'core:event:',
'core:image:',
'core:menu:',
'core:path:',
'core:resources:',
'core:tray:',
'core:webview:',
'core:window:',
];
const blockedPluginPermissionPrefixes = [
'clipboard-manager:',
'deep-link:',
'dialog:',
'fs:',
'notification:',
'opener:',
'window-state:',
];
const sharedTauriCommand = extractTsStringConst(
sharedContractSource,
'HOST_BRIDGE_TAURI_COMMAND',
@@ -1300,8 +1331,20 @@ assertSameList(
allowedPermissions,
'desktop shell capability permissions',
);
if ((capability.permissions ?? []).some((permission) => permission.startsWith('notification:'))) {
throw new Error('desktop shell must not expose notification plugin commands to H5');
for (const permission of capability.permissions ?? []) {
if (blockedCoreDefaultPermissions.includes(permission)) {
throw new Error(`desktop shell must not expose ${permission} to H5`);
}
if (
blockedCorePermissionPrefixes.some((prefix) => permission.startsWith(prefix))
) {
throw new Error(`desktop shell must not expose Tauri core permission ${permission} to H5`);
}
if (
blockedPluginPermissionPrefixes.some((prefix) => permission.startsWith(prefix))
) {
throw new Error(`desktop shell must not expose plugin permission ${permission} to H5`);
}
}
assertSameList(
extractTauriBuildCommands(buildScript),
@@ -4,7 +4,6 @@
"description": "主窗口只开放 Genarrative 桌面宿主壳需要的受控命令。",
"windows": ["main"],
"permissions": [
"core:default",
"allow-host-bridge-request"
]
}
+74 -2
View File
@@ -28,6 +28,10 @@ const hostBridgeSource = bridgeSourceFiles
.join('\n');
const urlPath = new URL('../src/shell/url.ts', import.meta.url);
const urlSource = fs.readFileSync(urlPath, 'utf8');
const deepLinkPath = new URL('../src/shell/deepLink.ts', import.meta.url);
const deepLinkSource = fs.readFileSync(deepLinkPath, 'utf8');
const navigationPath = new URL('../src/shell/navigation.ts', import.meta.url);
const navigationSource = fs.readFileSync(navigationPath, 'utf8');
const webViewPolicyPath = new URL('../src/shell/webViewPolicy.ts', import.meta.url);
const webViewPolicySource = fs.readFileSync(webViewPolicyPath, 'utf8');
const webViewHistoryPath = new URL('../src/shell/webViewHistory.ts', import.meta.url);
@@ -167,6 +171,17 @@ function extractStringConstExport(source, exportName) {
return match[1];
}
function extractNumberConstExport(source, exportName) {
const match = source.match(
new RegExp(`export const ${exportName}\\s*=\\s*(\\d+);`),
);
if (!match) {
throw new Error(`unable to read ${exportName}`);
}
return Number(match[1]);
}
function extractMobileBridgeHandledMethods(source) {
const match = source.match(
/async function dispatchMobileHostBridgeRequest[\s\S]*?switch \(request\.method\) \{([\s\S]*?)\n \}/,
@@ -464,6 +479,14 @@ const sharedMethods = extractStringArrayExport(
sharedContractSource,
'HOST_BRIDGE_METHODS',
);
const sharedHostBridgeProtocol = extractStringConstExport(
sharedContractSource,
'HOST_BRIDGE_PROTOCOL',
);
const sharedHostBridgeVersion = extractNumberConstExport(
sharedContractSource,
'HOST_BRIDGE_VERSION',
);
const handledMobileMethods = extractMobileBridgeHandledMethods(dispatchSource);
const mobileCapabilities = extractStringArrayExport(
hostBridgeSource,
@@ -709,8 +732,8 @@ if (
throw new Error('mobile shell Android app link data must only bind https://app.genarrative.world');
}
if (appConfig.extra?.genarrativeHostBridgeVersion !== 1) {
throw new Error('mobile shell extra HostBridge version must be 1');
if (appConfig.extra?.genarrativeHostBridgeVersion !== sharedHostBridgeVersion) {
throw new Error('mobile shell extra HostBridge version must match shared HostBridge version');
}
for (const snippet of [
@@ -718,6 +741,8 @@ for (const snippet of [
"Linking.addEventListener('url'",
'buildMobileShellUrlFromDeepLink',
'configureMobileHostBridgeNavigation',
'HOST_BRIDGE_PROTOCOL',
'HOST_BRIDGE_VERSION',
'shouldAcceptMobileShellHostBridgeMessage',
'webViewRef.current?.reload()',
'const reloadCurrentWebView = useCallback(() => {',
@@ -876,6 +901,40 @@ for (const snippet of [
}
}
for (const snippet of [
'buildMobileShellUrl(',
'HOST_BRIDGE_VERSION.toString()',
"url.searchParams.set('clientRuntime', 'native_app')",
"url.searchParams.set('hostShell', 'expo_mobile')",
"url.searchParams.set('hostPlatform', options.platform)",
"url.searchParams.set('bridgeVersion', HOST_BRIDGE_VERSION.toString())",
"url.searchParams.set('hostCapabilities', options.capabilities.join(','))",
]) {
if (!urlSource.includes(snippet)) {
throw new Error(`mobile shell host-context URL builder missing ${snippet}`);
}
}
for (const snippet of [
'resolveMobileShellBaseWebUrl(baseWebUrl)',
'resolveTargetPath(rawUrl, webOrigin)',
'buildMobileShellUrl(new URL(targetPath, webOrigin).toString(), options)',
]) {
if (!deepLinkSource.includes(snippet)) {
throw new Error(`mobile shell deep link host-context flow missing ${snippet}`);
}
}
for (const snippet of [
'resolveMobileShellWebViewUrl',
'shouldOpenInMobileShellWebView(rawUrl, allowedOrigin)',
'new URL(rawUrl, allowedOrigin).toString()',
]) {
if (!navigationSource.includes(snippet)) {
throw new Error(`mobile shell native-page navigation policy missing ${snippet}`);
}
}
if (shellAppSource.includes('127.0.0.1:3000')) {
throw new Error(
'mobile shell ShellApp must not hard-code localhost as the default H5 URL',
@@ -1014,6 +1073,7 @@ for (const snippet of [
'inFlightHostBridgeResponses',
'resolveMobileHostBridgeResponse',
'rememberHostBridgeResponse',
'buildMobileShellUrl(webViewUrl, navigation.urlOptions)',
]) {
if (!hostBridgeSource.includes(snippet)) {
throw new Error(`mobile shell HostBridge missing ${snippet}`);
@@ -1044,6 +1104,18 @@ if (
throw new Error('mobile shell HostBridge version must not be duplicated in app or bridge source');
}
if (shellAppSource.includes(`bridge: '${sharedHostBridgeProtocol}'`)) {
throw new Error('mobile shell event injection must use HOST_BRIDGE_PROTOCOL');
}
if (shellAppSource.includes(`version: ${sharedHostBridgeVersion}`)) {
throw new Error('mobile shell event injection must use HOST_BRIDGE_VERSION');
}
if (urlSource.includes(`bridgeVersion', '${sharedHostBridgeVersion}'`)) {
throw new Error('mobile shell URL builder must use HOST_BRIDGE_VERSION');
}
for (const capability of sdkBackedCapabilities) {
if (
shellAppSource.includes(`'${capability}'`) ||
@@ -4,10 +4,15 @@ import fs from 'node:fs';
const appConfigPath = new URL('../app.json', import.meta.url);
const packagePath = new URL('../package.json', import.meta.url);
const sharedContractPath = new URL(
'../../../packages/shared/src/contracts/hostBridge.ts',
import.meta.url,
);
const shellRoot = new URL('../', import.meta.url);
const appConfig = JSON.parse(fs.readFileSync(appConfigPath, 'utf8')).expo;
const packageConfig = JSON.parse(fs.readFileSync(packagePath, 'utf8'));
const sharedContractSource = fs.readFileSync(sharedContractPath, 'utf8');
const npmCommand = process.platform === 'win32' ? 'npm.cmd' : 'npm';
const result = spawnSync(
@@ -81,6 +86,22 @@ function findPlugin(name) {
);
}
function extractNumberConstExport(source, exportName) {
const match = source.match(
new RegExp(`export const ${exportName}\\s*=\\s*(\\d+);`),
);
if (!match) {
throw new Error(`unable to read ${exportName}`);
}
return Number(match[1]);
}
const sharedHostBridgeVersion = extractNumberConstExport(
sharedContractSource,
'HOST_BRIDGE_VERSION',
);
assertEqual(expoConfig.name, 'Genarrative', 'name');
assertEqual(expoConfig.slug, 'genarrative-mobile-shell', 'slug');
assertEqual(expoConfig.scheme, 'genarrative', 'scheme');
@@ -105,7 +126,7 @@ if ('releaseChannel' in expoConfig || 'channel' in expoConfig) {
}
assertEqual(
expoConfig.extra?.genarrativeHostBridgeVersion,
1,
sharedHostBridgeVersion,
'HostBridge version',
);
+6 -2
View File
@@ -15,6 +15,10 @@ import { SafeAreaProvider, SafeAreaView } from 'react-native-safe-area-context';
import type { WebViewMessageEvent } from 'react-native-webview';
import { WebView } from 'react-native-webview';
import {
HOST_BRIDGE_PROTOCOL,
HOST_BRIDGE_VERSION,
} from '../../../../packages/shared/src/contracts/hostBridge';
import {
configureMobileHostBridgeNavigation,
handleMobileHostBridgeMessage,
@@ -97,8 +101,8 @@ export default function ShellApp() {
const injectHostBridgeEvent = useCallback((event: string, payload: unknown) => {
webViewRef.current?.injectJavaScript(
buildHostBridgeMessageScript({
bridge: 'GenarrativeHostBridge',
version: 1,
bridge: HOST_BRIDGE_PROTOCOL,
version: HOST_BRIDGE_VERSION,
event,
payload,
}),
+4 -1
View File
@@ -1,5 +1,6 @@
import { describe, expect, test } from 'vitest';
import { HOST_BRIDGE_VERSION } from '../../../../packages/shared/src/contracts/hostBridge';
import {
DEFAULT_MOBILE_SHELL_WEB_URL,
buildMobileShellUrl,
@@ -25,7 +26,9 @@ describe('buildMobileShellUrl', () => {
expect(url.searchParams.get('hostShell')).toBe('expo_mobile');
expect(url.searchParams.get('hostPlatform')).toBe('ios');
expect(url.searchParams.get('hostVersion')).toBe('0.1.0');
expect(url.searchParams.get('bridgeVersion')).toBe('1');
expect(url.searchParams.get('bridgeVersion')).toBe(
HOST_BRIDGE_VERSION.toString(),
);
expect(url.searchParams.get('hostCapabilities')).toBe(
'host.getRuntime,share.open',
);
+5 -4
View File
@@ -1,6 +1,7 @@
import type {
HostBridgeCapability,
NativeHostPlatform,
import {
HOST_BRIDGE_VERSION,
type HostBridgeCapability,
type NativeHostPlatform,
} from '../../../../packages/shared/src/contracts/hostBridge';
export type MobileShellUrlOptions = {
@@ -58,7 +59,7 @@ export function buildMobileShellUrl(
url.searchParams.set('hostShell', 'expo_mobile');
url.searchParams.set('hostPlatform', options.platform);
url.searchParams.set('hostVersion', options.hostVersion);
url.searchParams.set('bridgeVersion', '1');
url.searchParams.set('bridgeVersion', HOST_BRIDGE_VERSION.toString());
url.searchParams.set('hostCapabilities', options.capabilities.join(','));
return url.toString();
}
@@ -1,5 +1,9 @@
import { describe, expect, test } from 'vitest';
import {
HOST_BRIDGE_PROTOCOL,
HOST_BRIDGE_VERSION,
} from '../../../../packages/shared/src/contracts/hostBridge';
import { parseMobileWebViewHistoryStateMessage } from './webViewHistory';
describe('parseMobileWebViewHistoryStateMessage', () => {
@@ -31,8 +35,8 @@ describe('parseMobileWebViewHistoryStateMessage', () => {
expect(
parseMobileWebViewHistoryStateMessage(
JSON.stringify({
bridge: 'GenarrativeHostBridge',
version: 1,
bridge: HOST_BRIDGE_PROTOCOL,
version: HOST_BRIDGE_VERSION,
method: 'host.getRuntime',
}),
),
@@ -51,6 +51,8 @@
- 2026-06-18 移动壳键盘布局:Expo Android 壳 `softwareKeyboardLayoutMode` 固定为 `resize`,让系统键盘打开时真实调整 WebView 可视高度;H5 继续使用已有 viewport / 输入法聚焦适配承接创作表单、聊天输入和玩法输入框,壳层不新增键盘遮挡补偿 UI、不伪造键盘状态。移动壳配置检查和 Expo public config smoke 会拒绝该字段缺失或漂移。
- 2026-06-18 移动壳媒体策略:Expo WebView 允许内联媒体播放和用户触发的全屏视频,但保留 `mediaPlaybackRequiresUserAction`,不允许无手势自动播放;固定玩法和 AI H5 sandbox 的音频仍由 H5 用户开关、运行态状态和宿主生命周期控制,壳层不注入额外播放器或假播放状态。移动壳配置检查会拒绝 WebView 媒体策略漂移。
- 2026-06-18 移动壳启动 URL 归一:Expo 壳的 `EXPO_PUBLIC_GENARRATIVE_WEB_URL` 和 deep link 基准地址只接受生产主站 `https://app.genarrative.world`,以及本机开发联调 `http://127.0.0.1``http://localhost``http://[::1]`;空值、相对路径、外域、`file:``javascript:` 等非法配置回退到默认 H5 地址后再附加 `native_app` 宿主上下文;deep link 仍只映射归一后基准 origin 的 H5 路径,禁止把外域或危险协议页面装进带完整 HostBridge 的 WebView。
- 2026-06-18 移动壳主动导航上下文:Expo 壳的 `navigation.openNativePage` 与 deep link 都必须复用 `buildMobileShellUrl(...)` 补写 `native_app``expo_mobile`、真实平台、版本和 capability 清单;受控导航只接受当前允许 origin 的同源 H5 URL。移动壳配置检查会拒绝主动导航或 deep link 绕过该宿主上下文构造入口。
- 2026-06-18 移动壳协议常量来源:Expo 壳的 HostBridge 事件注入、入口 URL `bridgeVersion``host.getRuntime` 回包和 Expo public config smoke 必须使用 `packages/shared/src/contracts/hostBridge.ts``HOST_BRIDGE_PROTOCOL` / `HOST_BRIDGE_VERSION`,不得在壳层重新写死协议名或版本字面量;配置检查会拒绝这些常量漂移。
- 2026-06-18 移动壳默认入口:Expo 壳默认 H5 地址固定为 `https://app.genarrative.world/`,开发联调本机 Vite 必须显式设置 `EXPO_PUBLIC_GENARRATIVE_WEB_URL=http://127.0.0.1:3000/``http://localhost:3000/``http://[::1]:3000/`;生产包不得在未配置环境变量时加载设备本机 localhost,也不得通过环境变量把第三方外域 H5 放入带完整 HostBridge 的 WebView。
- 2026-06-18 移动壳安装包身份:Expo 移动壳的 iOS bundle identifier 与 Android package 统一固定为 `world.genarrative.mobile`,应用版本固定为 `0.1.0`iOS `buildNumber` 从字符串 `"1"` 起步,Android `versionCode` 从整数 `1` 起步;后续分发安装包时递增构建号 / versionCode,产品版本号按发布节奏调整。移动壳配置检查会校验 `app.json``package.json` 版本一致,并拒绝缺失或漂移的包标识,当前不写入假商店元数据、假更新端点或占位渠道 SDK 配置。
- 2026-06-18 移动壳 HostBridge 版本单一来源:Expo 移动壳的 H5 入口 query 和 `host.getRuntime` 回包都读取 `MOBILE_SHELL_HOST_VERSION`,该常量必须与 `app.json` / `package.json` 版本一致;配置检查会拒绝 `App.tsx``bridge.ts` 重新散落硬编码版本,避免安装包版本升级时 H5 首屏上下文与 runtime 回读分叉。
@@ -77,6 +79,7 @@
- 2026-06-18 移动壳文件 bytes 校验:Expo 图片 / 音频导入导出不得只信系统 picker 返回 MIME、文件扩展名或 H5 声明 MIME;移动壳必须识别 PNG / JPEG / WebP、MP3 / MP4-M4A / WAV / OGG / WebM base64 bytes 头部,要求导入 MIME 归一结果与真实 bytes 匹配,导出 payload 的 `mimeType``base64Data` 解码 bytes 匹配。不匹配返回 `invalid_request`,不会写入缓存文件、调起系统分享或把内容回传给 H5。移动图片导出还必须按 MIME 给系统分享 / 保存面板补齐 `.png` / `.jpg` / `.webp` 文件名扩展,避免缓存文件名与真实图片类型漂移。配置检查和移动壳测试覆盖该边界。
- 2026-06-18 桌面壳 DevTools 边界:Tauri 主 WebView 配置必须显式 `devtools=false`Cargo 依赖不得启用 Tauri `devtools` feature;桌面壳本地调试走普通浏览器和 Vite,不把 debug / release 桌面包变成可打开浏览器检查器的调试容器。配置检查会拒绝主窗口 DevTools 或 release feature 被重新打开。
- 2026-06-18 桌面壳 Tauri 命令白名单:桌面壳源码、Tauri build manifest、主窗口 capability 和本地自动生成权限目录都只能暴露 `host_bridge_request` 一个受控 command;所有桌面能力继续在 Rust 内部按 HostBridge method 白名单分发,不新增可被 H5 直接 `invoke` 的 Tauri command,也不授予插件 JS guest API。检查脚本会拒绝多余 command、权限列表顺序漂移和残留的自动生成权限文件。
- 2026-06-18 桌面壳 capability 最小化:Tauri 主窗口 capability 只授予 `allow-host-bridge-request`,不得授予 `core:default``core:*:default`、任意 core 子权限或 dialog / fs / notification / opener / clipboard / deep-link / window-state 等插件权限。窗口、菜单、托盘、剪贴板、文件、通知和外链能力只能由 Rust 壳内部调用,再经 `host_bridge_request` 分发。
- 2026-06-18 HostBridge request id replayExpo 和 Tauri 壳都必须按 request id 回放首次完成结果;同 id 进行中的请求共享同一执行结果,已完成请求直接回放缓存响应,避免系统分享、外链、剪贴板、文件选择 / 保存、本地通知、窗口导航等宿主副作用被重复触发。两端配置检查和测试会锁住 replay 结构。
- 2026-06-18 HostBridge request envelope 校验:共享契约提供 `isHostBridgeMethod``normalizeHostBridgeRequestId`,Expo 壳直接复用,Tauri 壳镜像同一白名单和 id 规则;空 id、控制字符 id、超长 id 和未知 method 都必须在 replay / 能力分发前返回 `invalid_request`,已知但当前壳未实现的登录 / 支付等 method 才返回 `unsupported_method`
- 2026-06-18 HostBridge method 白名单跨壳门禁:`packages/shared/src/contracts/hostBridge.ts``HOST_BRIDGE_METHODS` 是唯一协议来源;Expo 壳 HostBridge 分发不得处理共享契约外 methodTauri 壳 Rust `HOST_BRIDGE_METHODS` 必须与共享契约逐项一致。新增宿主 method 必须先更新共享契约,再落两端壳实现或明确 unsupported。
@@ -182,6 +182,7 @@ Tauri 壳同样只负责桌面宿主能力,不承接玩法业务。
- H5 通过 `window.__TAURI__.core.invoke('host_bridge_request', request)` 或后续封装的 `nativeAppHostBridge` 调用桌面能力。
- Rust 侧只暴露一个受控 `host_bridge_request` command,再在 Rust 内部按 method 白名单分发。
- Tauri capabilities 只授予主窗口所需命令;默认不开放文件系统、shell、全局剪贴板或任意插件能力。
- 主窗口 capability 只授予 `allow-host-bridge-request`;不得使用 `core:default``core:*:default` 或插件 command 权限作为兜底。窗口、菜单、托盘、剪贴板、文件、通知和外链能力只能由 Rust 壳内部调用,再通过 `host_bridge_request` 的 HostBridge method 白名单分发给 H5。
- 桌面支付首期走现有 H5 / 二维码 / 外部浏览器路径,不在 Rust 侧保存支付凭据。
- 文件导出、作品卡保存、图片拖拽导入、应用菜单、系统托盘、自动更新等桌面能力按后续需求逐项开放;其中应用菜单和系统托盘属于桌面壳自有能力,不作为 HostBridge method 暴露给 H5。
- 主 WebView 默认拒绝网页自动下载或 `<a download>` 触发的落盘动作;用户保存文本、图片、音频等内容必须走已声明的 `file.exportText``file.exportImage``file.exportAudio` HostBridge method,由 Rust 侧执行 MIME、大小、文件名清洗和系统保存对话框确认。
@@ -319,6 +320,10 @@ GameBridge 禁止:
2026-06-18 追加:移动壳启动 H5 URL 增加宿主侧归一。`EXPO_PUBLIC_GENARRATIVE_WEB_URL` 和 deep link 基准地址只接受生产主站 `https://app.genarrative.world`,以及本机开发联调的 `http://127.0.0.1``http://localhost``http://[::1]` 入口;空值、相对路径、外域、`file:``javascript:` 等非法配置统一回退到默认 H5 地址,再附加 `native_app` 宿主上下文。deep link 仍只允许映射到归一后基准 origin 的 H5 路径,避免外域页面被装进带完整 HostBridge 的 WebView。
2026-06-18 追加:移动壳 `navigation.openNativePage` 与 deep link 共享同一宿主上下文补写口径。受控主动导航先解析为当前允许 origin 的同源 H5 URL,再通过 `buildMobileShellUrl(...)` 重新附加 `clientRuntime=native_app``hostShell=expo_mobile`、当前平台、版本和真实 capability 清单;deep link 同样在同源路径归一后复用该构造函数。`apps/mobile-shell/scripts/check-config.mjs` 会拒绝主动导航或 deep link 绕过宿主上下文构造入口,避免新页面按普通浏览器运行态启动。
2026-06-18 追加:移动壳 HostBridge 协议名和协议版本统一从 `packages/shared/src/contracts/hostBridge.ts``HOST_BRIDGE_PROTOCOL` / `HOST_BRIDGE_VERSION` 读取。Expo 入口 query、WebView 事件注入、`host.getRuntime` 回包和 Expo public config smoke 都必须反查共享常量;移动壳配置检查会拒绝重新写死 `GenarrativeHostBridge` 或字面量版本。
2026-06-18 追加:移动壳默认 H5 地址固定为 `https://app.genarrative.world/`。开发联调如需加载本机 Vite,必须显式设置 `EXPO_PUBLIC_GENARRATIVE_WEB_URL=http://127.0.0.1:3000/``http://localhost:3000/``http://[::1]:3000/`;生产包不得在未配置环境变量时默认加载设备本机 localhost,也不得通过环境变量把第三方外域 H5 放入带完整 HostBridge 的 WebView。
2026-06-18 追加:移动壳系统深链声明固定为生产主站唯一入口。iOS `associatedDomains` 只能包含 `applinks:app.genarrative.world`Android `intentFilters` 只能存在一个 `VIEW` / `autoVerify=true` 的 App Link 过滤器,category 只能是 `BROWSABLE``DEFAULT`data 只能绑定 `https://app.genarrative.world`,不得额外声明外域、明文协议、pathPattern 或其它可接管范围。实际 deep link 解析仍由壳层把同源路径归一后附加 HostBridge 上下文,非法来源回退默认首页。
@@ -348,6 +353,8 @@ GameBridge 禁止:
2026-06-18 追加:桌面壳当前真实能力完整清单为 `host.getRuntime``appearance.getColorScheme``host.events``app.lifecycle``share.open``share.setTarget``navigation.openNativePage``navigation.canGoBack``app.reloadWebView``app.openExternalUrl``app.setTitle``app.setBadgeCount``network.status``network.statusChanged``clipboard.writeText``clipboard.readText``file.exportText``file.importText``file.exportImage``file.importImage``file.importAudio``file.exportAudio``file.imageDropped``notification.showLocal``npm run check:native-shells` 会从 Rust capability 清单反查本段,避免实现、入口 URL 和方案文档再次漂移。
2026-06-18 追加:桌面壳主窗口 capability 最小化为 `allow-host-bridge-request`。Tauri `core:default` 会同时包含 event、window、webview、menu、tray 等默认命令集合,其中 webview 默认集包含 DevTools 内部切换命令;当前壳不再授予这些 core / plugin JS 权限。`apps/desktop-shell/scripts/check-config.mjs` 会拒绝 `core:default``core:*:default`、任意 core 子权限和 dialog / fs / notification / opener / clipboard / deep-link / window-state 等插件权限进入主窗口 capability。
2026-06-18 追加:桌面壳的网络状态同样只通过 HostBridge 暴露给 H5。H5 外部生成队列概览在桌面壳明确离线或不可达时暂停轮询,窗口恢复在线后重新刷新;Tauri 仍不开放任意网络探测、shell 或文件系统能力给主站。
2026-06-18 追加:桌面壳声明并实现 `file.importText`,通过系统文件选择框读取用户选择的文本文件,只接受 `text/plain``text/markdown``text/csv``application/json` 对应扩展名,单次不超过 5 MiB;成功只返回清洗后的文件名、MIME、UTF-8 文本内容和字节数,不暴露本机绝对路径,也不开放通用文件系统。H5 创作 Agent 工作台复用同一 HostBridge 文本导入入口和后端解析接口;普通浏览器、小程序和未声明该能力的裁剪壳继续保留原 `<input type="file">` 路径。