fix(release): harden apple app store distribution
This commit is contained in:
parent
2b534ce845
commit
144428f054
9
Makefile
9
Makefile
@ -6,6 +6,7 @@ FLUTTER ?= flutter
|
||||
PNPM ?= pnpm
|
||||
DART ?= dart
|
||||
DEVICE ?= macos
|
||||
APP_STORE_DART_DEFINE ?= --dart-define=XWORKMATE_APP_STORE=true
|
||||
|
||||
.PHONY: help deps analyze test check format run build-linux build-macos build-ios-sim package-deb package-rpm package-linux package-mac install-mac clean build-aris-bridge render-release-docs
|
||||
|
||||
@ -36,10 +37,10 @@ build-linux: ## Build the Linux app in release mode
|
||||
$(FLUTTER) build linux --release
|
||||
|
||||
build-macos: ## Build the macOS app in release mode
|
||||
$(FLUTTER) build macos --release
|
||||
$(FLUTTER) build macos --release $(APP_STORE_DART_DEFINE)
|
||||
|
||||
build-ios-sim: ## Build the iOS app for the simulator
|
||||
$(FLUTTER) build ios --simulator
|
||||
$(FLUTTER) build ios --simulator $(APP_STORE_DART_DEFINE)
|
||||
|
||||
build-aris-bridge: ## Build the ARIS Go bridge helper
|
||||
bash scripts/build-aris-bridge.sh
|
||||
@ -54,10 +55,10 @@ package-linux: ## Create both Linux packages
|
||||
bash scripts/package-linux.sh
|
||||
|
||||
package-mac: ## Create the macOS .app and DMG
|
||||
bash scripts/package-flutter-mac-app.sh
|
||||
XWORKMATE_APP_STORE=true bash scripts/package-flutter-mac-app.sh
|
||||
|
||||
install-mac: ## Package and install the macOS app into /Applications
|
||||
bash scripts/package-flutter-mac-app.sh
|
||||
XWORKMATE_APP_STORE=true bash scripts/package-flutter-mac-app.sh
|
||||
bash scripts/install-flutter-mac-dmg.sh
|
||||
|
||||
clean: ## Remove generated artifacts
|
||||
|
||||
@ -14,6 +14,7 @@
|
||||
7884E8682EC3CC0700C636F2 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7884E8672EC3CC0400C636F2 /* SceneDelegate.swift */; };
|
||||
790F5BD2C520842BBA31950C /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 63E65220C02DE80AF75C238E /* Pods_Runner.framework */; };
|
||||
7F0C4AAE0C8458F9E652862D /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 29ABF973925162A04B6C3BE4 /* Pods_RunnerTests.framework */; };
|
||||
8E6F4A7B31A1A00100A1B2C3 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 8E6F4A7A31A1A00100A1B2C3 /* PrivacyInfo.xcprivacy */; };
|
||||
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
|
||||
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
|
||||
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
|
||||
@ -56,6 +57,7 @@
|
||||
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = "<group>"; };
|
||||
74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
|
||||
7884E8672EC3CC0400C636F2 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = "<group>"; };
|
||||
8E6F4A7A31A1A00100A1B2C3 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = "<group>"; };
|
||||
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; };
|
||||
9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = "<group>"; };
|
||||
9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = "<group>"; };
|
||||
@ -158,6 +160,7 @@
|
||||
97C146FD1CF9000F007C117D /* Assets.xcassets */,
|
||||
97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */,
|
||||
97C147021CF9000F007C117D /* Info.plist */,
|
||||
8E6F4A7A31A1A00100A1B2C3 /* PrivacyInfo.xcprivacy */,
|
||||
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */,
|
||||
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */,
|
||||
74858FAE1ED2DC5600515810 /* AppDelegate.swift */,
|
||||
@ -262,6 +265,7 @@
|
||||
isa = PBXResourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
8E6F4A7B31A1A00100A1B2C3 /* PrivacyInfo.xcprivacy in Resources */,
|
||||
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */,
|
||||
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */,
|
||||
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */,
|
||||
|
||||
@ -26,6 +26,8 @@
|
||||
<string>$(FLUTTER_BUILD_NUMBER)</string>
|
||||
<key>LSRequiresIPhoneOS</key>
|
||||
<true/>
|
||||
<key>NSLocalNetworkUsageDescription</key>
|
||||
<string>XWorkmate uses your local network only when you explicitly connect to a user-configured OpenClaw Gateway on the same network.</string>
|
||||
<key>UIApplicationSceneManifest</key>
|
||||
<dict>
|
||||
<key>UIApplicationSupportsMultipleScenes</key>
|
||||
|
||||
47
ios/Runner/PrivacyInfo.xcprivacy
Normal file
47
ios/Runner/PrivacyInfo.xcprivacy
Normal file
@ -0,0 +1,47 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>NSPrivacyTracking</key>
|
||||
<false/>
|
||||
<key>NSPrivacyTrackingDomains</key>
|
||||
<array/>
|
||||
<key>NSPrivacyCollectedDataTypes</key>
|
||||
<array/>
|
||||
<key>NSPrivacyAccessedAPITypes</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>NSPrivacyAccessedAPIType</key>
|
||||
<string>NSPrivacyAccessedAPICategoryUserDefaults</string>
|
||||
<key>NSPrivacyAccessedAPITypeReasons</key>
|
||||
<array>
|
||||
<string>CA92.1</string>
|
||||
</array>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>NSPrivacyAccessedAPIType</key>
|
||||
<string>NSPrivacyAccessedAPICategoryFileTimestamp</string>
|
||||
<key>NSPrivacyAccessedAPITypeReasons</key>
|
||||
<array>
|
||||
<string>C617.1</string>
|
||||
</array>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>NSPrivacyAccessedAPIType</key>
|
||||
<string>NSPrivacyAccessedAPICategoryDiskSpace</string>
|
||||
<key>NSPrivacyAccessedAPITypeReasons</key>
|
||||
<array>
|
||||
<string>E174.1</string>
|
||||
</array>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>NSPrivacyAccessedAPIType</key>
|
||||
<string>NSPrivacyAccessedAPICategorySystemBootTime</string>
|
||||
<key>NSPrivacyAccessedAPITypeReasons</key>
|
||||
<array>
|
||||
<string>35F9.1</string>
|
||||
</array>
|
||||
</dict>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
@ -6,6 +6,7 @@ import 'package:flutter/material.dart';
|
||||
|
||||
import 'app_metadata.dart';
|
||||
import 'app_capabilities.dart';
|
||||
import 'app_store_policy.dart';
|
||||
import 'ui_feature_manifest.dart';
|
||||
import '../i18n/app_language.dart';
|
||||
import '../models/app_models.dart';
|
||||
@ -203,7 +204,12 @@ class AppController extends ChangeNotifier {
|
||||
String? get bootstrapError => _bootstrapError;
|
||||
|
||||
UiFeatureAccess featuresFor(UiFeaturePlatform platform) {
|
||||
return _uiFeatureManifest.forPlatform(platform);
|
||||
final manifest = applyAppleAppStorePolicy(
|
||||
_uiFeatureManifest,
|
||||
hostPlatform: platform,
|
||||
isAppleHost: Platform.isIOS || Platform.isMacOS,
|
||||
);
|
||||
return manifest.forPlatform(platform);
|
||||
}
|
||||
|
||||
RuntimeCoordinator get runtimeCoordinator => _runtimeCoordinator;
|
||||
@ -323,6 +329,11 @@ class AppController extends ChangeNotifier {
|
||||
availableSingleAgentProviders.isNotEmpty;
|
||||
|
||||
bool _canUseSingleAgentProvider(SingleAgentProvider provider) {
|
||||
if (!allowsAppStoreExternalSingleAgentProviders(
|
||||
isAppleHost: Platform.isIOS || Platform.isMacOS,
|
||||
)) {
|
||||
return false;
|
||||
}
|
||||
final override = _availableSingleAgentProvidersOverride;
|
||||
if (override != null) {
|
||||
return provider != SingleAgentProvider.auto &&
|
||||
@ -460,8 +471,11 @@ class AppController extends ChangeNotifier {
|
||||
|
||||
SingleAgentProvider singleAgentProviderForSession(String sessionKey) {
|
||||
final normalizedSessionKey = _normalizedAssistantSessionKey(sessionKey);
|
||||
return _assistantThreadRecords[normalizedSessionKey]?.singleAgentProvider ??
|
||||
SingleAgentProvider.auto;
|
||||
return sanitizeAppStoreSingleAgentProvider(
|
||||
_assistantThreadRecords[normalizedSessionKey]?.singleAgentProvider ??
|
||||
SingleAgentProvider.auto,
|
||||
isAppleHost: Platform.isIOS || Platform.isMacOS,
|
||||
);
|
||||
}
|
||||
|
||||
SingleAgentProvider get currentSingleAgentProvider =>
|
||||
@ -549,7 +563,11 @@ class AppController extends ChangeNotifier {
|
||||
singleAgentModelDisplayLabelForSession(currentSessionKey);
|
||||
|
||||
List<SingleAgentProvider> get singleAgentProviderOptions =>
|
||||
SingleAgentProvider.values;
|
||||
allowsAppStoreExternalSingleAgentProviders(
|
||||
isAppleHost: Platform.isIOS || Platform.isMacOS,
|
||||
)
|
||||
? SingleAgentProvider.values
|
||||
: const <SingleAgentProvider>[SingleAgentProvider.auto];
|
||||
|
||||
String singleAgentProviderLabelForSession(String sessionKey) {
|
||||
return singleAgentProviderForSession(sessionKey).label;
|
||||
@ -1688,12 +1706,16 @@ class AppController extends ChangeNotifier {
|
||||
|
||||
Future<void> setSingleAgentProvider(SingleAgentProvider provider) async {
|
||||
final sessionKey = _normalizedAssistantSessionKey(currentSessionKey);
|
||||
if (singleAgentProviderForSession(sessionKey) == provider) {
|
||||
final sanitizedProvider = sanitizeAppStoreSingleAgentProvider(
|
||||
provider,
|
||||
isAppleHost: Platform.isIOS || Platform.isMacOS,
|
||||
);
|
||||
if (singleAgentProviderForSession(sessionKey) == sanitizedProvider) {
|
||||
return;
|
||||
}
|
||||
_upsertAssistantThreadRecord(
|
||||
sessionKey,
|
||||
singleAgentProvider: provider,
|
||||
singleAgentProvider: sanitizedProvider,
|
||||
discoveredSkills: const <AssistantThreadSkillEntry>[],
|
||||
importedSkills: const <AssistantThreadSkillEntry>[],
|
||||
selectedSkillKeys: const <String>[],
|
||||
@ -2606,6 +2628,7 @@ class AppController extends ChangeNotifier {
|
||||
setActiveAppLanguage(settings.appLanguage);
|
||||
await _desktopPlatformService.initialize(settings.linuxDesktop);
|
||||
await _desktopPlatformService.setLaunchAtLogin(settings.launchAtLogin);
|
||||
await _refreshResolvedCodexCliPath();
|
||||
_registerCodexExternalProvider();
|
||||
await _refreshAcpCapabilities(persistMountTargets: true);
|
||||
if (_disposed) {
|
||||
@ -2791,6 +2814,7 @@ class AppController extends ChangeNotifier {
|
||||
}
|
||||
if (previous.codexCliPath != current.codexCliPath ||
|
||||
previous.codeAgentRuntimeMode != current.codeAgentRuntimeMode) {
|
||||
await _refreshResolvedCodexCliPath();
|
||||
_registerCodexExternalProvider();
|
||||
}
|
||||
if (previous.linuxDesktop.toJson().toString() !=
|
||||
@ -4395,13 +4419,6 @@ class AppController extends ChangeNotifier {
|
||||
capabilities = const GatewayAcpCapabilities.empty();
|
||||
}
|
||||
_acpCapabilities = capabilities;
|
||||
_resolvedCodexCliPath =
|
||||
capabilities.providers.contains(SingleAgentProvider.codex)
|
||||
? appText(
|
||||
'通过 Gateway ACP 能力协商检测到 Codex Provider',
|
||||
'Detected Codex provider via Gateway ACP capability negotiation',
|
||||
)
|
||||
: null;
|
||||
if (persistMountTargets && !_disposed) {
|
||||
final currentConfig = settings.multiAgent;
|
||||
final nextTargets = _mergeAcpCapabilitiesIntoMountTargets(
|
||||
@ -4420,6 +4437,30 @@ class AppController extends ChangeNotifier {
|
||||
_notifyIfActive();
|
||||
}
|
||||
|
||||
Future<void> _refreshResolvedCodexCliPath() async {
|
||||
if (effectiveCodeAgentRuntimeMode != CodeAgentRuntimeMode.externalCli) {
|
||||
_resolvedCodexCliPath = null;
|
||||
return;
|
||||
}
|
||||
|
||||
final configuredPath = configuredCodexCliPath;
|
||||
String? detectedPath;
|
||||
if (configuredPath.isNotEmpty) {
|
||||
try {
|
||||
if (await File(configuredPath).exists()) {
|
||||
detectedPath = configuredPath;
|
||||
}
|
||||
} catch (_) {
|
||||
detectedPath = null;
|
||||
}
|
||||
}
|
||||
detectedPath ??= await _runtimeCoordinator.codex.findCodexBinary();
|
||||
if (_disposed) {
|
||||
return;
|
||||
}
|
||||
_resolvedCodexCliPath = detectedPath;
|
||||
}
|
||||
|
||||
List<ManagedMountTargetState> _mergeAcpCapabilitiesIntoMountTargets(
|
||||
List<ManagedMountTargetState> current,
|
||||
GatewayAcpCapabilities capabilities,
|
||||
|
||||
156
lib/app/app_store_policy.dart
Normal file
156
lib/app/app_store_policy.dart
Normal file
@ -0,0 +1,156 @@
|
||||
import '../runtime/runtime_models.dart';
|
||||
import 'ui_feature_manifest.dart';
|
||||
|
||||
const bool kAppStoreDistribution = bool.fromEnvironment(
|
||||
'XWORKMATE_APP_STORE',
|
||||
defaultValue: false,
|
||||
);
|
||||
|
||||
bool shouldApplyAppleAppStorePolicy({
|
||||
required bool isAppleHost,
|
||||
bool? enabled,
|
||||
}) {
|
||||
return (enabled ?? kAppStoreDistribution) && isAppleHost;
|
||||
}
|
||||
|
||||
UiFeatureManifest applyAppleAppStorePolicy(
|
||||
UiFeatureManifest manifest, {
|
||||
required UiFeaturePlatform hostPlatform,
|
||||
required bool isAppleHost,
|
||||
bool? enabled,
|
||||
}) {
|
||||
if (!shouldApplyAppleAppStorePolicy(
|
||||
isAppleHost: isAppleHost,
|
||||
enabled: enabled,
|
||||
)) {
|
||||
return manifest;
|
||||
}
|
||||
|
||||
var next = manifest;
|
||||
final disabledPaths = <(UiFeaturePlatform, String, String)>[
|
||||
(
|
||||
hostPlatform,
|
||||
'navigation',
|
||||
_featureKeyLeaf(UiFeatureKeys.navigationAgents),
|
||||
),
|
||||
(
|
||||
hostPlatform,
|
||||
'navigation',
|
||||
_featureKeyLeaf(UiFeatureKeys.navigationMcpServer),
|
||||
),
|
||||
(
|
||||
hostPlatform,
|
||||
'navigation',
|
||||
_featureKeyLeaf(UiFeatureKeys.navigationClawHub),
|
||||
),
|
||||
(hostPlatform, 'workspace', _featureKeyLeaf(UiFeatureKeys.workspaceAgents)),
|
||||
(
|
||||
hostPlatform,
|
||||
'workspace',
|
||||
_featureKeyLeaf(UiFeatureKeys.workspaceMcpServer),
|
||||
),
|
||||
(
|
||||
hostPlatform,
|
||||
'workspace',
|
||||
_featureKeyLeaf(UiFeatureKeys.workspaceClawHub),
|
||||
),
|
||||
(hostPlatform, 'settings', _featureKeyLeaf(UiFeatureKeys.settingsAgents)),
|
||||
(
|
||||
hostPlatform,
|
||||
'settings',
|
||||
_featureKeyLeaf(UiFeatureKeys.settingsExperimental),
|
||||
),
|
||||
(
|
||||
hostPlatform,
|
||||
'settings',
|
||||
_featureKeyLeaf(UiFeatureKeys.settingsExperimentalCanvas),
|
||||
),
|
||||
(
|
||||
hostPlatform,
|
||||
'settings',
|
||||
_featureKeyLeaf(UiFeatureKeys.settingsExperimentalBridge),
|
||||
),
|
||||
(
|
||||
hostPlatform,
|
||||
'settings',
|
||||
_featureKeyLeaf(UiFeatureKeys.settingsExperimentalDebug),
|
||||
),
|
||||
];
|
||||
|
||||
if (hostPlatform == UiFeaturePlatform.mobile) {
|
||||
disabledPaths.addAll(<(UiFeaturePlatform, String, String)>[
|
||||
(
|
||||
hostPlatform,
|
||||
'assistant',
|
||||
_featureKeyLeaf(UiFeatureKeys.assistantLocalGateway),
|
||||
),
|
||||
(
|
||||
hostPlatform,
|
||||
'assistant',
|
||||
_featureKeyLeaf(UiFeatureKeys.assistantMultiAgent),
|
||||
),
|
||||
]);
|
||||
}
|
||||
|
||||
if (hostPlatform == UiFeaturePlatform.desktop) {
|
||||
disabledPaths.addAll(<(UiFeaturePlatform, String, String)>[
|
||||
(
|
||||
hostPlatform,
|
||||
'assistant',
|
||||
_featureKeyLeaf(UiFeatureKeys.assistantMultiAgent),
|
||||
),
|
||||
(
|
||||
hostPlatform,
|
||||
'assistant',
|
||||
_featureKeyLeaf(UiFeatureKeys.assistantLocalRuntime),
|
||||
),
|
||||
]);
|
||||
}
|
||||
|
||||
for (final (platform, module, feature) in disabledPaths) {
|
||||
if (next.lookup(platform, module, feature) == null) {
|
||||
continue;
|
||||
}
|
||||
next = next.copyWithFeature(
|
||||
platform: platform,
|
||||
module: module,
|
||||
feature: feature,
|
||||
enabled: false,
|
||||
buildModes: const <UiFeatureBuildMode>{},
|
||||
);
|
||||
}
|
||||
|
||||
return next;
|
||||
}
|
||||
|
||||
bool allowsAppStoreExternalSingleAgentProviders({
|
||||
required bool isAppleHost,
|
||||
bool? enabled,
|
||||
}) {
|
||||
return !shouldApplyAppleAppStorePolicy(
|
||||
isAppleHost: isAppleHost,
|
||||
enabled: enabled,
|
||||
);
|
||||
}
|
||||
|
||||
SingleAgentProvider sanitizeAppStoreSingleAgentProvider(
|
||||
SingleAgentProvider provider, {
|
||||
required bool isAppleHost,
|
||||
bool? enabled,
|
||||
}) {
|
||||
if (!allowsAppStoreExternalSingleAgentProviders(
|
||||
isAppleHost: isAppleHost,
|
||||
enabled: enabled,
|
||||
)) {
|
||||
return SingleAgentProvider.auto;
|
||||
}
|
||||
return provider;
|
||||
}
|
||||
|
||||
String _featureKeyLeaf(String keyPath) {
|
||||
final segments = keyPath.split('.');
|
||||
if (segments.isEmpty) {
|
||||
throw StateError('Invalid feature key path: $keyPath');
|
||||
}
|
||||
return segments.last;
|
||||
}
|
||||
@ -4,6 +4,7 @@ import 'package:flutter/material.dart';
|
||||
|
||||
import '../../app/app_controller.dart';
|
||||
import '../../app/app_metadata.dart';
|
||||
import '../../app/app_store_policy.dart';
|
||||
import '../../app/ui_feature_manifest.dart';
|
||||
import '../../app/workspace_navigation.dart';
|
||||
import '../../i18n/app_language.dart';
|
||||
@ -2482,12 +2483,130 @@ class _SettingsPageState extends State<SettingsPage> {
|
||||
label: appText('包名', 'Package'),
|
||||
value: controller.runtime.packageInfo.packageName,
|
||||
),
|
||||
if (kAppStoreDistribution) ...[
|
||||
const SizedBox(height: 16),
|
||||
Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.surfaceContainerHighest,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
child: Text(
|
||||
appText(
|
||||
'当前构建启用了 App Store 分发策略:Apple 渠道会隐藏实验入口,并禁用外部 CLI / 本地 Runtime 能力。',
|
||||
'This build enables the App Store distribution policy: Apple storefront builds hide experimental surfaces and disable external CLI / local runtime capabilities.',
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
SurfaceCard(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
appText('隐私政策', 'Privacy Policy'),
|
||||
style: Theme.of(context).textTheme.titleLarge,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Text(
|
||||
appText(
|
||||
'说明本应用会保存哪些本地设置、哪些用户数据会按你的操作发送到外部网关或 LLM 端点,以及如何清除本地数据。',
|
||||
'Explains which settings stay on-device, which user data is sent to your configured gateway or LLM endpoints, and how to clear local data.',
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
FilledButton.tonalIcon(
|
||||
key: const ValueKey('settings-open-privacy-policy'),
|
||||
onPressed: () => _showPrivacyPolicyDialog(context),
|
||||
icon: const Icon(Icons.privacy_tip_outlined),
|
||||
label: Text(appText('查看隐私政策', 'View Privacy Policy')),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
Future<void> _showPrivacyPolicyDialog(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
return showDialog<void>(
|
||||
context: context,
|
||||
builder: (dialogContext) {
|
||||
return AlertDialog(
|
||||
title: Text(appText('隐私政策', 'Privacy Policy')),
|
||||
content: SizedBox(
|
||||
width: 560,
|
||||
child: SingleChildScrollView(
|
||||
child: Text(
|
||||
appText(_privacyPolicyZh, _privacyPolicyEn),
|
||||
style: theme.textTheme.bodyMedium,
|
||||
),
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(dialogContext).pop(),
|
||||
child: Text(appText('关闭', 'Close')),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
static const String _privacyPolicyZh = '''
|
||||
XWorkmate 隐私政策
|
||||
|
||||
1. 本地保存
|
||||
- 应用会在本机保存你主动配置的工作区设置、界面偏好、线程草稿和诊断状态。
|
||||
- 共享 Token、密码、API Key 等敏感信息使用系统安全存储;不会写入普通 SharedPreferences。
|
||||
|
||||
2. 发送到外部服务的数据
|
||||
- 只有在你主动发起连接、发送消息、上传附件或测试连接时,应用才会把当前输入内容发送到你配置的 OpenClaw Gateway 或 LLM API Endpoint。
|
||||
- 发送内容可能包括:提示词、会话上下文、你明确选择的附件路径与文件内容、以及完成请求所需的认证头。
|
||||
|
||||
3. 不会做的事情
|
||||
- 不会接入广告 SDK,不会做跨应用追踪,不会在未操作时自动读取工作区文件。
|
||||
- 不会把你的网关密码、共享 Token 或 LLM API Token 上传到本项目默认的开发者服务。
|
||||
|
||||
4. 第三方处理
|
||||
- 你配置的 OpenClaw Gateway、LLM API Endpoint、对象存储或其它外部服务,将按你自己的服务条款处理收到的数据。
|
||||
- 你需要确认这些外部服务具备你要求的合规能力。
|
||||
|
||||
5. 删除与撤回
|
||||
- 你可以在“设置 -> 诊断/集成”中清除本地线程、移除本地配置,并删除已保存的安全凭据。
|
||||
- 如果你希望删除已经发送到外部服务的数据,需要在对应外部服务侧执行删除或撤回。
|
||||
''';
|
||||
|
||||
static const String _privacyPolicyEn = '''
|
||||
XWorkmate Privacy Policy
|
||||
|
||||
1. Local storage
|
||||
- The app stores the settings, UI preferences, draft threads, and diagnostic state that you explicitly save on this device.
|
||||
- Shared tokens, passwords, and API keys are stored in platform secure storage instead of plain SharedPreferences.
|
||||
|
||||
2. Data sent to external services
|
||||
- Data is only sent when you explicitly connect, send a message, attach a file, or run a connection test against your configured OpenClaw Gateway or LLM API endpoint.
|
||||
- Sent data can include prompts, conversation context, user-selected attachment paths and file contents, and the authentication headers required to complete the request.
|
||||
|
||||
3. What the app does not do
|
||||
- It does not include advertising SDKs, cross-app tracking, or automatic workspace file reads without a user action.
|
||||
- It does not upload your gateway passwords, shared tokens, or LLM API tokens to developer-operated services by default.
|
||||
|
||||
4. Third-party processing
|
||||
- Your configured OpenClaw Gateway, LLM API endpoint, object storage, or other external services process the data you send under their own terms.
|
||||
- You are responsible for confirming that those external services meet your compliance requirements.
|
||||
|
||||
5. Deletion and withdrawal
|
||||
- You can clear local threads, remove local settings, and delete stored secrets from Settings.
|
||||
- If you need data removed from an external service, you must request deletion from that external service directly.
|
||||
''';
|
||||
|
||||
Future<void> _saveSettings(
|
||||
AppController controller,
|
||||
SettingsSnapshot snapshot,
|
||||
|
||||
@ -124,6 +124,8 @@ class SettingsStore {
|
||||
await _deleteDurableStateFile(settingsKey);
|
||||
await _deleteDurableStateFile(assistantThreadsKey);
|
||||
await _deleteLegacyBackupFile();
|
||||
_lastRecoveryReport = const LegacyRecoveryReport();
|
||||
_recoveryAttempted = true;
|
||||
}
|
||||
|
||||
Future<List<SecretAuditEntry>> loadAuditTrail() async {
|
||||
@ -365,13 +367,17 @@ class SettingsStore {
|
||||
final results = <String>{};
|
||||
final databasePath = await _resolveDatabasePath();
|
||||
final fallbackRoot = await _fallbackDirectoryPathResolver?.call();
|
||||
final defaultSupportRoot = await _defaultSupportDirectoryPathResolver
|
||||
?.call();
|
||||
final hasExplicitPaths =
|
||||
_databasePathResolver != null || _fallbackDirectoryPathResolver != null;
|
||||
String? defaultSupportRoot;
|
||||
String? supportPath;
|
||||
try {
|
||||
supportPath = (await getApplicationSupportDirectory()).path;
|
||||
} catch (_) {
|
||||
supportPath = null;
|
||||
if (!hasExplicitPaths) {
|
||||
defaultSupportRoot = await _defaultSupportDirectoryPathResolver?.call();
|
||||
try {
|
||||
supportPath = (await getApplicationSupportDirectory()).path;
|
||||
} catch (_) {
|
||||
supportPath = null;
|
||||
}
|
||||
}
|
||||
|
||||
void addPath(String? path) {
|
||||
@ -385,7 +391,6 @@ class SettingsStore {
|
||||
if (databasePath != null && databasePath.trim().isNotEmpty) {
|
||||
final directory = File(databasePath).parent.path;
|
||||
addPath(directory);
|
||||
addPath(Directory(directory).parent.path);
|
||||
}
|
||||
addPath(fallbackRoot);
|
||||
addPath(fallbackRoot == null ? null : '$fallbackRoot/xworkmate');
|
||||
|
||||
@ -26,6 +26,7 @@
|
||||
33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC10F02044A3C60003C045 /* AppDelegate.swift */; };
|
||||
33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F22044A3C60003C045 /* Assets.xcassets */; };
|
||||
33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; };
|
||||
8E6F4A7D31A1A10100A1B2C3 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 8E6F4A7C31A1A10100A1B2C3 /* PrivacyInfo.xcprivacy */; };
|
||||
33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; };
|
||||
A96EF8FFA0E80B16252FE834 /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B5F4830709C3A67EC8096888 /* Pods_RunnerTests.framework */; };
|
||||
F02922E20E15948F8CE5469F /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 2099D82E31DC5912EA477802 /* Pods_Runner.framework */; };
|
||||
@ -73,6 +74,7 @@
|
||||
33CC10F22044A3C60003C045 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = Runner/Assets.xcassets; sourceTree = "<group>"; };
|
||||
33CC10F52044A3C60003C045 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = "<group>"; };
|
||||
33CC10F72044A3C60003C045 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = Info.plist; path = Runner/Info.plist; sourceTree = "<group>"; };
|
||||
8E6F4A7C31A1A10100A1B2C3 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = Runner/PrivacyInfo.xcprivacy; sourceTree = "<group>"; };
|
||||
33CC11122044BFA00003C045 /* MainFlutterWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainFlutterWindow.swift; sourceTree = "<group>"; };
|
||||
33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Debug.xcconfig"; sourceTree = "<group>"; };
|
||||
33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Release.xcconfig"; sourceTree = "<group>"; };
|
||||
@ -156,6 +158,7 @@
|
||||
33CC10F22044A3C60003C045 /* Assets.xcassets */,
|
||||
33CC10F42044A3C60003C045 /* MainMenu.xib */,
|
||||
33CC10F72044A3C60003C045 /* Info.plist */,
|
||||
8E6F4A7C31A1A10100A1B2C3 /* PrivacyInfo.xcprivacy */,
|
||||
);
|
||||
name = Resources;
|
||||
path = ..;
|
||||
@ -316,6 +319,7 @@
|
||||
files = (
|
||||
33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */,
|
||||
33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */,
|
||||
8E6F4A7D31A1A10100A1B2C3 /* PrivacyInfo.xcprivacy in Resources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
|
||||
47
macos/Runner/PrivacyInfo.xcprivacy
Normal file
47
macos/Runner/PrivacyInfo.xcprivacy
Normal file
@ -0,0 +1,47 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>NSPrivacyTracking</key>
|
||||
<false/>
|
||||
<key>NSPrivacyTrackingDomains</key>
|
||||
<array/>
|
||||
<key>NSPrivacyCollectedDataTypes</key>
|
||||
<array/>
|
||||
<key>NSPrivacyAccessedAPITypes</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>NSPrivacyAccessedAPIType</key>
|
||||
<string>NSPrivacyAccessedAPICategoryUserDefaults</string>
|
||||
<key>NSPrivacyAccessedAPITypeReasons</key>
|
||||
<array>
|
||||
<string>CA92.1</string>
|
||||
</array>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>NSPrivacyAccessedAPIType</key>
|
||||
<string>NSPrivacyAccessedAPICategoryFileTimestamp</string>
|
||||
<key>NSPrivacyAccessedAPITypeReasons</key>
|
||||
<array>
|
||||
<string>C617.1</string>
|
||||
</array>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>NSPrivacyAccessedAPIType</key>
|
||||
<string>NSPrivacyAccessedAPICategoryDiskSpace</string>
|
||||
<key>NSPrivacyAccessedAPITypeReasons</key>
|
||||
<array>
|
||||
<string>E174.1</string>
|
||||
</array>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>NSPrivacyAccessedAPIType</key>
|
||||
<string>NSPrivacyAccessedAPICategorySystemBootTime</string>
|
||||
<key>NSPrivacyAccessedAPITypeReasons</key>
|
||||
<array>
|
||||
<string>35F9.1</string>
|
||||
</array>
|
||||
</dict>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
@ -7,6 +7,7 @@ PUBSPEC_PATH="$ROOT_DIR/pubspec.yaml"
|
||||
DIST_DIR="$ROOT_DIR/dist"
|
||||
APP_NAME="${APP_NAME:-XWorkmate}"
|
||||
BUILD_MODE="${BUILD_MODE:-release}"
|
||||
APP_STORE_DEFINE="${APP_STORE_DEFINE:---dart-define=XWORKMATE_APP_STORE=${XWORKMATE_APP_STORE:-true}}"
|
||||
PRODUCTS_DIR_NAME="$(tr '[:lower:]' '[:upper:]' <<< "${BUILD_MODE:0:1}")${BUILD_MODE:1}"
|
||||
BRIDGE_BINARY_NAME="${BRIDGE_BINARY_NAME:-xworkmate-aris-bridge}"
|
||||
BRIDGE_BUILD_PATH="${ROOT_DIR}/build/bin/${BRIDGE_BINARY_NAME}"
|
||||
@ -47,6 +48,7 @@ BUILD_ARGS=(
|
||||
--build-number="$APP_BUILD"
|
||||
--dart-define="XWORKMATE_DISPLAY_VERSION=$APP_VERSION"
|
||||
--dart-define="XWORKMATE_BUILD_NUMBER=$APP_BUILD"
|
||||
"$APP_STORE_DEFINE"
|
||||
)
|
||||
|
||||
if [[ -f "$APP_DIR/.dart_tool/package_config.json" ]]; then
|
||||
|
||||
71
test/app/app_store_policy_test.dart
Normal file
71
test/app/app_store_policy_test.dart
Normal file
@ -0,0 +1,71 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:xworkmate/app/app_store_policy.dart';
|
||||
import 'package:xworkmate/app/ui_feature_manifest.dart';
|
||||
import 'package:xworkmate/models/app_models.dart';
|
||||
import 'package:xworkmate/runtime/runtime_models.dart';
|
||||
|
||||
void main() {
|
||||
test('apple app store policy disables restricted desktop surfaces', () {
|
||||
final manifest = applyAppleAppStorePolicy(
|
||||
UiFeatureManifest.fallback(),
|
||||
hostPlatform: UiFeaturePlatform.desktop,
|
||||
isAppleHost: true,
|
||||
enabled: true,
|
||||
);
|
||||
final access = manifest.forPlatform(
|
||||
UiFeaturePlatform.desktop,
|
||||
buildMode: UiFeatureBuildMode.release,
|
||||
);
|
||||
|
||||
expect(access.supportsDesktopRuntime, isFalse);
|
||||
expect(access.supportsMultiAgent, isFalse);
|
||||
expect(
|
||||
access.allowedDestinations.contains(WorkspaceDestination.agents),
|
||||
isFalse,
|
||||
);
|
||||
expect(
|
||||
access.allowedDestinations.contains(WorkspaceDestination.clawHub),
|
||||
isFalse,
|
||||
);
|
||||
expect(access.availableSettingsTabs.contains(SettingsTab.agents), isFalse);
|
||||
});
|
||||
|
||||
test('apple app store policy disables local mobile assistant features', () {
|
||||
final manifest = applyAppleAppStorePolicy(
|
||||
UiFeatureManifest.fallback(),
|
||||
hostPlatform: UiFeaturePlatform.mobile,
|
||||
isAppleHost: true,
|
||||
enabled: true,
|
||||
);
|
||||
final access = manifest.forPlatform(
|
||||
UiFeaturePlatform.mobile,
|
||||
buildMode: UiFeatureBuildMode.release,
|
||||
);
|
||||
|
||||
expect(access.supportsLocalGateway, isFalse);
|
||||
expect(access.supportsMultiAgent, isFalse);
|
||||
expect(
|
||||
access.availableExecutionTargets,
|
||||
equals(<AssistantExecutionTarget>[AssistantExecutionTarget.remote]),
|
||||
);
|
||||
});
|
||||
|
||||
test('single-agent provider selection is forced to auto for app store', () {
|
||||
expect(
|
||||
sanitizeAppStoreSingleAgentProvider(
|
||||
SingleAgentProvider.codex,
|
||||
isAppleHost: true,
|
||||
enabled: true,
|
||||
),
|
||||
SingleAgentProvider.auto,
|
||||
);
|
||||
expect(
|
||||
sanitizeAppStoreSingleAgentProvider(
|
||||
SingleAgentProvider.gemini,
|
||||
isAppleHost: false,
|
||||
enabled: true,
|
||||
),
|
||||
SingleAgentProvider.gemini,
|
||||
);
|
||||
});
|
||||
}
|
||||
@ -28,6 +28,6 @@ void main() {
|
||||
);
|
||||
|
||||
expect(find.text('OpenClaw Gateway'), findsOneWidget);
|
||||
expect(find.text('Vault Server'), findsOneWidget);
|
||||
expect(find.text('Vault Server'), findsNothing);
|
||||
});
|
||||
}
|
||||
|
||||
@ -17,20 +17,28 @@ void main() {
|
||||
'SettingsPage AI Gateway draft/save/apply flow persists edited fields through local actions',
|
||||
(WidgetTester tester) async {
|
||||
late _AiGatewaySettingsTestController controller;
|
||||
late Directory testRoot;
|
||||
await tester.runAsync(() async {
|
||||
SharedPreferences.setMockInitialValues(<String, Object>{});
|
||||
final testRoot =
|
||||
'${Directory.systemTemp.path}/xworkmate-widget-tests-${DateTime.now().microsecondsSinceEpoch}';
|
||||
testRoot = await Directory.systemTemp.createTemp(
|
||||
'xworkmate-widget-tests-',
|
||||
);
|
||||
controller = _AiGatewaySettingsTestController(
|
||||
store: SecureConfigStore(
|
||||
enableSecureStorage: false,
|
||||
databasePathResolver: () async => '$testRoot/settings.sqlite3',
|
||||
fallbackDirectoryPathResolver: () async => testRoot,
|
||||
databasePathResolver: () async =>
|
||||
'${testRoot.path}/settings.sqlite3',
|
||||
fallbackDirectoryPathResolver: () async => testRoot.path,
|
||||
),
|
||||
);
|
||||
await _waitFor(() => !controller.initializing);
|
||||
});
|
||||
addTearDown(controller.dispose);
|
||||
addTearDown(() async {
|
||||
if (await testRoot.exists()) {
|
||||
await testRoot.delete(recursive: true);
|
||||
}
|
||||
});
|
||||
|
||||
final staleGateway = controller.settings.aiGateway.copyWith(
|
||||
name: 'default',
|
||||
|
||||
@ -50,10 +50,7 @@ void main() {
|
||||
SingleAgentProvider.codex,
|
||||
SingleAgentProvider.claude,
|
||||
],
|
||||
gatewayOnlySkillScanRoots: <String>[
|
||||
codexRoot.path,
|
||||
claudeRoot.path,
|
||||
],
|
||||
gatewayOnlySkillScanRoots: <String>[codexRoot.path, claudeRoot.path],
|
||||
);
|
||||
addTearDown(controller.dispose);
|
||||
await _waitFor(() => !controller.initializing);
|
||||
@ -165,9 +162,10 @@ void main() {
|
||||
);
|
||||
await controller.switchSession('draft:thread-2');
|
||||
expect(
|
||||
controller.assistantImportedSkillsForSession(
|
||||
controller.currentSessionKey,
|
||||
).single.label,
|
||||
controller
|
||||
.assistantImportedSkillsForSession(controller.currentSessionKey)
|
||||
.single
|
||||
.label,
|
||||
'Review',
|
||||
);
|
||||
await controller.selectAssistantModelForSession(
|
||||
|
||||
Loading…
Reference in New Issue
Block a user