合并最新master分支

合入 origin/master 最新画布、外部接口与后端模块化改动。

按新队列语义解决画布测试和原生壳检查冲突。

把编辑器 Agent 迁移到模块化实现并适配 LlmRunRequest。

删除已废弃的旧编辑器 Agent 单文件与个人 Claude 本地配置。
This commit is contained in:
AIGameCreator App
2026-07-20 16:08:24 +08:00
443 changed files with 22979 additions and 9298 deletions
-20
View File
@@ -1,20 +0,0 @@
{
"permissions": {
"allow": [
"Bash(npm test:*)",
"Bash(netstat -ano)",
"Bash(npm run:*)",
"Bash(findstr :8081)",
"Bash(taskkill:*)",
"Bash(findstr LISTENING)",
"Bash(npx tsc:*)",
"Bash(lsof -ti:8081)",
"Bash(curl -s http://localhost:8081/health)",
"Bash(npm -v)",
"Bash(cargo --version)",
"Bash(rustc --version)",
"Skill(code-review)",
"Skill(code-review:*)"
]
}
}
@@ -318,6 +318,14 @@ For image edit/redraw that should replace an existing canvas layer, pass `projec
For sound effects and BGM, `assetFolderId` and `assetLabel` can write the generated audio to the account asset library, same as image/video generation.
## Successful Responses with Warnings
Character image generation (including character redraw through `kind: "character"`), icon spritesheet generation, and UI asset extraction can return HTTP 2xx with an optional structured `warning`. A 2xx response means the task completed, but it does not guarantee that every requested post-processed derivative exists.
- Apply the returned `project` and media snapshots before interpreting optional derivatives: character responses use `resource` / `asset`, while icon spritesheet and UI extraction responses use `spritesheetResource` / `spritesheetAsset`. When `warning.code` is `postprocess-failed-source-preserved`, the saved provider source image is the authoritative main result. Character output has no transparent derivative; icon spritesheet and UI extraction output have neither a transparent spritesheet nor slices. Display `warning.reason` directly, and do not synthesize missing derivatives or restart generation.
- `sliceWarning` is a separate condition used only when transparent spritesheet post-processing succeeded but automatic slicing failed. Keep `sliceWarning.reason` as the original diagnostic and continue using the complete transparent spritesheet; a UI may add context when displaying it, but must not rewrite the stored reason.
- The service contract keeps `warning` and `sliceWarning` mutually exclusive. As defensive handling for a malformed response containing both, treat the general `warning` as authoritative and do not misclassify the source-preserved result as a slicing-only warning.
## Guardrails
- Do not invent endpoints outside the OpenAPI, especially internal worker or runtime task-list routes.
@@ -78,6 +78,14 @@ Ask a follow-up only when two routes could both be correct and produce different
All generation requests should be placed into both the current canvas and its same-name asset-library folder. For endpoints that support `assetLabel`, pass it. For UI extraction, use `spritesheetLabel`. For icon spritesheet, the folder is enough. For character animation, the endpoint does not return `asset`; after success call `POST /api/external/v1/editor/assets` using the first returned frame as `imageSrc`, the session `assetFolderId`, and `assetKind: "character-animation"`.
## HTTP 2xx Warning Handling
Character image generation (including character redraw through `kind: "character"`), icon spritesheet generation, and UI asset extraction may return HTTP 2xx while carrying a structured `warning`; completion does not imply that all post-processed derivatives exist.
- Consume the returned `project` and media snapshots as authoritative: character responses use `resource` / `asset`, while icon spritesheet and UI extraction responses use `spritesheetResource` / `spritesheetAsset`. `warning.code: "postprocess-failed-source-preserved"` means the saved provider source is the main result. Character output has no transparent derivative, while icon spritesheet and UI extraction have no transparent spritesheet and no slices. Display `warning.reason` directly; do not construct missing assets or retry the provider generation from scratch.
- `sliceWarning` is only for a transparent spritesheet that was created successfully but could not be split automatically. Use the complete transparent spritesheet and preserve `sliceWarning.reason` as the original diagnostic; it is not a post-processing/source-preserved warning.
- The service contract keeps `warning` and `sliceWarning` mutually exclusive. If a malformed response contains both, prioritize the general `warning` over `sliceWarning` defensively.
## Reference Image Upload
If the user provides a local file as a reference image, run upload before the generation request:
+2 -1
View File
@@ -78,7 +78,7 @@ Best practices:
- Avoid overlapping queries that duplicate row delivery.
- Use indexes for subscribed filters.
## 2.2.0 to 2.6.0 Delta
## 2.2.0 to 2.6.1 Delta
Genarrative introduced SpacetimeDB around 2.2.0. Important changes since then:
@@ -88,6 +88,7 @@ Genarrative introduced SpacetimeDB around 2.2.0. Important changes since then:
- **2.4.1**: Rust and TypeScript procedural views can declare primary keys, enabling `OnUpdate` events for subscribed views; fixed index schema from ST tables.
- **2.5.0**: procedures are stable, C# procedural views gain primary keys, event tables allow broader layout-altering automigrations, BTreeSet storage makes row insertion deterministic and avoids accidentally quadratic bulk insert behavior, `wasm_memory_bytes` billing metric semantics changed, template version constraints unified, `publish --delete-data` config fallback fixed, CLI `call` accepts hex Identity arguments.
- **2.6.0**: procedural-view primary keys are available across Rust, TypeScript, and C#, commitlog gains `max_segment_size` / `write_buffer_size` / `preallocate_segments`, the default write buffer increases for throughput, event-table automigrations improve, and CLI binary distribution expands.
- **2.6.1**: procedure contexts again receive the caller `Identity` and `ConnectionId`; generated TypeScript `Option<T>` fields use optional keys; `spacetime init --template` lists available templates when no template argument is supplied.
## Debugging Checklist
+2
View File
@@ -29,6 +29,7 @@ GENARRATIVE_LLM_PROVIDER="ark"
GENARRATIVE_LLM_BASE_URL="https://ark.cn-beijing.volces.com/api/v3"
GENARRATIVE_LLM_API_KEY="eb750614-e0b5-402a-bfea-4224862d251e"
GENARRATIVE_LLM_MODEL="doubao-1-5-pro-32k-character-250715"
GENARRATIVE_EDITOR_BGFILTER_BASE_URL="https://u1082648-b442-cd409e05.westx.seetacloud.com:8443"
APIMART_BASE_URL="https://api.apimart.ai/v1"
APIMART_API_KEY=""
APIMART_IMAGE_REQUEST_TIMEOUT_MS=180000
@@ -36,6 +37,7 @@ DASHSCOPE_SCENE_IMAGE_MODEL="wan2.2-t2i-flash"
DASHSCOPE_REFERENCE_IMAGE_MODEL="qwen-image-2.0"
DASHSCOPE_COVER_IMAGE_MODEL="wan2.2-t2i-flash"
ARK_CHARACTER_VIDEO_REQUEST_TIMEOUT_MS=420000
# 启用服务端大模型调试日志(记录所有输入输出)
LLM_DEBUG_LOG="true"
+52
View File
@@ -24,6 +24,40 @@ module.exports = {
'no-console': 'off',
},
},
{
files: ['**/scripts/**/*.{ts,js,mjs,cjs}'],
rules: {
'no-console': 'off',
},
},
{
files: ['**/*.test.{ts,tsx,js,mjs,cjs}'],
rules: {
'no-console': 'off',
'unused-imports/no-unused-vars': 'off',
},
},
{
files: ['miniprogram/**/*.js'],
globals: {
App: 'readonly',
},
},
{
files: [
'apps/admin-web/src/pages/*.tsx',
'src/components/platform-entry/PlatformMobileHomeWelcomeDialog.tsx',
],
rules: {
'react-refresh/only-export-components': 'off',
},
},
{
files: ['src/components/platform-entry/PlatformEntryFlowShellImpl.tsx'],
rules: {
'react-hooks/exhaustive-deps': 'off',
},
},
{
files: ['src/components/game-canvas/**/*.tsx'],
rules: {
@@ -61,7 +95,25 @@ module.exports = {
'dist',
'dist_check',
'dist_check_monster_position',
'coverage',
'node_modules',
'server-rs/target',
'server-rs/target-*',
'apps/desktop-shell/src-tauri/target',
'target',
'src/components/CharacterAnimator.tsx',
'src/components/jump-hop-runtime/**',
'src/components/match3d-runtime/**',
'src/components/rpg-creation-editor/**',
'src/components/rpg-entry/**',
'src/components/rpg-runtime-shell/**',
'src/hooks/rpg-runtime-story/**',
'src/prompts/customWorldPrompts.ts',
'src/services/ai.ts',
'src/services/miniGameDraftGenerationProgress.ts',
'src/services/puzzle-clear/**',
'src/services/recommendedRuntimeGuestLaunch.test.ts',
'src/data/sceneEncounterPreviews.ts',
'public/Icons',
'media',
'.codex-logs',
+2 -1
View File
@@ -48,6 +48,7 @@ temp*build*/
/.app/
/target/
/logs
/.claude/settings.local.json
/.codegraph/
/.playwright-cli/
**/.playwright-cli/
@@ -57,7 +58,7 @@ temp*build*/
.worktrees/
.rag/
.env.secrets.local
.claude/settings.local.json
nohup.out
spacetime.local.json
deploy/container/api-server.env
deploy/container/worker-smoke/
+5 -6
View File
@@ -98,21 +98,20 @@ npm run check:content
主运行时:
- [src/App.tsx](./src/App.tsx)
- [src/components/GameShell.tsx](./src/components/GameShell.tsx)
- [src/AuthenticatedApp.tsx](./src/AuthenticatedApp.tsx)
- [src/routing/appRoutes.tsx](./src/routing/appRoutes.tsx)
- [src/hooks/useCombatFlow.ts](./src/hooks/useCombatFlow.ts)
- [src/hooks/useStoryGeneration.ts](./src/hooks/useStoryGeneration.ts)
主流程内嵌编辑能力:
- [src/components/CustomWorldEntityEditorModal.tsx](./src/components/CustomWorldEntityEditorModal.tsx)
- [src/components/CustomWorldNpcVisualEditor.tsx](./src/components/CustomWorldNpcVisualEditor.tsx)
- [src/components/CustomWorldRoleAssetStudioModal.tsx](./src/components/CustomWorldRoleAssetStudioModal.tsx)
- [src/components/image-editor/ImageCanvasEditorView.tsx](./src/components/image-editor/ImageCanvasEditorView.tsx)
- [src/components/rpg-creation-editor/RpgCreationEntityEditorModal.tsx](./src/components/rpg-creation-editor/RpgCreationEntityEditorModal.tsx)
- [src/components/rpg-creation-asset-studio/RpgCreationRoleAssetStudioModal.tsx](./src/components/rpg-creation-asset-studio/RpgCreationRoleAssetStudioModal.tsx)
核心数据:
- [src/data/scenePresets.ts](./src/data/scenePresets.ts)
- [src/data/characterPresets.ts](./src/data/characterPresets.ts)
- [src/data/monsterPresets.ts](./src/data/monsterPresets.ts)
- [src/data/npcInteractions.ts](./src/data/npcInteractions.ts)
- [src/data/treasureInteractions.ts](./src/data/treasureInteractions.ts)
+13 -13
View File
@@ -2,23 +2,21 @@ import type {
AdminAccountListResponse,
AdminCreateAccountRequest,
AdminCreateAccountResponse,
AdminUpsertCreationEntryEventBannersRequest,
AdminUpsertCreationEntryTypeConfigRequest,
AdminCreateEditorShowcaseCampaignImageUploadTicketRequest,
AdminCreateEditorShowcaseCampaignImageUploadTicketResponse,
AdminCreationEntryConfigResponse,
AdminDashboardQuery,
AdminDashboardResponse,
AdminDebugHttpRequest,
AdminDebugHttpResponse,
AdminDisableProfileRedeemCodeRequest,
AdminDisableProfileTaskConfigRequest,
AdminDatabaseTableListResponse,
AdminDatabaseTableRowsQuery,
AdminDatabaseTableRowsResponse,
AdminCreateEditorShowcaseCampaignImageUploadTicketRequest,
AdminCreateEditorShowcaseCampaignImageUploadTicketResponse,
AdminDebugHttpRequest,
AdminDebugHttpResponse,
AdminDirectUploadTicketPayload,
AdminDisableProfileRedeemCodeRequest,
AdminDisableProfileTaskConfigRequest,
AdminEditorAssetListQuery,
AdminEditorAssetListResponse,
AdminDirectUploadTicketPayload,
AdminEditorShowcaseAssetResponse,
AdminEditorShowcaseCampaignResponse,
AdminEditorShowcaseDisplayRequest,
@@ -37,14 +35,16 @@ import type {
AdminRechargeRefundPreviewRequest,
AdminRechargeRefundPreviewResponse,
AdminRechargeRefundRegisterRequest,
AdminTrackingEventListQuery,
AdminTrackingEventKeyListResponse,
AdminTrackingEventListQuery,
AdminTrackingEventListResponse,
AdminUpdateWorkVisibilityRequest,
AdminUpdateWorkVisibilityResponse,
AdminUpdateAccountRequest,
AdminUpdateAccountResponse,
AdminUpdateWorkVisibilityRequest,
AdminUpdateWorkVisibilityResponse,
AdminUploadedEditorShowcaseCampaignImage,
AdminUpsertCreationEntryEventBannersRequest,
AdminUpsertCreationEntryTypeConfigRequest,
AdminUpsertEditorShowcaseCampaignRequest,
AdminUpsertFeatureGateConfigRequest,
AdminUpsertProfileInviteCodeRequest,
@@ -53,11 +53,11 @@ import type {
AdminUpsertProfileTaskConfigRequest,
AdminUpsertProfileWalletConfigRequest,
AdminUpsertPublicWorkInteractionConfigRequest,
AdminWorkVisibilityListResponse,
AdminUserDetailQuery,
AdminUserDetailResponse,
AdminWalletRestrictionRequest,
AdminWalletRestrictionResponse,
AdminWorkVisibilityListResponse,
ApiErrorEnvelope,
ApiMeta,
ApiSuccessEnvelope,
+7 -7
View File
@@ -17,26 +17,25 @@ import {
getStoredAdminToken,
setStoredAdminToken,
} from '../auth/adminAuthStore';
import {AdminCreationEntrySwitchPage} from '../pages/AdminCreationEntrySwitchPage';
import {AdminAccountsPage} from '../pages/AdminAccountsPage';
import {AdminCreationEntrySwitchPage} from '../pages/AdminCreationEntrySwitchPage';
import {AdminDashboardPage} from '../pages/AdminDashboardPage';
import {AdminDebugHttpPage} from '../pages/AdminDebugHttpPage';
import {AdminDatabaseTablesPage} from '../pages/AdminDatabaseTablesPage';
import {AdminInviteCodePage} from '../pages/AdminInviteCodePage';
import {AdminLoginPage} from '../pages/AdminLoginPage';
import {AdminEditorGenerationPricingPage} from '../pages/AdminEditorGenerationPricingPage';
import {AdminDebugHttpPage} from '../pages/AdminDebugHttpPage';
import {AdminEditorAssetQueryPage} from '../pages/AdminEditorAssetQueryPage';
import {AdminEditorGenerationPricingPage} from '../pages/AdminEditorGenerationPricingPage';
import {AdminEditorShowcaseReviewPage} from '../pages/AdminEditorShowcaseReviewPage';
import {AdminGrayReleaseConfigPage} from '../pages/AdminGrayReleaseConfigPage';
import {AdminInviteCodePage} from '../pages/AdminInviteCodePage';
import {AdminLoginPage} from '../pages/AdminLoginPage';
import {AdminOverviewPage} from '../pages/AdminOverviewPage';
import {AdminProfileWalletConfigPage} from '../pages/AdminProfileWalletConfigPage';
import {AdminRechargeProductPage} from '../pages/AdminRechargeProductPage';
import {AdminRechargeOrderPage} from '../pages/AdminRechargeOrderPage';
import {AdminRechargeProductPage} from '../pages/AdminRechargeProductPage';
import {AdminRedeemCodePage} from '../pages/AdminRedeemCodePage';
import {AdminTaskConfigPage} from '../pages/AdminTaskConfigPage';
import {AdminTrackingEventsPage} from '../pages/AdminTrackingEventsPage';
import {AdminWorkVisibilityPage} from '../pages/AdminWorkVisibilityPage';
import {AdminShell} from './AdminShell';
import type {AdminRouteId} from './adminRoutes';
import {
getAccessibleAdminRoutes,
@@ -44,6 +43,7 @@ import {
resolveAdminRoute,
routeHash,
} from './adminRoutes';
import {AdminShell} from './AdminShell';
type SessionStatus = 'checking' | 'guest' | 'authenticated';
+9 -9
View File
@@ -1,25 +1,25 @@
import {
Activity,
Bug,
BadgeDollarSign,
Bug,
Coins,
LayoutDashboard,
LogOut,
Megaphone,
Database,
Eye,
GitBranch,
Images,
Star,
WalletCards,
ShieldCheck,
LayoutDashboard,
ListChecks,
LogOut,
Megaphone,
ReceiptText,
ShieldCheck,
SlidersHorizontal,
Database,
Star,
Table2,
TicketCheck,
TicketPercent,
ReceiptText,
Users,
WalletCards,
} from 'lucide-react';
import type {ReactNode} from 'react';
@@ -5,8 +5,8 @@ import {
adminTrackingEventDefinitions,
buildAdminTrackingEventKeyOptions,
filterAdminProfileTaskTrackingEventDefinitions,
filterAdminTrackingEventKeyOptions,
filterAdminTrackingEventDefinitions,
filterAdminTrackingEventKeyOptions,
findAdminTrackingEventDefinition,
} from './trackingEventDefinitions';
@@ -1,6 +1,7 @@
/* @vitest-environment jsdom */
import {
act,
fireEvent,
render,
screen,
@@ -308,6 +309,9 @@ test('Dashboard 四张趋势图共享横向日期窗口', async () => {
if (!sourceChart || !targetChart) {
throw new Error('趋势图未完整渲染');
}
await act(async () => {
await Promise.resolve();
});
setScrollableDimensions(sourceChart, {
clientWidth: 100,
scrollWidth: 500,
@@ -319,11 +323,11 @@ test('Dashboard 四张趋势图共享横向日期窗口', async () => {
scrollLeft: 0,
});
await act(async () => {
fireEvent.scroll(sourceChart);
await waitFor(() => {
expect(targetChart.scrollLeft).toBe(200);
});
expect(targetChart.scrollLeft).toBe(200);
});
test('Dashboard 访问人数趋势明确区分每日桶与时段去重总数', async () => {
@@ -2,12 +2,12 @@ import { ChevronDown, ChevronRight, Eye, FileText, RefreshCcw, X } from 'lucide-
import { Fragment, type ReactNode } from 'react';
import { useCallback, useEffect, useRef, useState } from 'react';
import type { AdminAssetReadUrlResponse } from '../api/adminApiClient';
import {
getAdminAssetReadUrl,
isAdminApiError,
listAdminEditorAssets,
} from '../api/adminApiClient';
import type { AdminAssetReadUrlResponse } from '../api/adminApiClient';
import type {
AdminEditorAssetListQuery,
AdminEditorAssetPayload,
@@ -2,6 +2,7 @@ import { Eye, FileText, RefreshCcw, Upload, X } from 'lucide-react';
import type { ReactNode } from 'react';
import { useEffect, useRef, useState } from 'react';
import type { AdminAssetReadUrlResponse } from '../api/adminApiClient';
import {
getAdminAssetReadUrl,
getAdminEditorShowcaseCampaign,
@@ -11,7 +12,6 @@ import {
uploadAdminEditorShowcaseCampaignImage,
upsertAdminEditorShowcaseCampaign,
} from '../api/adminApiClient';
import type { AdminAssetReadUrlResponse } from '../api/adminApiClient';
import type {
AdminEditorShowcaseAssetPayload,
AdminEditorShowcaseCampaignPayload,
@@ -10,13 +10,13 @@ import type {
AdminTrackingEventKeyPayload,
TrackingScopeKind,
} from '../api/adminApiTypes';
import { AdminUserReferenceButton } from '../components/AdminUserReferenceButton';
import {
buildAdminTrackingEventKeyOptions,
filterAdminTrackingEventKeyOptions,
filterAdminTrackingEventDefinitions,
filterAdminTrackingEventKeyOptions,
findAdminTrackingEventDefinition,
} from '../config/trackingEventDefinitions';
import { AdminUserReferenceButton } from '../components/AdminUserReferenceButton';
import { handlePageError } from './pageUtils';
interface AdminTrackingEventsPageProps {
+3 -7
View File
@@ -1,5 +1,5 @@
import fs from 'node:fs';
import { spawnSync } from 'node:child_process';
import fs from 'node:fs';
const configPath = new URL('../src-tauri/tauri.conf.json', import.meta.url);
const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
@@ -1117,7 +1117,7 @@ function extractDesktopCapabilities(source) {
}
function extractDesktopHandledMethods(source) {
const matchBodies = [...source.matchAll(/match request\.method\.as_str\(\) \{([\s\S]*?)\n \}/g)].map(
const matchBodies = [...source.matchAll(/match request\.method\.as_str\(\) \{([\s\S]*?)\n {4}\}/g)].map(
(match) => match[1],
);
if (matchBodies.length === 0) {
@@ -1152,7 +1152,7 @@ function extractDesktopHostBridgeMethodBody(source, method) {
const nextMethodMatch = source
.slice(methodStart + method.length)
.match(/\n (?:"[^"]+"|_)\s*=>/);
.match(/\n {8}(?:"[^"]+"|_)\s*=>/);
const nextMethodStart = nextMethodMatch
? methodStart + method.length + nextMethodMatch.index
: -1;
@@ -1410,10 +1410,6 @@ function extractTauriInvokeCommands(source) {
.filter(Boolean);
}
function collectRustSourceBasenames(files) {
return files.map((file) => file.pathname.split('/').pop()).sort();
}
function collectRustSourceRelativePaths(files) {
const rootPath = rustSourceDir.pathname.endsWith('/')
? rustSourceDir.pathname
@@ -1,6 +1,6 @@
import {spawnSync} from 'node:child_process';
import fs from 'node:fs';
import path from 'node:path';
import {spawnSync} from 'node:child_process';
const shellRoot = new URL('../', import.meta.url);
const repoRoot = path.resolve(shellRoot.pathname, '../..');
+4 -13
View File
@@ -1,5 +1,5 @@
import fs from 'node:fs';
import { spawnSync } from 'node:child_process';
import fs from 'node:fs';
import { PNG } from 'pngjs';
@@ -82,11 +82,6 @@ const filesPath = new URL('../src/host-bridge/files.ts', import.meta.url);
const filesSource = fs.readFileSync(filesPath, 'utf8');
const filesTestPath = new URL('../src/host-bridge/files.test.ts', import.meta.url);
const filesTestSource = fs.readFileSync(filesTestPath, 'utf8');
const filePayloadsPath = new URL(
'../src/host-bridge/filePayloads.ts',
import.meta.url,
);
const filePayloadsSource = fs.readFileSync(filePayloadsPath, 'utf8');
const hapticsPath = new URL('../src/host-bridge/haptics.ts', import.meta.url);
const hapticsSource = fs.readFileSync(hapticsPath, 'utf8');
const hapticsTestPath = new URL(
@@ -495,7 +490,7 @@ function extractNumberConstExport(source, exportName) {
function extractMobileBridgeHandledMethods(source) {
const match = source.match(
/async function dispatchMobileHostBridgeRequest[\s\S]*?switch \(request\.method\) \{([\s\S]*?)\n \}/,
/async function dispatchMobileHostBridgeRequest[\s\S]*?switch \(request\.method\) \{([\s\S]*?)\n {2}\}/,
);
if (!match) {
throw new Error('unable to read mobile shell HostBridge handler methods');
@@ -506,7 +501,7 @@ function extractMobileBridgeHandledMethods(source) {
function extractMobileBridgeUnsupportedMethods(source) {
const match = source.match(
/async function dispatchMobileHostBridgeRequest[\s\S]*?switch \(request\.method\) \{([\s\S]*?)\n \}/,
/async function dispatchMobileHostBridgeRequest[\s\S]*?switch \(request\.method\) \{([\s\S]*?)\n {2}\}/,
);
if (!match) {
throw new Error('unable to read mobile shell HostBridge unsupported methods');
@@ -514,7 +509,7 @@ function extractMobileBridgeUnsupportedMethods(source) {
const unsupportedMethods = new Set();
const casePattern =
/case '([^']+)':([\s\S]*?)(?=\n case '|\n default:|\n \})/g;
/case '([^']+)':([\s\S]*?)(?=\n {4}case '|\n {4}default:|\n {2}\})/g;
for (const entry of match[1].matchAll(casePattern)) {
if (entry[2].includes('unsupported(request.method)')) {
unsupportedMethods.add(entry[1]);
@@ -990,10 +985,6 @@ const sharedEvents = extractStringArrayExport(
sharedContractSource,
'HOST_BRIDGE_EVENTS',
);
const sharedBlockedDownloadProtocols = extractStringArrayExport(
sharedContractSource,
'HOST_BRIDGE_MOBILE_WEBVIEW_BLOCKED_DOWNLOAD_PROTOCOLS',
);
const sharedMobileBaseCapabilities = extractStringArrayExport(
sharedContractSource,
'HOST_BRIDGE_EXPO_MOBILE_BASE_CAPABILITIES',
@@ -1,5 +1,5 @@
import fs from 'node:fs';
import {spawnSync} from 'node:child_process';
import fs from 'node:fs';
const shellRoot = new URL('../', import.meta.url);
const easConfigPath = new URL('eas.json', shellRoot);
@@ -1,5 +1,4 @@
import {spawnSync} from 'node:child_process';
import fs from 'node:fs';
const appConfigPath = new URL('../app.json', import.meta.url);
@@ -1,5 +1,4 @@
import {spawnSync} from 'node:child_process';
import fs from 'node:fs';
const shellRoot = new URL('../', import.meta.url);
@@ -625,6 +625,8 @@ describe('handleMobileHostBridgeMessage', () => {
});
test('app.openExternalUrl 原生异常返回稳定 host_error', async () => {
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => undefined);
try {
vi.mocked(Linking.canOpenURL).mockRejectedValueOnce({
code: 'native_linking_failure',
message: 'native linking failed',
@@ -643,6 +645,12 @@ describe('handleMobileHostBridgeMessage', () => {
code: 'host_error',
message: 'external URL cannot be opened',
});
expect(warnSpy).toHaveBeenCalledWith(
'mobile HostBridge navigation failed for external.open',
);
} finally {
warnSpy.mockRestore();
}
});
test('app.openExternalUrl 拒绝危险协议', async () => {
+18 -18
View File
@@ -1,4 +1,10 @@
import { type HostBridgeRequest } from '../../../../packages/shared/src/contracts/hostBridge';
import { getMobileHostBridgeAppearanceColorScheme } from './appearance';
import { setMobileAppBadgeCount } from './badge';
import {
readMobileHostBridgeClipboardText,
writeMobileHostBridgeClipboardText,
} from './clipboard';
import {
captureMobileHostBridgeImageFile,
exportMobileHostBridgeAudioFile,
@@ -9,32 +15,26 @@ import {
importMobileHostBridgeImageFile,
importMobileHostBridgeTextFile,
} from './files';
import {
failure,
type MobileHostBridgeNavigation,
unsupported,
} from './protocol';
import { resetQrScannerForTest, scanMobileHostBridgeQrCode } from './scanner';
import {
openShare,
resetMobileHostBridgeShareTargetForTest,
setMobileHostBridgeShareTarget,
} from './share';
import { showMobileHostBridgeLocalNotification } from './notifications';
import {
readMobileHostBridgeClipboardText,
writeMobileHostBridgeClipboardText,
} from './clipboard';
import { runMobileHostBridgeHapticsImpact } from './haptics';
import { setMobileAppBadgeCount } from './badge';
import { getMobileHostBridgeAppearanceColorScheme } from './appearance';
import {
openMobileHostBridgeExternalUrl,
openMobileHostBridgeNativePage,
reloadMobileHostBridgeWebView,
} from './navigation';
import { getMobileHostBridgeNetworkStatus } from './network';
import { showMobileHostBridgeLocalNotification } from './notifications';
import {
failure,
type MobileHostBridgeNavigation,
unsupported,
} from './protocol';
import { getMobileHostBridgeRuntimeResponse } from './runtime';
import { resetQrScannerForTest, scanMobileHostBridgeQrCode } from './scanner';
import {
openShare,
resetMobileHostBridgeShareTargetForTest,
setMobileHostBridgeShareTarget,
} from './share';
let navigation: MobileHostBridgeNavigation | null = null;
@@ -2,15 +2,15 @@ import type * as ImagePicker from 'expo-image-picker';
import {
type FileImportImageResult,
type HostBridgeAudioMimeType,
type HostBridgeDocumentMimeType,
type HostBridgeImageMimeType,
type HostBridgeTextMimeType,
HOST_BRIDGE_AUDIO_MIME_TYPES,
HOST_BRIDGE_DOCUMENT_MIME_TYPES,
HOST_BRIDGE_IMAGE_MIME_TYPES,
HOST_BRIDGE_IMPORT_IMAGE_MAX_BYTES,
HOST_BRIDGE_TEXT_MIME_TYPES,
type HostBridgeAudioMimeType,
type HostBridgeDocumentMimeType,
type HostBridgeImageMimeType,
type HostBridgeTextMimeType,
normalizeHostBridgeExportFileName,
normalizeHostBridgeImportFileName,
} from '../../../../packages/shared/src/contracts/hostBridge';

Some files were not shown because too many files have changed in this diff Show More