Local branch had diverged from origin/master (sibling commits on the same base). Rewrote local history linearly on top of origin/master, folding in all local content: exa/hot-platform discovery hunters, leaks ledger, nightly sweep orchestrator, updated .gitignore and skill docs, plus the latest hunt output and vault state. Remote-only files (vault keys, channel scripts) were restored rather than dropped, so the resulting tree is a full union of both sides.
1 line
4.5 MiB
Plaintext
1 line
4.5 MiB
Plaintext
{"repo": "bhrumom/fabushi", "files": {"fabushi/lib/core/config/app_config.dart": {"3323b5c4de0a31d36fe4d8934069ae04a3a16fc0": {"t": 1785745925.8578932, "n": 11587, "h": "457c3958c206ce9d", "c": "import 'package:flutter/foundation.dart';\n\n/// 应用配置 - 统一管理所有配置项\nclass AppConfig {\n AppConfig._();\n\n static final AppConfig instance = AppConfig._();\n\n // 环境配置\n static const String environment = String.fromEnvironment(\n 'ENV',\n defaultValue: 'production',\n );\n\n static bool get isProduction {\n if (kIsWeb) {\n final currentUrl = Uri.base.toString();\n if (currentUrl.contains('fabushi-flutter-web-dev') ||\n currentUrl.contains('localhost')) {\n return false;\n }\n if (currentUrl.contains('fabushi-flutter-web-prod')) {\n return true;\n }\n }\n return environment == 'production';\n }\n\n static bool get isDevelopment => !isProduction;\n static bool get isStaging => environment == 'staging';\n static bool get isWeb => kIsWeb;\n\n // API配置\n static const String configuredApiBaseUrl = String.fromEnvironment(\n 'API_BASE_URL',\n defaultValue: '',\n );\n static const String primaryBackendUrl = 'https://api.ombhrum.com';\n static const String cloudflareWorkerProdUrl = 'https://api.ombhrum.com';\n static const String cloudflareWorkerDevUrl =\n 'https://fabushi-flutter-web-dev.bhrumom.workers.dev';\n static const String localDevUrl = 'http://localhost:8787';\n static const String publicWebUrl = 'https://flutter.ombhrum.com';\n static const String configuredAiBackendUrl = String.fromEnvironment(\n 'AI_BACKEND_URL',\n defaultValue: '',\n );\n static const String defaultAiBackendUrl = primaryBackendUrl;\n static const String configuredDachengAiWebUrl = String.fromEnvironment(\n 'DACHENG_AI_WEB_URL',\n defaultValue: '',\n );\n static const bool desktopControlEnabled = bool.fromEnvironment(\n 'DACHENG_DESKTOP_CONTROL',\n defaultValue: false,\n );\n static const String configuredOpenClawDeepSeekApiKey = String.fromEnvironment(\n 'DACHENG_OPENCLAW_DEEPSEEK_API_KEY',\n defaultValue: '',\n );\n static const String defaultDachengAiWebUrl =\n 'https://fabushi.ombhrum.com/app/ai';\n\n static String get currentBackendUrl {\n if (configuredApiBaseUrl.isNotEmpty) {\n return configuredApiBaseUrl;\n }\n if (kIsWeb) {\n final currentUrl = Uri.base;\n final host = currentUrl.host;\n if (host.contains('localhost')) {\n return localDevUrl;\n }\n if (host.contains('fabushi-flutter-web-dev')) {\n return cloudflareWorkerDevUrl;\n }\n if (host.contains('fabushi-flutter-web-prod')) {\n return cloudflareWorkerProdUrl;\n }\n }\n return primaryBackendUrl;\n }\n\n static String get apiUrl => currentBackendUrl;\n\n static String get currentAiBackendUrl {\n if (configuredAiBackendUrl.isNotEmpty) {\n return configuredAiBackendUrl;\n }\n return defaultAiBackendUrl;\n }\n\n static String get dachengAiWebUrl {\n if (configuredDachengAiWebUrl.isNotEmpty) {\n return configuredDachengAiWebUrl;\n }\n return defaultDachengAiWebUrl;\n }\n\n static String get openClawDeepSeekProxyBaseUrl {\n final baseUrl = currentAiBackendUrl.replaceFirst(RegExp(r'/+$'), '');\n return '$baseUrl/api/openclaw/deepseek/v1';\n }\n\n /// OpenAI Responses-compatible endpoint consumed by the Mahayana Rust SDK.\n static String get codexDeepSeekResponsesBaseUrl {\n final baseUrl = currentAiBackendUrl.replaceFirst(RegExp(r'/+$'), '');\n return '$baseUrl/codex-deepseek/v1';\n }\n\n static Uri buildDachengAiWebUri({\n String? prompt,\n String? bookTitle,\n String? context,\n }) {\n final base = Uri.parse(dachengAiWebUrl);\n final query = <String, String>{\n ...base.queryParameters,\n if (prompt != null && prompt.trim().isNotEmpty) 'prompt': prompt.trim(),\n if (bookTitle != null && bookTitle.trim().isNotEmpty)\n 'book': bookTitle.trim(),\n if (context != null && context.trim().isNotEmpty)\n 'context': context.trim(),\n };\n return base.replace(queryParameters: query.isEmpty ? null : query);\n }\n\n /// 大厂式 API 网关入口:客户端只允许构造自家后端的相对路径。\n ///\n /// 第三方 API、上游 API、缓存、重试、fallback 都应在后端处理;\n /// App 端不得直接传入 https://... 或 http://... 这类绝对 URL。\n static Uri buildBackendUri(\n String endpoint, {\n Map<String, String>? queryParameters,\n }) {\n final normalizedEndpoint = endpoint.trim();\n if (normalizedEndpoint.isEmpty || !normalizedEndpoint.startsWith('/')) {\n throw ArgumentError(\n 'API endpoint must be a first-party relative path starting with /: $endpoint',\n );\n }\n if (normalizedEndpoint.startsWith('//') ||\n normalizedEndpoint.contains('://')) {\n throw ArgumentError(\n 'API endpoint must not be an absolute or protocol-relative URL: $endpoint',\n );\n }\n\n final baseUrl = currentBackendUrl.replaceFirst(RegExp(r'/+$'), '');\n final uri = Uri.parse('$baseUrl$normalizedEndpoint');\n if (queryParameters == null || queryParameters.isEmpty) {\n return uri;\n }\n return uri.replace(queryParameters: queryParameters);\n }\n\n static String buildBackendUrl(\n String endpoint, {\n Map<String, String>? queryParameters,\n }) {\n return buildBackendUri(\n endpoint,\n queryParameters: queryParameters,\n ).toString();\n }\n\n // 应用信息\n static const String appName = '大乘';\n // 保持与 pubspec.yaml 中的 version 同步,供 Web 回退和服务端比对使用。\n static const String appVersion = '1.0.0';\n static const int appBuildNumber = 400;\n\n // 功能开关\n static const bool enableFirebaseAuth = true;\n static const bool enableAlipay = true;\n static const bool enableAppleIAP = true;\n static const bool enableVideoFeed = true;\n static bool get enableDebugMode => !isProduction;\n\n // 传输配置\n static const int fileChunkSize = 1024;\n static const int maxRetryCount = 3;\n static const int timeoutDuration = 5000;\n static const int maxConcurrentTransfers = 5;\n\n // 缓存配置\n static const int cacheMaxAge = 7;\n static const int maxCacheSize = 100;\n\n // 分页配置\n static const int defaultPageSize = 20;\n static const int maxPageSize = 100;\n\n // 超时配置\n static final Duration requestTimeout = const Duration(seconds: 30);\n static final Duration connectTimeout = const Duration(seconds: 10);\n\n // 重试配置\n static const int maxRetries = 3;\n static final Duration retryDelay = const Duration(seconds: 1);\n\n // 日志配置\n static const bool enableLogging = true;\n static bool get enableNetworkLogging => !isProduction;\n static bool get enablePerformanceLogging => !isProduction;\n\n // 存储键名\n static const String tokenStorageKey = 'auth_token';\n static const String userInfoStorageKey = 'user_info';\n static const String backendUrlStorageKey = 'backend_url';\n static const String testModeStorageKey = 'test_mode';\n\n // API端点\n static String get loginUrl => buildBackendUrl('/api/auth/login');\n static String get registerUrl => buildBackendUrl('/api/auth/register');\n static String get verifyUrl => buildBackendUrl('/api/auth/verify');\n static String get logoutUrl => buildBackendUrl('/api/auth/logout');\n static String get deleteAccountUrl => buildBackendUrl('/api/auth/delete');\n static String get sendVerificationCodeUrl =>\n buildBackendUrl('/api/auth/send-verification-code');\n static String get verifyCodeUrl => buildBackendUrl('/api/auth/verify-code');\n static String get forgotPasswordUrl =>\n buildBackendUrl('/api/auth/forgot-password');\n static String get resetPasswordUrl =>\n buildBackendUrl('/api/auth/reset-password');\n static String get userInfoUrl => buildBackendUrl('/api/auth/user-info');\n static String get bindEmailUrl => buildBackendUrl('/api/auth/bind-email');\n static String get appVersionPolicyUrl =>\n buildBackendUrl('/api/app/version-policy');\n\n static String get agentChatUrl => buildBackendUrl('/api/agent/chat');\n static String get aiQuotaUrl => buildBackendUrl('/api/ai/quota');\n static String agentRunEventsUrl(String runId) =>\n buildBackendUrl('/api/agent/runs/$runId/events');\n static String agentRunCancelUrl(String runId) =>\n buildBackendUrl('/api/agent/runs/$runId/cancel');\n static String agentMessageFeedbackUrl(String messageId) =>\n buildBackendUrl('/api/agent/messages/$messageId/feedback');\n\n static String get alipayCreateOrderUrl =>\n buildBackendUrl('/api/alipay/create-order');\n static String get alipayQueryOrderUrl =>\n buildBackendUrl('/api/alipay/query-order');\n static String get alipayMembershipStatusUrl =>\n buildBackendUrl('/api/alipay/check-membership');\n\n static String get stripeMembershipStatusUrl =>\n buildBackendUrl('/api/stripe/membership-status');\n static String get stripeCreateSubscriptionUrl =>\n buildBackendUrl('/api/stripe/create-subscription');\n static String get stripeSessionStatusUrl =>\n buildBackendUrl('/api/stripe/session-status');\n\n static String get appleVerifyReceiptUrl =>\n buildBackendUrl('/api/apple/verify-receipt');\n static String get purchaseEntitlementUrl =>\n buildBackendUrl('/api/purchases/entitlement');\n static String get walletBalanceUrl => buildBackendUrl('/api/wallet/balance');\n static String get walletSpendUrl => buildBackendUrl('/api/wallet/spend');\n\n static const String zenBuddhaAssetProductId = 'zen_buddha_asset';\n static const String zenBuddhaAssetDisplayName = '3D佛像素材';\n static const String zenBuddhaAssetPriceLabel = '¥33.00';\n static const int zenBuddhaAssetFudeGoldPrice = 33;\n static const String zenBuddhaAssetCardImagePath =\n 'assets/images/zen_buddha_material_card.png';\n\n static String get adminCheckStatusUrl =>\n buildBackendUrl('/api/admin/check-status');\n static String get adminCreateRedeemCodeUrl =>\n buildBackendUrl('/api/admin/create-redeem-code');\n static String get adminRedeemCodesUrl =>\n buildBackendUrl('/api/admin/redeem-codes');\n static String get adminUseRedeemCodeUrl =>\n buildBackendUrl('/api/admin/use-redeem-code');\n static String get adminPurchaseHistoryUrl =>\n buildBackendUrl('/api/admin/purchase-history');\n static String get adminRedeemHistoryUrl =>\n buildBackendUrl('/api/admin/redeem-history');\n\n static String get leaderboardUrl => buildBackendUrl('/api/leaderboard');\n static String get updateTransferDataUrl =>\n buildBackendUrl('/api/leaderboard/update');\n\n static String get healthCheckUrl => buildBackendUrl('/health');\n\n // 3D 佛像模型配置\n // 如果 R2 上需要切换到新的对象键,优先改这里,便于强制绕开旧缓存。\n static const String buddhaModelAssetPath = 'models/buddha_model.model';\n // Android/iOS both use the R2 flutter_scene .model; local cache misses\n // download through AssetLoaderService.\n\n // 当前线上正确模型明显大于 100MB,小于该阈值视为误传/降质文件。\n static const int minBuddhaModelSizeBytes = 100 * 1024 * 1024;\n\n // 请求头\n static Map<String, String> get defaultHeaders => {\n 'Content-Type': 'application/json',\n 'Accept': 'application/json',\n 'User-Agent': 'FabushiApp/${isWeb ? \"Web\" : \"Mobile\"}',\n };\n\n // 错误消息\n static const Map<String, String> errorMessages = {\n 'network_error': '网络连接失败,请检查网络设置',\n 'server_error': '服务器错误,请稍后重试',\n 'timeout_error': '请求超时,请检查网络连接',\n 'auth_error': '认证失败,请重新登录',\n 'permission_error': '权限不足',\n 'validation_error': '数据验证失败',\n 'unknown_error': '未知错误,请联系客服',\n };\n\n // 备用地址\n static List<String> get fallbackUrls {\n return [primaryBackendUrl];\n }\n\n // 调试配置\n static const bool enableApiLogging = true;\n static const bool debugMode = bool.fromEnvironment(\n 'DEBUG',\n defaultValue: false,\n );\n\n static void printConfigInfo() {\n if (kDebugMode) {\n print('=== 应用配置 ===');\n print('环境: ${isProduction ? \"生产\" : \"开发\"}');\n print('平台: ${isWeb ? \"Web\" : \"Native\"}');\n print('API URL: $currentBackendUrl');\n print('================');\n }\n }\n\n static void printCurrentConfig() => printConfigInfo();\n}\n"}, "bd9205ae7b29be90fb73be3283c826d6fd812049": {"t": 1785757945.3367124, "n": 11587, "h": "457c3958c206ce9d", "c": "import 'package:flutter/foundation.dart';\n\n/// 应用配置 - 统一管理所有配置项\nclass AppConfig {\n AppConfig._();\n\n static final AppConfig instance = AppConfig._();\n\n // 环境配置\n static const String environment = String.fromEnvironment(\n 'ENV',\n defaultValue: 'production',\n );\n\n static bool get isProduction {\n if (kIsWeb) {\n final currentUrl = Uri.base.toString();\n if (currentUrl.contains('fabushi-flutter-web-dev') ||\n currentUrl.contains('localhost')) {\n return false;\n }\n if (currentUrl.contains('fabushi-flutter-web-prod')) {\n return true;\n }\n }\n return environment == 'production';\n }\n\n static bool get isDevelopment => !isProduction;\n static bool get isStaging => environment == 'staging';\n static bool get isWeb => kIsWeb;\n\n // API配置\n static const String configuredApiBaseUrl = String.fromEnvironment(\n 'API_BASE_URL',\n defaultValue: '',\n );\n static const String primaryBackendUrl = 'https://api.ombhrum.com';\n static const String cloudflareWorkerProdUrl = 'https://api.ombhrum.com';\n static const String cloudflareWorkerDevUrl =\n 'https://fabushi-flutter-web-dev.bhrumom.workers.dev';\n static const String localDevUrl = 'http://localhost:8787';\n static const String publicWebUrl = 'https://flutter.ombhrum.com';\n static const String configuredAiBackendUrl = String.fromEnvironment(\n 'AI_BACKEND_URL',\n defaultValue: '',\n );\n static const String defaultAiBackendUrl = primaryBackendUrl;\n static const String configuredDachengAiWebUrl = String.fromEnvironment(\n 'DACHENG_AI_WEB_URL',\n defaultValue: '',\n );\n static const bool desktopControlEnabled = bool.fromEnvironment(\n 'DACHENG_DESKTOP_CONTROL',\n defaultValue: false,\n );\n static const String configuredOpenClawDeepSeekApiKey = String.fromEnvironment(\n 'DACHENG_OPENCLAW_DEEPSEEK_API_KEY',\n defaultValue: '',\n );\n static const String defaultDachengAiWebUrl =\n 'https://fabushi.ombhrum.com/app/ai';\n\n static String get currentBackendUrl {\n if (configuredApiBaseUrl.isNotEmpty) {\n return configuredApiBaseUrl;\n }\n if (kIsWeb) {\n final currentUrl = Uri.base;\n final host = currentUrl.host;\n if (host.contains('localhost')) {\n return localDevUrl;\n }\n if (host.contains('fabushi-flutter-web-dev')) {\n return cloudflareWorkerDevUrl;\n }\n if (host.contains('fabushi-flutter-web-prod')) {\n return cloudflareWorkerProdUrl;\n }\n }\n return primaryBackendUrl;\n }\n\n static String get apiUrl => currentBackendUrl;\n\n static String get currentAiBackendUrl {\n if (configuredAiBackendUrl.isNotEmpty) {\n return configuredAiBackendUrl;\n }\n return defaultAiBackendUrl;\n }\n\n static String get dachengAiWebUrl {\n if (configuredDachengAiWebUrl.isNotEmpty) {\n return configuredDachengAiWebUrl;\n }\n return defaultDachengAiWebUrl;\n }\n\n static String get openClawDeepSeekProxyBaseUrl {\n final baseUrl = currentAiBackendUrl.replaceFirst(RegExp(r'/+$'), '');\n return '$baseUrl/api/openclaw/deepseek/v1';\n }\n\n /// OpenAI Responses-compatible endpoint consumed by the Mahayana Rust SDK.\n static String get codexDeepSeekResponsesBaseUrl {\n final baseUrl = currentAiBackendUrl.replaceFirst(RegExp(r'/+$'), '');\n return '$baseUrl/codex-deepseek/v1';\n }\n\n static Uri buildDachengAiWebUri({\n String? prompt,\n String? bookTitle,\n String? context,\n }) {\n final base = Uri.parse(dachengAiWebUrl);\n final query = <String, String>{\n ...base.queryParameters,\n if (prompt != null && prompt.trim().isNotEmpty) 'prompt': prompt.trim(),\n if (bookTitle != null && bookTitle.trim().isNotEmpty)\n 'book': bookTitle.trim(),\n if (context != null && context.trim().isNotEmpty)\n 'context': context.trim(),\n };\n return base.replace(queryParameters: query.isEmpty ? null : query);\n }\n\n /// 大厂式 API 网关入口:客户端只允许构造自家后端的相对路径。\n ///\n /// 第三方 API、上游 API、缓存、重试、fallback 都应在后端处理;\n /// App 端不得直接传入 https://... 或 http://... 这类绝对 URL。\n static Uri buildBackendUri(\n String endpoint, {\n Map<String, String>? queryParameters,\n }) {\n final normalizedEndpoint = endpoint.trim();\n if (normalizedEndpoint.isEmpty || !normalizedEndpoint.startsWith('/')) {\n throw ArgumentError(\n 'API endpoint must be a first-party relative path starting with /: $endpoint',\n );\n }\n if (normalizedEndpoint.startsWith('//') ||\n normalizedEndpoint.contains('://')) {\n throw ArgumentError(\n 'API endpoint must not be an absolute or protocol-relative URL: $endpoint',\n );\n }\n\n final baseUrl = currentBackendUrl.replaceFirst(RegExp(r'/+$'), '');\n final uri = Uri.parse('$baseUrl$normalizedEndpoint');\n if (queryParameters == null || queryParameters.isEmpty) {\n return uri;\n }\n return uri.replace(queryParameters: queryParameters);\n }\n\n static String buildBackendUrl(\n String endpoint, {\n Map<String, String>? queryParameters,\n }) {\n return buildBackendUri(\n endpoint,\n queryParameters: queryParameters,\n ).toString();\n }\n\n // 应用信息\n static const String appName = '大乘';\n // 保持与 pubspec.yaml 中的 version 同步,供 Web 回退和服务端比对使用。\n static const String appVersion = '1.0.0';\n static const int appBuildNumber = 400;\n\n // 功能开关\n static const bool enableFirebaseAuth = true;\n static const bool enableAlipay = true;\n static const bool enableAppleIAP = true;\n static const bool enableVideoFeed = true;\n static bool get enableDebugMode => !isProduction;\n\n // 传输配置\n static const int fileChunkSize = 1024;\n static const int maxRetryCount = 3;\n static const int timeoutDuration = 5000;\n static const int maxConcurrentTransfers = 5;\n\n // 缓存配置\n static const int cacheMaxAge = 7;\n static const int maxCacheSize = 100;\n\n // 分页配置\n static const int defaultPageSize = 20;\n static const int maxPageSize = 100;\n\n // 超时配置\n static final Duration requestTimeout = const Duration(seconds: 30);\n static final Duration connectTimeout = const Duration(seconds: 10);\n\n // 重试配置\n static const int maxRetries = 3;\n static final Duration retryDelay = const Duration(seconds: 1);\n\n // 日志配置\n static const bool enableLogging = true;\n static bool get enableNetworkLogging => !isProduction;\n static bool get enablePerformanceLogging => !isProduction;\n\n // 存储键名\n static const String tokenStorageKey = 'auth_token';\n static const String userInfoStorageKey = 'user_info';\n static const String backendUrlStorageKey = 'backend_url';\n static const String testModeStorageKey = 'test_mode';\n\n // API端点\n static String get loginUrl => buildBackendUrl('/api/auth/login');\n static String get registerUrl => buildBackendUrl('/api/auth/register');\n static String get verifyUrl => buildBackendUrl('/api/auth/verify');\n static String get logoutUrl => buildBackendUrl('/api/auth/logout');\n static String get deleteAccountUrl => buildBackendUrl('/api/auth/delete');\n static String get sendVerificationCodeUrl =>\n buildBackendUrl('/api/auth/send-verification-code');\n static String get verifyCodeUrl => buildBackendUrl('/api/auth/verify-code');\n static String get forgotPasswordUrl =>\n buildBackendUrl('/api/auth/forgot-password');\n static String get resetPasswordUrl =>\n buildBackendUrl('/api/auth/reset-password');\n static String get userInfoUrl => buildBackendUrl('/api/auth/user-info');\n static String get bindEmailUrl => buildBackendUrl('/api/auth/bind-email');\n static String get appVersionPolicyUrl =>\n buildBackendUrl('/api/app/version-policy');\n\n static String get agentChatUrl => buildBackendUrl('/api/agent/chat');\n static String get aiQuotaUrl => buildBackendUrl('/api/ai/quota');\n static String agentRunEventsUrl(String runId) =>\n buildBackendUrl('/api/agent/runs/$runId/events');\n static String agentRunCancelUrl(String runId) =>\n buildBackendUrl('/api/agent/runs/$runId/cancel');\n static String agentMessageFeedbackUrl(String messageId) =>\n buildBackendUrl('/api/agent/messages/$messageId/feedback');\n\n static String get alipayCreateOrderUrl =>\n buildBackendUrl('/api/alipay/create-order');\n static String get alipayQueryOrderUrl =>\n buildBackendUrl('/api/alipay/query-order');\n static String get alipayMembershipStatusUrl =>\n buildBackendUrl('/api/alipay/check-membership');\n\n static String get stripeMembershipStatusUrl =>\n buildBackendUrl('/api/stripe/membership-status');\n static String get stripeCreateSubscriptionUrl =>\n buildBackendUrl('/api/stripe/create-subscription');\n static String get stripeSessionStatusUrl =>\n buildBackendUrl('/api/stripe/session-status');\n\n static String get appleVerifyReceiptUrl =>\n buildBackendUrl('/api/apple/verify-receipt');\n static String get purchaseEntitlementUrl =>\n buildBackendUrl('/api/purchases/entitlement');\n static String get walletBalanceUrl => buildBackendUrl('/api/wallet/balance');\n static String get walletSpendUrl => buildBackendUrl('/api/wallet/spend');\n\n static const String zenBuddhaAssetProductId = 'zen_buddha_asset';\n static const String zenBuddhaAssetDisplayName = '3D佛像素材';\n static const String zenBuddhaAssetPriceLabel = '¥33.00';\n static const int zenBuddhaAssetFudeGoldPrice = 33;\n static const String zenBuddhaAssetCardImagePath =\n 'assets/images/zen_buddha_material_card.png';\n\n static String get adminCheckStatusUrl =>\n buildBackendUrl('/api/admin/check-status');\n static String get adminCreateRedeemCodeUrl =>\n buildBackendUrl('/api/admin/create-redeem-code');\n static String get adminRedeemCodesUrl =>\n buildBackendUrl('/api/admin/redeem-codes');\n static String get adminUseRedeemCodeUrl =>\n buildBackendUrl('/api/admin/use-redeem-code');\n static String get adminPurchaseHistoryUrl =>\n buildBackendUrl('/api/admin/purchase-history');\n static String get adminRedeemHistoryUrl =>\n buildBackendUrl('/api/admin/redeem-history');\n\n static String get leaderboardUrl => buildBackendUrl('/api/leaderboard');\n static String get updateTransferDataUrl =>\n buildBackendUrl('/api/leaderboard/update');\n\n static String get healthCheckUrl => buildBackendUrl('/health');\n\n // 3D 佛像模型配置\n // 如果 R2 上需要切换到新的对象键,优先改这里,便于强制绕开旧缓存。\n static const String buddhaModelAssetPath = 'models/buddha_model.model';\n // Android/iOS both use the R2 flutter_scene .model; local cache misses\n // download through AssetLoaderService.\n\n // 当前线上正确模型明显大于 100MB,小于该阈值视为误传/降质文件。\n static const int minBuddhaModelSizeBytes = 100 * 1024 * 1024;\n\n // 请求头\n static Map<String, String> get defaultHeaders => {\n 'Content-Type': 'application/json',\n 'Accept': 'application/json',\n 'User-Agent': 'FabushiApp/${isWeb ? \"Web\" : \"Mobile\"}',\n };\n\n // 错误消息\n static const Map<String, String> errorMessages = {\n 'network_error': '网络连接失败,请检查网络设置',\n 'server_error': '服务器错误,请稍后重试',\n 'timeout_error': '请求超时,请检查网络连接',\n 'auth_error': '认证失败,请重新登录',\n 'permission_error': '权限不足',\n 'validation_error': '数据验证失败',\n 'unknown_error': '未知错误,请联系客服',\n };\n\n // 备用地址\n static List<String> get fallbackUrls {\n return [primaryBackendUrl];\n }\n\n // 调试配置\n static const bool enableApiLogging = true;\n static const bool debugMode = bool.fromEnvironment(\n 'DEBUG',\n defaultValue: false,\n );\n\n static void printConfigInfo() {\n if (kDebugMode) {\n print('=== 应用配置 ===');\n print('环境: ${isProduction ? \"生产\" : \"开发\"}');\n print('平台: ${isWeb ? \"Web\" : \"Native\"}');\n print('API URL: $currentBackendUrl');\n print('================');\n }\n }\n\n static void printCurrentConfig() => printConfigInfo();\n}\n"}, "6350892067731a13abd2a62d2fcebe7fc6a97fc4": {"t": 1785757946.9795732, "n": 11337, "h": "97225a52695e76a8", "c": "import 'package:flutter/foundation.dart';\n\n/// 应用配置 - 统一管理所有配置项\nclass AppConfig {\n AppConfig._();\n\n static final AppConfig instance = AppConfig._();\n\n // 环境配置\n static const String environment = String.fromEnvironment(\n 'ENV',\n defaultValue: 'production',\n );\n\n static bool get isProduction {\n if (kIsWeb) {\n final currentUrl = Uri.base.toString();\n if (currentUrl.contains('fabushi-flutter-web-dev') ||\n currentUrl.contains('localhost')) {\n return false;\n }\n if (currentUrl.contains('fabushi-flutter-web-prod')) {\n return true;\n }\n }\n return environment == 'production';\n }\n\n static bool get isDevelopment => !isProduction;\n static bool get isStaging => environment == 'staging';\n static bool get isWeb => kIsWeb;\n\n // API配置\n static const String configuredApiBaseUrl = String.fromEnvironment(\n 'API_BASE_URL',\n defaultValue: '',\n );\n static const String primaryBackendUrl = 'https://api.ombhrum.com';\n static const String cloudflareWorkerProdUrl = 'https://api.ombhrum.com';\n static const String cloudflareWorkerDevUrl =\n 'https://fabushi-flutter-web-dev.bhrumom.workers.dev';\n static const String localDevUrl = 'http://localhost:8787';\n static const String publicWebUrl = 'https://flutter.ombhrum.com';\n static const String configuredAiBackendUrl = String.fromEnvironment(\n 'AI_BACKEND_URL',\n defaultValue: '',\n );\n static const String defaultAiBackendUrl = primaryBackendUrl;\n static const String configuredDachengAiWebUrl = String.fromEnvironment(\n 'DACHENG_AI_WEB_URL',\n defaultValue: '',\n );\n static const bool desktopControlEnabled = bool.fromEnvironment(\n 'DACHENG_DESKTOP_CONTROL',\n defaultValue: false,\n );\n static const String configuredOpenClawDeepSeekApiKey = String.fromEnvironment(\n 'DACHENG_OPENCLAW_DEEPSEEK_API_KEY',\n defaultValue: '',\n );\n static const String defaultDachengAiWebUrl =\n 'https://fabushi.ombhrum.com/app/ai';\n\n static String get currentBackendUrl {\n if (configuredApiBaseUrl.isNotEmpty) {\n return configuredApiBaseUrl;\n }\n if (kIsWeb) {\n final currentUrl = Uri.base;\n final host = currentUrl.host;\n if (host.contains('localhost')) {\n return localDevUrl;\n }\n if (host.contains('fabushi-flutter-web-dev')) {\n return cloudflareWorkerDevUrl;\n }\n if (host.contains('fabushi-flutter-web-prod')) {\n return cloudflareWorkerProdUrl;\n }\n }\n return primaryBackendUrl;\n }\n\n static String get apiUrl => currentBackendUrl;\n\n static String get currentAiBackendUrl {\n if (configuredAiBackendUrl.isNotEmpty) {\n return configuredAiBackendUrl;\n }\n return defaultAiBackendUrl;\n }\n\n static String get dachengAiWebUrl {\n if (configuredDachengAiWebUrl.isNotEmpty) {\n return configuredDachengAiWebUrl;\n }\n return defaultDachengAiWebUrl;\n }\n\n static String get openClawDeepSeekProxyBaseUrl {\n final baseUrl = currentAiBackendUrl.replaceFirst(RegExp(r'/+$'), '');\n return '$baseUrl/api/openclaw/deepseek/v1';\n }\n\n static Uri buildDachengAiWebUri({\n String? prompt,\n String? bookTitle,\n String? context,\n }) {\n final base = Uri.parse(dachengAiWebUrl);\n final query = <String, String>{\n ...base.queryParameters,\n if (prompt != null && prompt.trim().isNotEmpty) 'prompt': prompt.trim(),\n if (bookTitle != null && bookTitle.trim().isNotEmpty)\n 'book': bookTitle.trim(),\n if (context != null && context.trim().isNotEmpty)\n 'context': context.trim(),\n };\n return base.replace(queryParameters: query.isEmpty ? null : query);\n }\n\n /// 大厂式 API 网关入口:客户端只允许构造自家后端的相对路径。\n ///\n /// 第三方 API、上游 API、缓存、重试、fallback 都应在后端处理;\n /// App 端不得直接传入 https://... 或 http://... 这类绝对 URL。\n static Uri buildBackendUri(\n String endpoint, {\n Map<String, String>? queryParameters,\n }) {\n final normalizedEndpoint = endpoint.trim();\n if (normalizedEndpoint.isEmpty || !normalizedEndpoint.startsWith('/')) {\n throw ArgumentError(\n 'API endpoint must be a first-party relative path starting with /: $endpoint',\n );\n }\n if (normalizedEndpoint.startsWith('//') ||\n normalizedEndpoint.contains('://')) {\n throw ArgumentError(\n 'API endpoint must not be an absolute or protocol-relative URL: $endpoint',\n );\n }\n\n final baseUrl = currentBackendUrl.replaceFirst(RegExp(r'/+$'), '');\n final uri = Uri.parse('$baseUrl$normalizedEndpoint');\n if (queryParameters == null || queryParameters.isEmpty) {\n return uri;\n }\n return uri.replace(queryParameters: queryParameters);\n }\n\n static String buildBackendUrl(\n String endpoint, {\n Map<String, String>? queryParameters,\n }) {\n return buildBackendUri(\n endpoint,\n queryParameters: queryParameters,\n ).toString();\n }\n\n // 应用信息\n static const String appName = '大乘';\n // 保持与 pubspec.yaml 中的 version 同步,供 Web 回退和服务端比对使用。\n static const String appVersion = '1.0.0';\n static const int appBuildNumber = 400;\n\n // 功能开关\n static const bool enableFirebaseAuth = true;\n static const bool enableAlipay = true;\n static const bool enableAppleIAP = true;\n static const bool enableVideoFeed = true;\n static bool get enableDebugMode => !isProduction;\n\n // 传输配置\n static const int fileChunkSize = 1024;\n static const int maxRetryCount = 3;\n static const int timeoutDuration = 5000;\n static const int maxConcurrentTransfers = 5;\n\n // 缓存配置\n static const int cacheMaxAge = 7;\n static const int maxCacheSize = 100;\n\n // 分页配置\n static const int defaultPageSize = 20;\n static const int maxPageSize = 100;\n\n // 超时配置\n static final Duration requestTimeout = const Duration(seconds: 30);\n static final Duration connectTimeout = const Duration(seconds: 10);\n\n // 重试配置\n static const int maxRetries = 3;\n static final Duration retryDelay = const Duration(seconds: 1);\n\n // 日志配置\n static const bool enableLogging = true;\n static bool get enableNetworkLogging => !isProduction;\n static bool get enablePerformanceLogging => !isProduction;\n\n // 存储键名\n static const String tokenStorageKey = 'auth_token';\n static const String userInfoStorageKey = 'user_info';\n static const String backendUrlStorageKey = 'backend_url';\n static const String testModeStorageKey = 'test_mode';\n\n // API端点\n static String get loginUrl => buildBackendUrl('/api/auth/login');\n static String get registerUrl => buildBackendUrl('/api/auth/register');\n static String get verifyUrl => buildBackendUrl('/api/auth/verify');\n static String get logoutUrl => buildBackendUrl('/api/auth/logout');\n static String get deleteAccountUrl => buildBackendUrl('/api/auth/delete');\n static String get sendVerificationCodeUrl =>\n buildBackendUrl('/api/auth/send-verification-code');\n static String get verifyCodeUrl => buildBackendUrl('/api/auth/verify-code');\n static String get forgotPasswordUrl =>\n buildBackendUrl('/api/auth/forgot-password');\n static String get resetPasswordUrl =>\n buildBackendUrl('/api/auth/reset-password');\n static String get userInfoUrl => buildBackendUrl('/api/auth/user-info');\n static String get bindEmailUrl => buildBackendUrl('/api/auth/bind-email');\n static String get appVersionPolicyUrl =>\n buildBackendUrl('/api/app/version-policy');\n\n static String get agentChatUrl => buildBackendUrl('/api/agent/chat');\n static String get aiQuotaUrl => buildBackendUrl('/api/ai/quota');\n static String agentRunEventsUrl(String runId) =>\n buildBackendUrl('/api/agent/runs/$runId/events');\n static String agentRunCancelUrl(String runId) =>\n buildBackendUrl('/api/agent/runs/$runId/cancel');\n static String agentMessageFeedbackUrl(String messageId) =>\n buildBackendUrl('/api/agent/messages/$messageId/feedback');\n\n static String get alipayCreateOrderUrl =>\n buildBackendUrl('/api/alipay/create-order');\n static String get alipayQueryOrderUrl =>\n buildBackendUrl('/api/alipay/query-order');\n static String get alipayMembershipStatusUrl =>\n buildBackendUrl('/api/alipay/check-membership');\n\n static String get stripeMembershipStatusUrl =>\n buildBackendUrl('/api/stripe/membership-status');\n static String get stripeCreateSubscriptionUrl =>\n buildBackendUrl('/api/stripe/create-subscription');\n static String get stripeSessionStatusUrl =>\n buildBackendUrl('/api/stripe/session-status');\n\n static String get appleVerifyReceiptUrl =>\n buildBackendUrl('/api/apple/verify-receipt');\n static String get purchaseEntitlementUrl =>\n buildBackendUrl('/api/purchases/entitlement');\n static String get walletBalanceUrl => buildBackendUrl('/api/wallet/balance');\n static String get walletSpendUrl => buildBackendUrl('/api/wallet/spend');\n\n static const String zenBuddhaAssetProductId = 'zen_buddha_asset';\n static const String zenBuddhaAssetDisplayName = '3D佛像素材';\n static const String zenBuddhaAssetPriceLabel = '¥33.00';\n static const int zenBuddhaAssetFudeGoldPrice = 33;\n static const String zenBuddhaAssetCardImagePath =\n 'assets/images/zen_buddha_material_card.png';\n\n static String get adminCheckStatusUrl =>\n buildBackendUrl('/api/admin/check-status');\n static String get adminCreateRedeemCodeUrl =>\n buildBackendUrl('/api/admin/create-redeem-code');\n static String get adminRedeemCodesUrl =>\n buildBackendUrl('/api/admin/redeem-codes');\n static String get adminUseRedeemCodeUrl =>\n buildBackendUrl('/api/admin/use-redeem-code');\n static String get adminPurchaseHistoryUrl =>\n buildBackendUrl('/api/admin/purchase-history');\n static String get adminRedeemHistoryUrl =>\n buildBackendUrl('/api/admin/redeem-history');\n\n static String get leaderboardUrl => buildBackendUrl('/api/leaderboard');\n static String get updateTransferDataUrl =>\n buildBackendUrl('/api/leaderboard/update');\n\n static String get healthCheckUrl => buildBackendUrl('/health');\n\n // 3D 佛像模型配置\n // 如果 R2 上需要切换到新的对象键,优先改这里,便于强制绕开旧缓存。\n static const String buddhaModelAssetPath = 'models/buddha_model.model';\n // Android/iOS both use the R2 flutter_scene .model; local cache misses\n // download through AssetLoaderService.\n\n // 当前线上正确模型明显大于 100MB,小于该阈值视为误传/降质文件。\n static const int minBuddhaModelSizeBytes = 100 * 1024 * 1024;\n\n // 请求头\n static Map<String, String> get defaultHeaders => {\n 'Content-Type': 'application/json',\n 'Accept': 'application/json',\n 'User-Agent': 'FabushiApp/${isWeb ? \"Web\" : \"Mobile\"}',\n };\n\n // 错误消息\n static const Map<String, String> errorMessages = {\n 'network_error': '网络连接失败,请检查网络设置',\n 'server_error': '服务器错误,请稍后重试',\n 'timeout_error': '请求超时,请检查网络连接',\n 'auth_error': '认证失败,请重新登录',\n 'permission_error': '权限不足',\n 'validation_error': '数据验证失败',\n 'unknown_error': '未知错误,请联系客服',\n };\n\n // 备用地址\n static List<String> get fallbackUrls {\n return [primaryBackendUrl];\n }\n\n // 调试配置\n static const bool enableApiLogging = true;\n static const bool debugMode = bool.fromEnvironment(\n 'DEBUG',\n defaultValue: false,\n );\n\n static void printConfigInfo() {\n if (kDebugMode) {\n print('=== 应用配置 ===');\n print('环境: ${isProduction ? \"生产\" : \"开发\"}');\n print('平台: ${isWeb ? \"Web\" : \"Native\"}');\n print('API URL: $currentBackendUrl');\n print('================');\n }\n }\n\n static void printCurrentConfig() => printConfigInfo();\n}\n"}, "eabfd4ea3e2cd4625bf4eeec4b45947c3f2f00a3": {"t": 1785757948.7922657, "n": 11159, "h": "3b765aa94b8fa38a", "c": "import 'package:flutter/foundation.dart';\n\n/// 应用配置 - 统一管理所有配置项\nclass AppConfig {\n AppConfig._();\n\n static final AppConfig instance = AppConfig._();\n\n // 环境配置\n static const String environment = String.fromEnvironment(\n 'ENV',\n defaultValue: 'production',\n );\n\n static bool get isProduction {\n if (kIsWeb) {\n final currentUrl = Uri.base.toString();\n if (currentUrl.contains('fabushi-flutter-web-dev') ||\n currentUrl.contains('localhost')) {\n return false;\n }\n if (currentUrl.contains('fabushi-flutter-web-prod')) {\n return true;\n }\n }\n return environment == 'production';\n }\n\n static bool get isDevelopment => !isProduction;\n static bool get isStaging => environment == 'staging';\n static bool get isWeb => kIsWeb;\n\n // API配置\n static const String configuredApiBaseUrl = String.fromEnvironment(\n 'API_BASE_URL',\n defaultValue: '',\n );\n static const String primaryBackendUrl = 'https://api.ombhrum.com';\n static const String cloudflareWorkerProdUrl = 'https://api.ombhrum.com';\n static const String cloudflareWorkerDevUrl =\n 'https://fabushi-flutter-web-dev.bhrumom.workers.dev';\n static const String localDevUrl = 'http://localhost:8787';\n static const String publicWebUrl = 'https://flutter.ombhrum.com';\n static const String configuredAiBackendUrl = String.fromEnvironment(\n 'AI_BACKEND_URL',\n defaultValue: '',\n );\n static const String defaultAiBackendUrl = primaryBackendUrl;\n static const String configuredDachengAiWebUrl = String.fromEnvironment(\n 'DACHENG_AI_WEB_URL',\n defaultValue: '',\n );\n static const bool desktopControlEnabled = bool.fromEnvironment(\n 'DACHENG_DESKTOP_CONTROL',\n defaultValue: false,\n );\n static const String configuredOpenClawDeepSeekApiKey = String.fromEnvironment(\n 'DACHENG_OPENCLAW_DEEPSEEK_API_KEY',\n defaultValue: '',\n );\n static const String defaultDachengAiWebUrl =\n 'https://fabushi.ombhrum.com/app/ai';\n\n static String get currentBackendUrl {\n if (configuredApiBaseUrl.isNotEmpty) {\n return configuredApiBaseUrl;\n }\n if (kIsWeb) {\n final currentUrl = Uri.base;\n final host = currentUrl.host;\n if (host.contains('localhost')) {\n return localDevUrl;\n }\n if (host.contains('fabushi-flutter-web-dev')) {\n return cloudflareWorkerDevUrl;\n }\n if (host.contains('fabushi-flutter-web-prod')) {\n return cloudflareWorkerProdUrl;\n }\n }\n return primaryBackendUrl;\n }\n\n static String get apiUrl => currentBackendUrl;\n\n static String get currentAiBackendUrl {\n if (configuredAiBackendUrl.isNotEmpty) {\n return configuredAiBackendUrl;\n }\n return defaultAiBackendUrl;\n }\n\n static String get dachengAiWebUrl {\n if (configuredDachengAiWebUrl.isNotEmpty) {\n return configuredDachengAiWebUrl;\n }\n return defaultDachengAiWebUrl;\n }\n\n static Uri buildDachengAiWebUri({\n String? prompt,\n String? bookTitle,\n String? context,\n }) {\n final base = Uri.parse(dachengAiWebUrl);\n final query = <String, String>{\n ...base.queryParameters,\n if (prompt != null && prompt.trim().isNotEmpty) 'prompt': prompt.trim(),\n if (bookTitle != null && bookTitle.trim().isNotEmpty)\n 'book': bookTitle.trim(),\n if (context != null && context.trim().isNotEmpty)\n 'context': context.trim(),\n };\n return base.replace(queryParameters: query.isEmpty ? null : query);\n }\n\n /// 大厂式 API 网关入口:客户端只允许构造自家后端的相对路径。\n ///\n /// 第三方 API、上游 API、缓存、重试、fallback 都应在后端处理;\n /// App 端不得直接传入 https://... 或 http://... 这类绝对 URL。\n static Uri buildBackendUri(\n String endpoint, {\n Map<String, String>? queryParameters,\n }) {\n final normalizedEndpoint = endpoint.trim();\n if (normalizedEndpoint.isEmpty || !normalizedEndpoint.startsWith('/')) {\n throw ArgumentError(\n 'API endpoint must be a first-party relative path starting with /: $endpoint',\n );\n }\n if (normalizedEndpoint.startsWith('//') ||\n normalizedEndpoint.contains('://')) {\n throw ArgumentError(\n 'API endpoint must not be an absolute or protocol-relative URL: $endpoint',\n );\n }\n\n final baseUrl = currentBackendUrl.replaceFirst(RegExp(r'/+$'), '');\n final uri = Uri.parse('$baseUrl$normalizedEndpoint');\n if (queryParameters == null || queryParameters.isEmpty) {\n return uri;\n }\n return uri.replace(queryParameters: queryParameters);\n }\n\n static String buildBackendUrl(\n String endpoint, {\n Map<String, String>? queryParameters,\n }) {\n return buildBackendUri(\n endpoint,\n queryParameters: queryParameters,\n ).toString();\n }\n\n // 应用信息\n static const String appName = '大乘';\n // 保持与 pubspec.yaml 中的 version 同步,供 Web 回退和服务端比对使用。\n static const String appVersion = '1.0.0';\n static const int appBuildNumber = 400;\n\n // 功能开关\n static const bool enableFirebaseAuth = true;\n static const bool enableAlipay = true;\n static const bool enableAppleIAP = true;\n static const bool enableVideoFeed = true;\n static bool get enableDebugMode => !isProduction;\n\n // 传输配置\n static const int fileChunkSize = 1024;\n static const int maxRetryCount = 3;\n static const int timeoutDuration = 5000;\n static const int maxConcurrentTransfers = 5;\n\n // 缓存配置\n static const int cacheMaxAge = 7;\n static const int maxCacheSize = 100;\n\n // 分页配置\n static const int defaultPageSize = 20;\n static const int maxPageSize = 100;\n\n // 超时配置\n static final Duration requestTimeout = const Duration(seconds: 30);\n static final Duration connectTimeout = const Duration(seconds: 10);\n\n // 重试配置\n static const int maxRetries = 3;\n static final Duration retryDelay = const Duration(seconds: 1);\n\n // 日志配置\n static const bool enableLogging = true;\n static bool get enableNetworkLogging => !isProduction;\n static bool get enablePerformanceLogging => !isProduction;\n\n // 存储键名\n static const String tokenStorageKey = 'auth_token';\n static const String userInfoStorageKey = 'user_info';\n static const String backendUrlStorageKey = 'backend_url';\n static const String testModeStorageKey = 'test_mode';\n\n // API端点\n static String get loginUrl => buildBackendUrl('/api/auth/login');\n static String get registerUrl => buildBackendUrl('/api/auth/register');\n static String get verifyUrl => buildBackendUrl('/api/auth/verify');\n static String get logoutUrl => buildBackendUrl('/api/auth/logout');\n static String get deleteAccountUrl => buildBackendUrl('/api/auth/delete');\n static String get sendVerificationCodeUrl =>\n buildBackendUrl('/api/auth/send-verification-code');\n static String get verifyCodeUrl => buildBackendUrl('/api/auth/verify-code');\n static String get forgotPasswordUrl =>\n buildBackendUrl('/api/auth/forgot-password');\n static String get resetPasswordUrl =>\n buildBackendUrl('/api/auth/reset-password');\n static String get userInfoUrl => buildBackendUrl('/api/auth/user-info');\n static String get bindEmailUrl => buildBackendUrl('/api/auth/bind-email');\n static String get appVersionPolicyUrl =>\n buildBackendUrl('/api/app/version-policy');\n\n static String get agentChatUrl => buildBackendUrl('/api/agent/chat');\n static String get aiQuotaUrl => buildBackendUrl('/api/ai/quota');\n static String agentRunEventsUrl(String runId) =>\n buildBackendUrl('/api/agent/runs/$runId/events');\n static String agentRunCancelUrl(String runId) =>\n buildBackendUrl('/api/agent/runs/$runId/cancel');\n static String agentMessageFeedbackUrl(String messageId) =>\n buildBackendUrl('/api/agent/messages/$messageId/feedback');\n\n static String get alipayCreateOrderUrl =>\n buildBackendUrl('/api/alipay/create-order');\n static String get alipayQueryOrderUrl =>\n buildBackendUrl('/api/alipay/query-order');\n static String get alipayMembershipStatusUrl =>\n buildBackendUrl('/api/alipay/check-membership');\n\n static String get stripeMembershipStatusUrl =>\n buildBackendUrl('/api/stripe/membership-status');\n static String get stripeCreateSubscriptionUrl =>\n buildBackendUrl('/api/stripe/create-subscription');\n static String get stripeSessionStatusUrl =>\n buildBackendUrl('/api/stripe/session-status');\n\n static String get appleVerifyReceiptUrl =>\n buildBackendUrl('/api/apple/verify-receipt');\n static String get purchaseEntitlementUrl =>\n buildBackendUrl('/api/purchases/entitlement');\n static String get walletBalanceUrl => buildBackendUrl('/api/wallet/balance');\n static String get walletSpendUrl => buildBackendUrl('/api/wallet/spend');\n\n static const String zenBuddhaAssetProductId = 'zen_buddha_asset';\n static const String zenBuddhaAssetDisplayName = '3D佛像素材';\n static const String zenBuddhaAssetPriceLabel = '¥33.00';\n static const int zenBuddhaAssetFudeGoldPrice = 33;\n static const String zenBuddhaAssetCardImagePath =\n 'assets/images/zen_buddha_material_card.png';\n\n static String get adminCheckStatusUrl =>\n buildBackendUrl('/api/admin/check-status');\n static String get adminCreateRedeemCodeUrl =>\n buildBackendUrl('/api/admin/create-redeem-code');\n static String get adminRedeemCodesUrl =>\n buildBackendUrl('/api/admin/redeem-codes');\n static String get adminUseRedeemCodeUrl =>\n buildBackendUrl('/api/admin/use-redeem-code');\n static String get adminPurchaseHistoryUrl =>\n buildBackendUrl('/api/admin/purchase-history');\n static String get adminRedeemHistoryUrl =>\n buildBackendUrl('/api/admin/redeem-history');\n\n static String get leaderboardUrl => buildBackendUrl('/api/leaderboard');\n static String get updateTransferDataUrl =>\n buildBackendUrl('/api/leaderboard/update');\n\n static String get healthCheckUrl => buildBackendUrl('/health');\n\n // 3D 佛像模型配置\n // 如果 R2 上需要切换到新的对象键,优先改这里,便于强制绕开旧缓存。\n static const String buddhaModelAssetPath = 'models/buddha_model.model';\n // Android/iOS both use the R2 flutter_scene .model; local cache misses\n // download through AssetLoaderService.\n\n // 当前线上正确模型明显大于 100MB,小于该阈值视为误传/降质文件。\n static const int minBuddhaModelSizeBytes = 100 * 1024 * 1024;\n\n // 请求头\n static Map<String, String> get defaultHeaders => {\n 'Content-Type': 'application/json',\n 'Accept': 'application/json',\n 'User-Agent': 'FabushiApp/${isWeb ? \"Web\" : \"Mobile\"}',\n };\n\n // 错误消息\n static const Map<String, String> errorMessages = {\n 'network_error': '网络连接失败,请检查网络设置',\n 'server_error': '服务器错误,请稍后重试',\n 'timeout_error': '请求超时,请检查网络连接',\n 'auth_error': '认证失败,请重新登录',\n 'permission_error': '权限不足',\n 'validation_error': '数据验证失败',\n 'unknown_error': '未知错误,请联系客服',\n };\n\n // 备用地址\n static List<String> get fallbackUrls {\n return [primaryBackendUrl];\n }\n\n // 调试配置\n static const bool enableApiLogging = true;\n static const bool debugMode = bool.fromEnvironment(\n 'DEBUG',\n defaultValue: false,\n );\n\n static void printConfigInfo() {\n if (kDebugMode) {\n print('=== 应用配置 ===');\n print('环境: ${isProduction ? \"生产\" : \"开发\"}');\n print('平台: ${isWeb ? \"Web\" : \"Native\"}');\n print('API URL: $currentBackendUrl');\n print('================');\n }\n }\n\n static void printCurrentConfig() => printConfigInfo();\n}\n"}, "2662ff6b6da15829552fd211f3d2aba35ca41fc0": {"t": 1785757951.625046, "n": 10950, "h": "ea6b8dcfbd799f6a", "c": "import 'package:flutter/foundation.dart';\n\n/// 应用配置 - 统一管理所有配置项\nclass AppConfig {\n AppConfig._();\n\n static final AppConfig instance = AppConfig._();\n\n // 环境配置\n static const String environment = String.fromEnvironment(\n 'ENV',\n defaultValue: 'production',\n );\n\n static bool get isProduction {\n if (kIsWeb) {\n final currentUrl = Uri.base.toString();\n if (currentUrl.contains('fabushi-flutter-web-dev') ||\n currentUrl.contains('localhost')) {\n return false;\n }\n if (currentUrl.contains('fabushi-flutter-web-prod')) {\n return true;\n }\n }\n return environment == 'production';\n }\n\n static bool get isDevelopment => !isProduction;\n static bool get isStaging => environment == 'staging';\n static bool get isWeb => kIsWeb;\n\n // API配置\n static const String configuredApiBaseUrl = String.fromEnvironment(\n 'API_BASE_URL',\n defaultValue: '',\n );\n static const String primaryBackendUrl = 'https://api.ombhrum.com';\n static const String cloudflareWorkerProdUrl = 'https://api.ombhrum.com';\n static const String cloudflareWorkerDevUrl =\n 'https://fabushi-flutter-web-dev.bhrumom.workers.dev';\n static const String localDevUrl = 'http://localhost:8787';\n static const String publicWebUrl = 'https://flutter.ombhrum.com';\n static const String configuredAiBackendUrl = String.fromEnvironment(\n 'AI_BACKEND_URL',\n defaultValue: '',\n );\n static const String defaultAiBackendUrl = primaryBackendUrl;\n static const String configuredDachengAiWebUrl = String.fromEnvironment(\n 'DACHENG_AI_WEB_URL',\n defaultValue: '',\n );\n static const bool desktopControlEnabled = bool.fromEnvironment(\n 'DACHENG_DESKTOP_CONTROL',\n defaultValue: false,\n );\n static const String configuredOpenClawDeepSeekApiKey = String.fromEnvironment(\n 'DACHENG_OPENCLAW_DEEPSEEK_API_KEY',\n defaultValue: '',\n );\n static const String defaultDachengAiWebUrl =\n 'https://fabushi.ombhrum.com/app/ai';\n\n static String get currentBackendUrl {\n if (configuredApiBaseUrl.isNotEmpty) {\n return configuredApiBaseUrl;\n }\n if (kIsWeb) {\n final currentUrl = Uri.base;\n final host = currentUrl.host;\n if (host.contains('localhost')) {\n return localDevUrl;\n }\n if (host.contains('fabushi-flutter-web-dev')) {\n return cloudflareWorkerDevUrl;\n }\n if (host.contains('fabushi-flutter-web-prod')) {\n return cloudflareWorkerProdUrl;\n }\n }\n return primaryBackendUrl;\n }\n\n static String get apiUrl => currentBackendUrl;\n\n static String get currentAiBackendUrl {\n if (configuredAiBackendUrl.isNotEmpty) {\n return configuredAiBackendUrl;\n }\n return defaultAiBackendUrl;\n }\n\n static String get dachengAiWebUrl {\n if (configuredDachengAiWebUrl.isNotEmpty) {\n return configuredDachengAiWebUrl;\n }\n return defaultDachengAiWebUrl;\n }\n\n static Uri buildDachengAiWebUri({\n String? prompt,\n String? bookTitle,\n String? context,\n }) {\n final base = Uri.parse(dachengAiWebUrl);\n final query = <String, String>{\n ...base.queryParameters,\n if (prompt != null && prompt.trim().isNotEmpty) 'prompt': prompt.trim(),\n if (bookTitle != null && bookTitle.trim().isNotEmpty)\n 'book': bookTitle.trim(),\n if (context != null && context.trim().isNotEmpty)\n 'context': context.trim(),\n };\n return base.replace(queryParameters: query.isEmpty ? null : query);\n }\n\n /// 大厂式 API 网关入口:客户端只允许构造自家后端的相对路径。\n ///\n /// 第三方 API、上游 API、缓存、重试、fallback 都应在后端处理;\n /// App 端不得直接传入 https://... 或 http://... 这类绝对 URL。\n static Uri buildBackendUri(\n String endpoint, {\n Map<String, String>? queryParameters,\n }) {\n final normalizedEndpoint = endpoint.trim();\n if (normalizedEndpoint.isEmpty || !normalizedEndpoint.startsWith('/')) {\n throw ArgumentError(\n 'API endpoint must be a first-party relative path starting with /: $endpoint',\n );\n }\n if (normalizedEndpoint.startsWith('//') ||\n normalizedEndpoint.contains('://')) {\n throw ArgumentError(\n 'API endpoint must not be an absolute or protocol-relative URL: $endpoint',\n );\n }\n\n final baseUrl = currentBackendUrl.replaceFirst(RegExp(r'/+$'), '');\n final uri = Uri.parse('$baseUrl$normalizedEndpoint');\n if (queryParameters == null || queryParameters.isEmpty) {\n return uri;\n }\n return uri.replace(queryParameters: queryParameters);\n }\n\n static String buildBackendUrl(\n String endpoint, {\n Map<String, String>? queryParameters,\n }) {\n return buildBackendUri(\n endpoint,\n queryParameters: queryParameters,\n ).toString();\n }\n\n // 应用信息\n static const String appName = '大乘';\n // 保持与 pubspec.yaml 中的 version 同步,供 Web 回退和服务端比对使用。\n static const String appVersion = '1.0.0';\n static const int appBuildNumber = 400;\n\n // 功能开关\n static const bool enableFirebaseAuth = true;\n static const bool enableAlipay = true;\n static const bool enableAppleIAP = true;\n static const bool enableVideoFeed = true;\n static bool get enableDebugMode => !isProduction;\n\n // 传输配置\n static const int fileChunkSize = 1024;\n static const int maxRetryCount = 3;\n static const int timeoutDuration = 5000;\n static const int maxConcurrentTransfers = 5;\n\n // 缓存配置\n static const int cacheMaxAge = 7;\n static const int maxCacheSize = 100;\n\n // 分页配置\n static const int defaultPageSize = 20;\n static const int maxPageSize = 100;\n\n // 超时配置\n static final Duration requestTimeout = const Duration(seconds: 30);\n static final Duration connectTimeout = const Duration(seconds: 10);\n\n // 重试配置\n static const int maxRetries = 3;\n static final Duration retryDelay = const Duration(seconds: 1);\n\n // 日志配置\n static const bool enableLogging = true;\n static bool get enableNetworkLogging => !isProduction;\n static bool get enablePerformanceLogging => !isProduction;\n\n // 存储键名\n static const String tokenStorageKey = 'auth_token';\n static const String userInfoStorageKey = 'user_info';\n static const String backendUrlStorageKey = 'backend_url';\n static const String testModeStorageKey = 'test_mode';\n\n // API端点\n static String get loginUrl => buildBackendUrl('/api/auth/login');\n static String get registerUrl => buildBackendUrl('/api/auth/register');\n static String get verifyUrl => buildBackendUrl('/api/auth/verify');\n static String get logoutUrl => buildBackendUrl('/api/auth/logout');\n static String get deleteAccountUrl => buildBackendUrl('/api/auth/delete');\n static String get sendVerificationCodeUrl =>\n buildBackendUrl('/api/auth/send-verification-code');\n static String get verifyCodeUrl => buildBackendUrl('/api/auth/verify-code');\n static String get forgotPasswordUrl =>\n buildBackendUrl('/api/auth/forgot-password');\n static String get resetPasswordUrl =>\n buildBackendUrl('/api/auth/reset-password');\n static String get userInfoUrl => buildBackendUrl('/api/auth/user-info');\n static String get bindEmailUrl => buildBackendUrl('/api/auth/bind-email');\n static String get appVersionPolicyUrl =>\n buildBackendUrl('/api/app/version-policy');\n\n static String get agentChatUrl => buildBackendUrl('/api/agent/chat');\n static String get aiQuotaUrl => buildBackendUrl('/api/ai/quota');\n static String agentRunEventsUrl(String runId) =>\n buildBackendUrl('/api/agent/runs/$runId/events');\n static String agentRunCancelUrl(String runId) =>\n buildBackendUrl('/api/agent/runs/$runId/cancel');\n static String agentMessageFeedbackUrl(String messageId) =>\n buildBackendUrl('/api/agent/messages/$messageId/feedback');\n\n static String get alipayCreateOrderUrl =>\n buildBackendUrl('/api/alipay/create-order');\n static String get alipayQueryOrderUrl =>\n buildBackendUrl('/api/alipay/query-order');\n static String get alipayMembershipStatusUrl =>\n buildBackendUrl('/api/alipay/check-membership');\n\n static String get stripeMembershipStatusUrl =>\n buildBackendUrl('/api/stripe/membership-status');\n static String get stripeCreateSubscriptionUrl =>\n buildBackendUrl('/api/stripe/create-subscription');\n static String get stripeSessionStatusUrl =>\n buildBackendUrl('/api/stripe/session-status');\n\n static String get appleVerifyReceiptUrl =>\n buildBackendUrl('/api/apple/verify-receipt');\n static String get purchaseEntitlementUrl =>\n buildBackendUrl('/api/purchases/entitlement');\n\n static const String zenBuddhaAssetProductId = 'zen_buddha_asset';\n static const String zenBuddhaAssetDisplayName = '3D佛像素材';\n static const String zenBuddhaAssetPriceLabel = '¥33.00';\n static const String zenBuddhaAssetCardImagePath =\n 'assets/images/zen_buddha_material_card.png';\n\n static String get adminCheckStatusUrl =>\n buildBackendUrl('/api/admin/check-status');\n static String get adminCreateRedeemCodeUrl =>\n buildBackendUrl('/api/admin/create-redeem-code');\n static String get adminRedeemCodesUrl =>\n buildBackendUrl('/api/admin/redeem-codes');\n static String get adminUseRedeemCodeUrl =>\n buildBackendUrl('/api/admin/use-redeem-code');\n static String get adminPurchaseHistoryUrl =>\n buildBackendUrl('/api/admin/purchase-history');\n static String get adminRedeemHistoryUrl =>\n buildBackendUrl('/api/admin/redeem-history');\n\n static String get leaderboardUrl => buildBackendUrl('/api/leaderboard');\n static String get updateTransferDataUrl =>\n buildBackendUrl('/api/leaderboard/update');\n\n static String get healthCheckUrl => buildBackendUrl('/health');\n\n // 3D 佛像模型配置\n // 如果 R2 上需要切换到新的对象键,优先改这里,便于强制绕开旧缓存。\n static const String buddhaModelAssetPath = 'models/buddha_model.model';\n // Android/iOS both use the R2 flutter_scene .model; local cache misses\n // download through AssetLoaderService.\n\n // 当前线上正确模型明显大于 100MB,小于该阈值视为误传/降质文件。\n static const int minBuddhaModelSizeBytes = 100 * 1024 * 1024;\n\n // 请求头\n static Map<String, String> get defaultHeaders => {\n 'Content-Type': 'application/json',\n 'Accept': 'application/json',\n 'User-Agent': 'FabushiApp/${isWeb ? \"Web\" : \"Mobile\"}',\n };\n\n // 错误消息\n static const Map<String, String> errorMessages = {\n 'network_error': '网络连接失败,请检查网络设置',\n 'server_error': '服务器错误,请稍后重试',\n 'timeout_error': '请求超时,请检查网络连接',\n 'auth_error': '认证失败,请重新登录',\n 'permission_error': '权限不足',\n 'validation_error': '数据验证失败',\n 'unknown_error': '未知错误,请联系客服',\n };\n\n // 备用地址\n static List<String> get fallbackUrls {\n return [primaryBackendUrl];\n }\n\n // 调试配置\n static const bool enableApiLogging = true;\n static const bool debugMode = bool.fromEnvironment(\n 'DEBUG',\n defaultValue: false,\n );\n\n static void printConfigInfo() {\n if (kDebugMode) {\n print('=== 应用配置 ===');\n print('环境: ${isProduction ? \"生产\" : \"开发\"}');\n print('平台: ${isWeb ? \"Web\" : \"Native\"}');\n print('API URL: $currentBackendUrl');\n print('================');\n }\n }\n\n static void printCurrentConfig() => printConfigInfo();\n}\n"}, "5c7e77b41a34b89959cff6bc82ac16f657947e3d": {"t": 1785757952.9736185, "n": 10881, "h": "bfe5a96cef2a3fc2", "c": "import 'package:flutter/foundation.dart';\n\n/// 应用配置 - 统一管理所有配置项\nclass AppConfig {\n AppConfig._();\n\n static final AppConfig instance = AppConfig._();\n\n // 环境配置\n static const String environment = String.fromEnvironment(\n 'ENV',\n defaultValue: 'production',\n );\n\n static bool get isProduction {\n if (kIsWeb) {\n final currentUrl = Uri.base.toString();\n if (currentUrl.contains('fabushi-flutter-web-dev') ||\n currentUrl.contains('localhost')) {\n return false;\n }\n if (currentUrl.contains('fabushi-flutter-web-prod')) {\n return true;\n }\n }\n return environment == 'production';\n }\n\n static bool get isDevelopment => !isProduction;\n static bool get isStaging => environment == 'staging';\n static bool get isWeb => kIsWeb;\n\n // API配置\n static const String configuredApiBaseUrl = String.fromEnvironment(\n 'API_BASE_URL',\n defaultValue: '',\n );\n static const String primaryBackendUrl = 'https://api.ombhrum.com';\n static const String cloudflareWorkerProdUrl = 'https://api.ombhrum.com';\n static const String cloudflareWorkerDevUrl =\n 'https://fabushi-flutter-web-dev.bhrumom.workers.dev';\n static const String localDevUrl = 'http://localhost:8787';\n static const String publicWebUrl = 'https://flutter.ombhrum.com';\n static const String configuredAiBackendUrl = String.fromEnvironment(\n 'AI_BACKEND_URL',\n defaultValue: '',\n );\n static const String vpsAiBackendUrl = 'https://ai.ombhrum.com';\n static const String configuredDachengAiWebUrl = String.fromEnvironment(\n 'DACHENG_AI_WEB_URL',\n defaultValue: '',\n );\n static const bool desktopControlEnabled = bool.fromEnvironment(\n 'DACHENG_DESKTOP_CONTROL',\n defaultValue: false,\n );\n static const String configuredOpenClawDeepSeekApiKey = String.fromEnvironment(\n 'DACHENG_OPENCLAW_DEEPSEEK_API_KEY',\n defaultValue: '',\n );\n static const String defaultDachengAiWebUrl =\n 'https://fabushi.ombhrum.com/app/ai';\n\n static String get currentBackendUrl {\n if (configuredApiBaseUrl.isNotEmpty) {\n return configuredApiBaseUrl;\n }\n if (kIsWeb) {\n final currentUrl = Uri.base;\n final host = currentUrl.host;\n if (host.contains('localhost')) {\n return localDevUrl;\n }\n if (host.contains('fabushi-flutter-web-dev')) {\n return cloudflareWorkerDevUrl;\n }\n if (host.contains('fabushi-flutter-web-prod')) {\n return cloudflareWorkerProdUrl;\n }\n }\n return primaryBackendUrl;\n }\n\n static String get apiUrl => currentBackendUrl;\n\n static String get currentAiBackendUrl {\n if (configuredAiBackendUrl.isNotEmpty) {\n return configuredAiBackendUrl;\n }\n return vpsAiBackendUrl;\n }\n\n static String get dachengAiWebUrl {\n if (configuredDachengAiWebUrl.isNotEmpty) {\n return configuredDachengAiWebUrl;\n }\n return defaultDachengAiWebUrl;\n }\n\n static Uri buildDachengAiWebUri({\n String? prompt,\n String? bookTitle,\n String? context,\n }) {\n final base = Uri.parse(dachengAiWebUrl);\n final query = <String, String>{\n ...base.queryParameters,\n if (prompt != null && prompt.trim().isNotEmpty) 'prompt': prompt.trim(),\n if (bookTitle != null && bookTitle.trim().isNotEmpty)\n 'book': bookTitle.trim(),\n if (context != null && context.trim().isNotEmpty)\n 'context': context.trim(),\n };\n return base.replace(queryParameters: query.isEmpty ? null : query);\n }\n\n /// 大厂式 API 网关入口:客户端只允许构造自家后端的相对路径。\n ///\n /// 第三方 API、上游 API、缓存、重试、fallback 都应在后端处理;\n /// App 端不得直接传入 https://... 或 http://... 这类绝对 URL。\n static Uri buildBackendUri(\n String endpoint, {\n Map<String, String>? queryParameters,\n }) {\n final normalizedEndpoint = endpoint.trim();\n if (normalizedEndpoint.isEmpty || !normalizedEndpoint.startsWith('/')) {\n throw ArgumentError(\n 'API endpoint must be a first-party relative path starting with /: $endpoint',\n );\n }\n if (normalizedEndpoint.startsWith('//') ||\n normalizedEndpoint.contains('://')) {\n throw ArgumentError(\n 'API endpoint must not be an absolute or protocol-relative URL: $endpoint',\n );\n }\n\n final baseUrl = currentBackendUrl.replaceFirst(RegExp(r'/+$'), '');\n final uri = Uri.parse('$baseUrl$normalizedEndpoint');\n if (queryParameters == null || queryParameters.isEmpty) {\n return uri;\n }\n return uri.replace(queryParameters: queryParameters);\n }\n\n static String buildBackendUrl(\n String endpoint, {\n Map<String, String>? queryParameters,\n }) {\n return buildBackendUri(\n endpoint,\n queryParameters: queryParameters,\n ).toString();\n }\n\n // 应用信息\n static const String appName = '大乘';\n // 保持与 pubspec.yaml 中的 version 同步,供 Web 回退和服务端比对使用。\n static const String appVersion = '1.0.0';\n static const int appBuildNumber = 400;\n\n // 功能开关\n static const bool enableFirebaseAuth = true;\n static const bool enableAlipay = true;\n static const bool enableAppleIAP = true;\n static const bool enableVideoFeed = true;\n static bool get enableDebugMode => !isProduction;\n\n // 传输配置\n static const int fileChunkSize = 1024;\n static const int maxRetryCount = 3;\n static const int timeoutDuration = 5000;\n static const int maxConcurrentTransfers = 5;\n\n // 缓存配置\n static const int cacheMaxAge = 7;\n static const int maxCacheSize = 100;\n\n // 分页配置\n static const int defaultPageSize = 20;\n static const int maxPageSize = 100;\n\n // 超时配置\n static final Duration requestTimeout = const Duration(seconds: 30);\n static final Duration connectTimeout = const Duration(seconds: 10);\n\n // 重试配置\n static const int maxRetries = 3;\n static final Duration retryDelay = const Duration(seconds: 1);\n\n // 日志配置\n static const bool enableLogging = true;\n static bool get enableNetworkLogging => !isProduction;\n static bool get enablePerformanceLogging => !isProduction;\n\n // 存储键名\n static const String tokenStorageKey = 'auth_token';\n static const String userInfoStorageKey = 'user_info';\n static const String backendUrlStorageKey = 'backend_url';\n static const String testModeStorageKey = 'test_mode';\n\n // API端点\n static String get loginUrl => buildBackendUrl('/api/auth/login');\n static String get registerUrl => buildBackendUrl('/api/auth/register');\n static String get verifyUrl => buildBackendUrl('/api/auth/verify');\n static String get logoutUrl => buildBackendUrl('/api/auth/logout');\n static String get deleteAccountUrl => buildBackendUrl('/api/auth/delete');\n static String get sendVerificationCodeUrl =>\n buildBackendUrl('/api/auth/send-verification-code');\n static String get verifyCodeUrl => buildBackendUrl('/api/auth/verify-code');\n static String get forgotPasswordUrl =>\n buildBackendUrl('/api/auth/forgot-password');\n static String get resetPasswordUrl =>\n buildBackendUrl('/api/auth/reset-password');\n static String get userInfoUrl => buildBackendUrl('/api/auth/user-info');\n static String get bindEmailUrl => buildBackendUrl('/api/auth/bind-email');\n static String get appVersionPolicyUrl =>\n buildBackendUrl('/api/app/version-policy');\n\n static String get agentChatUrl => buildBackendUrl('/api/agent/chat');\n static String agentRunEventsUrl(String runId) =>\n buildBackendUrl('/api/agent/runs/$runId/events');\n static String agentRunCancelUrl(String runId) =>\n buildBackendUrl('/api/agent/runs/$runId/cancel');\n static String agentMessageFeedbackUrl(String messageId) =>\n buildBackendUrl('/api/agent/messages/$messageId/feedback');\n\n static String get alipayCreateOrderUrl =>\n buildBackendUrl('/api/alipay/create-order');\n static String get alipayQueryOrderUrl =>\n buildBackendUrl('/api/alipay/query-order');\n static String get alipayMembershipStatusUrl =>\n buildBackendUrl('/api/alipay/check-membership');\n\n static String get stripeMembershipStatusUrl =>\n buildBackendUrl('/api/stripe/membership-status');\n static String get stripeCreateSubscriptionUrl =>\n buildBackendUrl('/api/stripe/create-subscription');\n static String get stripeSessionStatusUrl =>\n buildBackendUrl('/api/stripe/session-status');\n\n static String get appleVerifyReceiptUrl =>\n buildBackendUrl('/api/apple/verify-receipt');\n static String get purchaseEntitlementUrl =>\n buildBackendUrl('/api/purchases/entitlement');\n\n static const String zenBuddhaAssetProductId = 'zen_buddha_asset';\n static const String zenBuddhaAssetDisplayName = '3D佛像素材';\n static const String zenBuddhaAssetPriceLabel = '¥33.00';\n static const String zenBuddhaAssetCardImagePath =\n 'assets/images/zen_buddha_material_card.png';\n\n static String get adminCheckStatusUrl =>\n buildBackendUrl('/api/admin/check-status');\n static String get adminCreateRedeemCodeUrl =>\n buildBackendUrl('/api/admin/create-redeem-code');\n static String get adminRedeemCodesUrl =>\n buildBackendUrl('/api/admin/redeem-codes');\n static String get adminUseRedeemCodeUrl =>\n buildBackendUrl('/api/admin/use-redeem-code');\n static String get adminPurchaseHistoryUrl =>\n buildBackendUrl('/api/admin/purchase-history');\n static String get adminRedeemHistoryUrl =>\n buildBackendUrl('/api/admin/redeem-history');\n\n static String get leaderboardUrl => buildBackendUrl('/api/leaderboard');\n static String get updateTransferDataUrl =>\n buildBackendUrl('/api/leaderboard/update');\n\n static String get healthCheckUrl => buildBackendUrl('/health');\n\n // 3D 佛像模型配置\n // 如果 R2 上需要切换到新的对象键,优先改这里,便于强制绕开旧缓存。\n static const String buddhaModelAssetPath = 'models/buddha_model.model';\n // Android/iOS both use the R2 flutter_scene .model; local cache misses\n // download through AssetLoaderService.\n\n // 当前线上正确模型明显大于 100MB,小于该阈值视为误传/降质文件。\n static const int minBuddhaModelSizeBytes = 100 * 1024 * 1024;\n\n // 请求头\n static Map<String, String> get defaultHeaders => {\n 'Content-Type': 'application/json',\n 'Accept': 'application/json',\n 'User-Agent': 'FabushiApp/${isWeb ? \"Web\" : \"Mobile\"}',\n };\n\n // 错误消息\n static const Map<String, String> errorMessages = {\n 'network_error': '网络连接失败,请检查网络设置',\n 'server_error': '服务器错误,请稍后重试',\n 'timeout_error': '请求超时,请检查网络连接',\n 'auth_error': '认证失败,请重新登录',\n 'permission_error': '权限不足',\n 'validation_error': '数据验证失败',\n 'unknown_error': '未知错误,请联系客服',\n };\n\n // 备用地址\n static List<String> get fallbackUrls {\n return [primaryBackendUrl];\n }\n\n // 调试配置\n static const bool enableApiLogging = true;\n static const bool debugMode = bool.fromEnvironment(\n 'DEBUG',\n defaultValue: false,\n );\n\n static void printConfigInfo() {\n if (kDebugMode) {\n print('=== 应用配置 ===');\n print('环境: ${isProduction ? \"生产\" : \"开发\"}');\n print('平台: ${isWeb ? \"Web\" : \"Native\"}');\n print('API URL: $currentBackendUrl');\n print('================');\n }\n }\n\n static void printCurrentConfig() => printConfigInfo();\n}\n"}, "307982225c5260797f47a78760a75ed6e3adc541": {"t": 1785757954.5925317, "n": 10732, "h": "8996aae2fcb6ba0e", "c": "import 'package:flutter/foundation.dart';\n\n/// 应用配置 - 统一管理所有配置项\nclass AppConfig {\n AppConfig._();\n\n static final AppConfig instance = AppConfig._();\n\n // 环境配置\n static const String environment = String.fromEnvironment(\n 'ENV',\n defaultValue: 'production',\n );\n\n static bool get isProduction {\n if (kIsWeb) {\n final currentUrl = Uri.base.toString();\n if (currentUrl.contains('fabushi-flutter-web-dev') ||\n currentUrl.contains('localhost')) {\n return false;\n }\n if (currentUrl.contains('fabushi-flutter-web-prod')) {\n return true;\n }\n }\n return environment == 'production';\n }\n\n static bool get isDevelopment => !isProduction;\n static bool get isStaging => environment == 'staging';\n static bool get isWeb => kIsWeb;\n\n // API配置\n static const String configuredApiBaseUrl = String.fromEnvironment(\n 'API_BASE_URL',\n defaultValue: '',\n );\n static const String primaryBackendUrl = 'https://api.ombhrum.com';\n static const String cloudflareWorkerProdUrl = 'https://api.ombhrum.com';\n static const String cloudflareWorkerDevUrl =\n 'https://fabushi-flutter-web-dev.bhrumom.workers.dev';\n static const String localDevUrl = 'http://localhost:8787';\n static const String publicWebUrl = 'https://flutter.ombhrum.com';\n static const String configuredAiBackendUrl = String.fromEnvironment(\n 'AI_BACKEND_URL',\n defaultValue: '',\n );\n static const String vpsAiBackendUrl = 'https://ai.ombhrum.com';\n static const String configuredDachengAiWebUrl = String.fromEnvironment(\n 'DACHENG_AI_WEB_URL',\n defaultValue: '',\n );\n static const bool desktopControlEnabled = bool.fromEnvironment(\n 'DACHENG_DESKTOP_CONTROL',\n defaultValue: false,\n );\n static const String defaultDachengAiWebUrl =\n 'https://fabushi.ombhrum.com/app/ai';\n\n static String get currentBackendUrl {\n if (configuredApiBaseUrl.isNotEmpty) {\n return configuredApiBaseUrl;\n }\n if (kIsWeb) {\n final currentUrl = Uri.base;\n final host = currentUrl.host;\n if (host.contains('localhost')) {\n return localDevUrl;\n }\n if (host.contains('fabushi-flutter-web-dev')) {\n return cloudflareWorkerDevUrl;\n }\n if (host.contains('fabushi-flutter-web-prod')) {\n return cloudflareWorkerProdUrl;\n }\n }\n return primaryBackendUrl;\n }\n\n static String get apiUrl => currentBackendUrl;\n\n static String get currentAiBackendUrl {\n if (configuredAiBackendUrl.isNotEmpty) {\n return configuredAiBackendUrl;\n }\n return vpsAiBackendUrl;\n }\n\n static String get dachengAiWebUrl {\n if (configuredDachengAiWebUrl.isNotEmpty) {\n return configuredDachengAiWebUrl;\n }\n return defaultDachengAiWebUrl;\n }\n\n static Uri buildDachengAiWebUri({\n String? prompt,\n String? bookTitle,\n String? context,\n }) {\n final base = Uri.parse(dachengAiWebUrl);\n final query = <String, String>{\n ...base.queryParameters,\n if (prompt != null && prompt.trim().isNotEmpty) 'prompt': prompt.trim(),\n if (bookTitle != null && bookTitle.trim().isNotEmpty)\n 'book': bookTitle.trim(),\n if (context != null && context.trim().isNotEmpty)\n 'context': context.trim(),\n };\n return base.replace(queryParameters: query.isEmpty ? null : query);\n }\n\n /// 大厂式 API 网关入口:客户端只允许构造自家后端的相对路径。\n ///\n /// 第三方 API、上游 API、缓存、重试、fallback 都应在后端处理;\n /// App 端不得直接传入 https://... 或 http://... 这类绝对 URL。\n static Uri buildBackendUri(\n String endpoint, {\n Map<String, String>? queryParameters,\n }) {\n final normalizedEndpoint = endpoint.trim();\n if (normalizedEndpoint.isEmpty || !normalizedEndpoint.startsWith('/')) {\n throw ArgumentError(\n 'API endpoint must be a first-party relative path starting with /: $endpoint',\n );\n }\n if (normalizedEndpoint.startsWith('//') ||\n normalizedEndpoint.contains('://')) {\n throw ArgumentError(\n 'API endpoint must not be an absolute or protocol-relative URL: $endpoint',\n );\n }\n\n final baseUrl = currentBackendUrl.replaceFirst(RegExp(r'/+$'), '');\n final uri = Uri.parse('$baseUrl$normalizedEndpoint');\n if (queryParameters == null || queryParameters.isEmpty) {\n return uri;\n }\n return uri.replace(queryParameters: queryParameters);\n }\n\n static String buildBackendUrl(\n String endpoint, {\n Map<String, String>? queryParameters,\n }) {\n return buildBackendUri(\n endpoint,\n queryParameters: queryParameters,\n ).toString();\n }\n\n // 应用信息\n static const String appName = '大乘';\n // 保持与 pubspec.yaml 中的 version 同步,供 Web 回退和服务端比对使用。\n static const String appVersion = '1.0.0';\n static const int appBuildNumber = 400;\n\n // 功能开关\n static const bool enableFirebaseAuth = true;\n static const bool enableAlipay = true;\n static const bool enableAppleIAP = true;\n static const bool enableVideoFeed = true;\n static bool get enableDebugMode => !isProduction;\n\n // 传输配置\n static const int fileChunkSize = 1024;\n static const int maxRetryCount = 3;\n static const int timeoutDuration = 5000;\n static const int maxConcurrentTransfers = 5;\n\n // 缓存配置\n static const int cacheMaxAge = 7;\n static const int maxCacheSize = 100;\n\n // 分页配置\n static const int defaultPageSize = 20;\n static const int maxPageSize = 100;\n\n // 超时配置\n static final Duration requestTimeout = const Duration(seconds: 30);\n static final Duration connectTimeout = const Duration(seconds: 10);\n\n // 重试配置\n static const int maxRetries = 3;\n static final Duration retryDelay = const Duration(seconds: 1);\n\n // 日志配置\n static const bool enableLogging = true;\n static bool get enableNetworkLogging => !isProduction;\n static bool get enablePerformanceLogging => !isProduction;\n\n // 存储键名\n static const String tokenStorageKey = 'auth_token';\n static const String userInfoStorageKey = 'user_info';\n static const String backendUrlStorageKey = 'backend_url';\n static const String testModeStorageKey = 'test_mode';\n\n // API端点\n static String get loginUrl => buildBackendUrl('/api/auth/login');\n static String get registerUrl => buildBackendUrl('/api/auth/register');\n static String get verifyUrl => buildBackendUrl('/api/auth/verify');\n static String get logoutUrl => buildBackendUrl('/api/auth/logout');\n static String get deleteAccountUrl => buildBackendUrl('/api/auth/delete');\n static String get sendVerificationCodeUrl =>\n buildBackendUrl('/api/auth/send-verification-code');\n static String get verifyCodeUrl => buildBackendUrl('/api/auth/verify-code');\n static String get forgotPasswordUrl =>\n buildBackendUrl('/api/auth/forgot-password');\n static String get resetPasswordUrl =>\n buildBackendUrl('/api/auth/reset-password');\n static String get userInfoUrl => buildBackendUrl('/api/auth/user-info');\n static String get bindEmailUrl => buildBackendUrl('/api/auth/bind-email');\n static String get appVersionPolicyUrl =>\n buildBackendUrl('/api/app/version-policy');\n\n static String get agentChatUrl => buildBackendUrl('/api/agent/chat');\n static String agentRunEventsUrl(String runId) =>\n buildBackendUrl('/api/agent/runs/$runId/events');\n static String agentRunCancelUrl(String runId) =>\n buildBackendUrl('/api/agent/runs/$runId/cancel');\n static String agentMessageFeedbackUrl(String messageId) =>\n buildBackendUrl('/api/agent/messages/$messageId/feedback');\n\n static String get alipayCreateOrderUrl =>\n buildBackendUrl('/api/alipay/create-order');\n static String get alipayQueryOrderUrl =>\n buildBackendUrl('/api/alipay/query-order');\n static String get alipayMembershipStatusUrl =>\n buildBackendUrl('/api/alipay/check-membership');\n\n static String get stripeMembershipStatusUrl =>\n buildBackendUrl('/api/stripe/membership-status');\n static String get stripeCreateSubscriptionUrl =>\n buildBackendUrl('/api/stripe/create-subscription');\n static String get stripeSessionStatusUrl =>\n buildBackendUrl('/api/stripe/session-status');\n\n static String get appleVerifyReceiptUrl =>\n buildBackendUrl('/api/apple/verify-receipt');\n static String get purchaseEntitlementUrl =>\n buildBackendUrl('/api/purchases/entitlement');\n\n static const String zenBuddhaAssetProductId = 'zen_buddha_asset';\n static const String zenBuddhaAssetDisplayName = '3D佛像素材';\n static const String zenBuddhaAssetPriceLabel = '¥33.00';\n static const String zenBuddhaAssetCardImagePath =\n 'assets/images/zen_buddha_material_card.png';\n\n static String get adminCheckStatusUrl =>\n buildBackendUrl('/api/admin/check-status');\n static String get adminCreateRedeemCodeUrl =>\n buildBackendUrl('/api/admin/create-redeem-code');\n static String get adminRedeemCodesUrl =>\n buildBackendUrl('/api/admin/redeem-codes');\n static String get adminUseRedeemCodeUrl =>\n buildBackendUrl('/api/admin/use-redeem-code');\n static String get adminPurchaseHistoryUrl =>\n buildBackendUrl('/api/admin/purchase-history');\n static String get adminRedeemHistoryUrl =>\n buildBackendUrl('/api/admin/redeem-history');\n\n static String get leaderboardUrl => buildBackendUrl('/api/leaderboard');\n static String get updateTransferDataUrl =>\n buildBackendUrl('/api/leaderboard/update');\n\n static String get healthCheckUrl => buildBackendUrl('/health');\n\n // 3D 佛像模型配置\n // 如果 R2 上需要切换到新的对象键,优先改这里,便于强制绕开旧缓存。\n static const String buddhaModelAssetPath = 'models/buddha_model.model';\n // Android/iOS both use the R2 flutter_scene .model; local cache misses\n // download through AssetLoaderService.\n\n // 当前线上正确模型明显大于 100MB,小于该阈值视为误传/降质文件。\n static const int minBuddhaModelSizeBytes = 100 * 1024 * 1024;\n\n // 请求头\n static Map<String, String> get defaultHeaders => {\n 'Content-Type': 'application/json',\n 'Accept': 'application/json',\n 'User-Agent': 'FabushiApp/${isWeb ? \"Web\" : \"Mobile\"}',\n };\n\n // 错误消息\n static const Map<String, String> errorMessages = {\n 'network_error': '网络连接失败,请检查网络设置',\n 'server_error': '服务器错误,请稍后重试',\n 'timeout_error': '请求超时,请检查网络连接',\n 'auth_error': '认证失败,请重新登录',\n 'permission_error': '权限不足',\n 'validation_error': '数据验证失败',\n 'unknown_error': '未知错误,请联系客服',\n };\n\n // 备用地址\n static List<String> get fallbackUrls {\n return [primaryBackendUrl];\n }\n\n // 调试配置\n static const bool enableApiLogging = true;\n static const bool debugMode = bool.fromEnvironment(\n 'DEBUG',\n defaultValue: false,\n );\n\n static void printConfigInfo() {\n if (kDebugMode) {\n print('=== 应用配置 ===');\n print('环境: ${isProduction ? \"生产\" : \"开发\"}');\n print('平台: ${isWeb ? \"Web\" : \"Native\"}');\n print('API URL: $currentBackendUrl');\n print('================');\n }\n }\n\n static void printCurrentConfig() => printConfigInfo();\n}\n"}, "494fb0d876ceaa527d43615b9a798980f19586de": {"t": 1785757955.9751368, "n": 10605, "h": "677c28dd862d45c1", "c": "import 'package:flutter/foundation.dart';\n\n/// 应用配置 - 统一管理所有配置项\nclass AppConfig {\n AppConfig._();\n\n static final AppConfig instance = AppConfig._();\n\n // 环境配置\n static const String environment = String.fromEnvironment(\n 'ENV',\n defaultValue: 'production',\n );\n\n static bool get isProduction {\n if (kIsWeb) {\n final currentUrl = Uri.base.toString();\n if (currentUrl.contains('fabushi-flutter-web-dev') ||\n currentUrl.contains('localhost')) {\n return false;\n }\n if (currentUrl.contains('fabushi-flutter-web-prod')) {\n return true;\n }\n }\n return environment == 'production';\n }\n\n static bool get isDevelopment => !isProduction;\n static bool get isStaging => environment == 'staging';\n static bool get isWeb => kIsWeb;\n\n // API配置\n static const String configuredApiBaseUrl = String.fromEnvironment(\n 'API_BASE_URL',\n defaultValue: '',\n );\n static const String primaryBackendUrl = 'https://api.ombhrum.com';\n static const String cloudflareWorkerProdUrl = 'https://api.ombhrum.com';\n static const String cloudflareWorkerDevUrl =\n 'https://fabushi-flutter-web-dev.bhrumom.workers.dev';\n static const String localDevUrl = 'http://localhost:8787';\n static const String publicWebUrl = 'https://flutter.ombhrum.com';\n static const String configuredAiBackendUrl = String.fromEnvironment(\n 'AI_BACKEND_URL',\n defaultValue: '',\n );\n static const String vpsAiBackendUrl = 'https://ai.ombhrum.com';\n static const String configuredDachengAiWebUrl = String.fromEnvironment(\n 'DACHENG_AI_WEB_URL',\n defaultValue: '',\n );\n static const String defaultDachengAiWebUrl =\n 'https://fabushi.ombhrum.com/app/ai';\n\n static String get currentBackendUrl {\n if (configuredApiBaseUrl.isNotEmpty) {\n return configuredApiBaseUrl;\n }\n if (kIsWeb) {\n final currentUrl = Uri.base;\n final host = currentUrl.host;\n if (host.contains('localhost')) {\n return localDevUrl;\n }\n if (host.contains('fabushi-flutter-web-dev')) {\n return cloudflareWorkerDevUrl;\n }\n if (host.contains('fabushi-flutter-web-prod')) {\n return cloudflareWorkerProdUrl;\n }\n }\n return primaryBackendUrl;\n }\n\n static String get apiUrl => currentBackendUrl;\n\n static String get currentAiBackendUrl {\n if (configuredAiBackendUrl.isNotEmpty) {\n return configuredAiBackendUrl;\n }\n return vpsAiBackendUrl;\n }\n\n static String get dachengAiWebUrl {\n if (configuredDachengAiWebUrl.isNotEmpty) {\n return configuredDachengAiWebUrl;\n }\n return defaultDachengAiWebUrl;\n }\n\n static Uri buildDachengAiWebUri({\n String? prompt,\n String? bookTitle,\n String? context,\n }) {\n final base = Uri.parse(dachengAiWebUrl);\n final query = <String, String>{\n ...base.queryParameters,\n if (prompt != null && prompt.trim().isNotEmpty) 'prompt': prompt.trim(),\n if (bookTitle != null && bookTitle.trim().isNotEmpty)\n 'book': bookTitle.trim(),\n if (context != null && context.trim().isNotEmpty)\n 'context': context.trim(),\n };\n return base.replace(queryParameters: query.isEmpty ? null : query);\n }\n\n /// 大厂式 API 网关入口:客户端只允许构造自家后端的相对路径。\n ///\n /// 第三方 API、上游 API、缓存、重试、fallback 都应在后端处理;\n /// App 端不得直接传入 https://... 或 http://... 这类绝对 URL。\n static Uri buildBackendUri(\n String endpoint, {\n Map<String, String>? queryParameters,\n }) {\n final normalizedEndpoint = endpoint.trim();\n if (normalizedEndpoint.isEmpty || !normalizedEndpoint.startsWith('/')) {\n throw ArgumentError(\n 'API endpoint must be a first-party relative path starting with /: $endpoint',\n );\n }\n if (normalizedEndpoint.startsWith('//') ||\n normalizedEndpoint.contains('://')) {\n throw ArgumentError(\n 'API endpoint must not be an absolute or protocol-relative URL: $endpoint',\n );\n }\n\n final baseUrl = currentBackendUrl.replaceFirst(RegExp(r'/+$'), '');\n final uri = Uri.parse('$baseUrl$normalizedEndpoint');\n if (queryParameters == null || queryParameters.isEmpty) {\n return uri;\n }\n return uri.replace(queryParameters: queryParameters);\n }\n\n static String buildBackendUrl(\n String endpoint, {\n Map<String, String>? queryParameters,\n }) {\n return buildBackendUri(\n endpoint,\n queryParameters: queryParameters,\n ).toString();\n }\n\n // 应用信息\n static const String appName = '大乘';\n // 保持与 pubspec.yaml 中的 version 同步,供 Web 回退和服务端比对使用。\n static const String appVersion = '1.0.0';\n static const int appBuildNumber = 400;\n\n // 功能开关\n static const bool enableFirebaseAuth = true;\n static const bool enableAlipay = true;\n static const bool enableAppleIAP = true;\n static const bool enableVideoFeed = true;\n static bool get enableDebugMode => !isProduction;\n\n // 传输配置\n static const int fileChunkSize = 1024;\n static const int maxRetryCount = 3;\n static const int timeoutDuration = 5000;\n static const int maxConcurrentTransfers = 5;\n\n // 缓存配置\n static const int cacheMaxAge = 7;\n static const int maxCacheSize = 100;\n\n // 分页配置\n static const int defaultPageSize = 20;\n static const int maxPageSize = 100;\n\n // 超时配置\n static final Duration requestTimeout = const Duration(seconds: 30);\n static final Duration connectTimeout = const Duration(seconds: 10);\n\n // 重试配置\n static const int maxRetries = 3;\n static final Duration retryDelay = const Duration(seconds: 1);\n\n // 日志配置\n static const bool enableLogging = true;\n static bool get enableNetworkLogging => !isProduction;\n static bool get enablePerformanceLogging => !isProduction;\n\n // 存储键名\n static const String tokenStorageKey = 'auth_token';\n static const String userInfoStorageKey = 'user_info';\n static const String backendUrlStorageKey = 'backend_url';\n static const String testModeStorageKey = 'test_mode';\n\n // API端点\n static String get loginUrl => buildBackendUrl('/api/auth/login');\n static String get registerUrl => buildBackendUrl('/api/auth/register');\n static String get verifyUrl => buildBackendUrl('/api/auth/verify');\n static String get logoutUrl => buildBackendUrl('/api/auth/logout');\n static String get deleteAccountUrl => buildBackendUrl('/api/auth/delete');\n static String get sendVerificationCodeUrl =>\n buildBackendUrl('/api/auth/send-verification-code');\n static String get verifyCodeUrl => buildBackendUrl('/api/auth/verify-code');\n static String get forgotPasswordUrl =>\n buildBackendUrl('/api/auth/forgot-password');\n static String get resetPasswordUrl =>\n buildBackendUrl('/api/auth/reset-password');\n static String get userInfoUrl => buildBackendUrl('/api/auth/user-info');\n static String get bindEmailUrl => buildBackendUrl('/api/auth/bind-email');\n static String get appVersionPolicyUrl =>\n buildBackendUrl('/api/app/version-policy');\n\n static String get agentChatUrl => buildBackendUrl('/api/agent/chat');\n static String agentRunEventsUrl(String runId) =>\n buildBackendUrl('/api/agent/runs/$runId/events');\n static String agentRunCancelUrl(String runId) =>\n buildBackendUrl('/api/agent/runs/$runId/cancel');\n static String agentMessageFeedbackUrl(String messageId) =>\n buildBackendUrl('/api/agent/messages/$messageId/feedback');\n\n static String get alipayCreateOrderUrl =>\n buildBackendUrl('/api/alipay/create-order');\n static String get alipayQueryOrderUrl =>\n buildBackendUrl('/api/alipay/query-order');\n static String get alipayMembershipStatusUrl =>\n buildBackendUrl('/api/alipay/check-membership');\n\n static String get stripeMembershipStatusUrl =>\n buildBackendUrl('/api/stripe/membership-status');\n static String get stripeCreateSubscriptionUrl =>\n buildBackendUrl('/api/stripe/create-subscription');\n static String get stripeSessionStatusUrl =>\n buildBackendUrl('/api/stripe/session-status');\n\n static String get appleVerifyReceiptUrl =>\n buildBackendUrl('/api/apple/verify-receipt');\n static String get purchaseEntitlementUrl =>\n buildBackendUrl('/api/purchases/entitlement');\n\n static const String zenBuddhaAssetProductId = 'zen_buddha_asset';\n static const String zenBuddhaAssetDisplayName = '3D佛像素材';\n static const String zenBuddhaAssetPriceLabel = '¥33.00';\n static const String zenBuddhaAssetCardImagePath =\n 'assets/images/zen_buddha_material_card.png';\n\n static String get adminCheckStatusUrl =>\n buildBackendUrl('/api/admin/check-status');\n static String get adminCreateRedeemCodeUrl =>\n buildBackendUrl('/api/admin/create-redeem-code');\n static String get adminRedeemCodesUrl =>\n buildBackendUrl('/api/admin/redeem-codes');\n static String get adminUseRedeemCodeUrl =>\n buildBackendUrl('/api/admin/use-redeem-code');\n static String get adminPurchaseHistoryUrl =>\n buildBackendUrl('/api/admin/purchase-history');\n static String get adminRedeemHistoryUrl =>\n buildBackendUrl('/api/admin/redeem-history');\n\n static String get leaderboardUrl => buildBackendUrl('/api/leaderboard');\n static String get updateTransferDataUrl =>\n buildBackendUrl('/api/leaderboard/update');\n\n static String get healthCheckUrl => buildBackendUrl('/health');\n\n // 3D 佛像模型配置\n // 如果 R2 上需要切换到新的对象键,优先改这里,便于强制绕开旧缓存。\n static const String buddhaModelAssetPath = 'models/buddha_model.model';\n // Android/iOS both use the R2 flutter_scene .model; local cache misses\n // download through AssetLoaderService.\n\n // 当前线上正确模型明显大于 100MB,小于该阈值视为误传/降质文件。\n static const int minBuddhaModelSizeBytes = 100 * 1024 * 1024;\n\n // 请求头\n static Map<String, String> get defaultHeaders => {\n 'Content-Type': 'application/json',\n 'Accept': 'application/json',\n 'User-Agent': 'FabushiApp/${isWeb ? \"Web\" : \"Mobile\"}',\n };\n\n // 错误消息\n static const Map<String, String> errorMessages = {\n 'network_error': '网络连接失败,请检查网络设置',\n 'server_error': '服务器错误,请稍后重试',\n 'timeout_error': '请求超时,请检查网络连接',\n 'auth_error': '认证失败,请重新登录',\n 'permission_error': '权限不足',\n 'validation_error': '数据验证失败',\n 'unknown_error': '未知错误,请联系客服',\n };\n\n // 备用地址\n static List<String> get fallbackUrls {\n return [primaryBackendUrl];\n }\n\n // 调试配置\n static const bool enableApiLogging = true;\n static const bool debugMode = bool.fromEnvironment(\n 'DEBUG',\n defaultValue: false,\n );\n\n static void printConfigInfo() {\n if (kDebugMode) {\n print('=== 应用配置 ===');\n print('环境: ${isProduction ? \"生产\" : \"开发\"}');\n print('平台: ${isWeb ? \"Web\" : \"Native\"}');\n print('API URL: $currentBackendUrl');\n print('================');\n }\n }\n\n static void printCurrentConfig() => printConfigInfo();\n}\n"}, "a123d97fb8371710504a8535d0ca5e9878e632e5": {"t": 1785757957.1935632, "n": 9648, "h": "6ae9da985d0e34ff", "c": "import 'package:flutter/foundation.dart';\n\n/// 应用配置 - 统一管理所有配置项\nclass AppConfig {\n AppConfig._();\n\n static final AppConfig instance = AppConfig._();\n\n // 环境配置\n static const String environment = String.fromEnvironment(\n 'ENV',\n defaultValue: 'production',\n );\n\n static bool get isProduction {\n if (kIsWeb) {\n final currentUrl = Uri.base.toString();\n if (currentUrl.contains('fabushi-flutter-web-dev') ||\n currentUrl.contains('localhost')) {\n return false;\n }\n if (currentUrl.contains('fabushi-flutter-web-prod')) {\n return true;\n }\n }\n return environment == 'production';\n }\n\n static bool get isDevelopment => !isProduction;\n static bool get isStaging => environment == 'staging';\n static bool get isWeb => kIsWeb;\n\n // API配置\n static const String configuredApiBaseUrl = String.fromEnvironment(\n 'API_BASE_URL',\n defaultValue: '',\n );\n static const String primaryBackendUrl = 'https://api.ombhrum.com';\n static const String cloudflareWorkerProdUrl = 'https://api.ombhrum.com';\n static const String cloudflareWorkerDevUrl =\n 'https://fabushi-flutter-web-dev.bhrumom.workers.dev';\n static const String localDevUrl = 'http://localhost:8787';\n static const String publicWebUrl = 'https://flutter.ombhrum.com';\n static const String configuredAiBackendUrl = String.fromEnvironment(\n 'AI_BACKEND_URL',\n defaultValue: '',\n );\n static const String vpsAiBackendUrl = 'https://ai.ombhrum.com';\n\n static String get currentBackendUrl {\n if (configuredApiBaseUrl.isNotEmpty) {\n return configuredApiBaseUrl;\n }\n if (kIsWeb) {\n final currentUrl = Uri.base;\n final host = currentUrl.host;\n if (host.contains('localhost')) {\n return localDevUrl;\n }\n if (host.contains('fabushi-flutter-web-dev')) {\n return cloudflareWorkerDevUrl;\n }\n if (host.contains('fabushi-flutter-web-prod')) {\n return cloudflareWorkerProdUrl;\n }\n }\n return primaryBackendUrl;\n }\n\n static String get apiUrl => currentBackendUrl;\n\n static String get currentAiBackendUrl {\n if (configuredAiBackendUrl.isNotEmpty) {\n return configuredAiBackendUrl;\n }\n return vpsAiBackendUrl;\n }\n\n /// 大厂式 API 网关入口:客户端只允许构造自家后端的相对路径。\n ///\n /// 第三方 API、上游 API、缓存、重试、fallback 都应在后端处理;\n /// App 端不得直接传入 https://... 或 http://... 这类绝对 URL。\n static Uri buildBackendUri(\n String endpoint, {\n Map<String, String>? queryParameters,\n }) {\n final normalizedEndpoint = endpoint.trim();\n if (normalizedEndpoint.isEmpty || !normalizedEndpoint.startsWith('/')) {\n throw ArgumentError(\n 'API endpoint must be a first-party relative path starting with /: $endpoint',\n );\n }\n if (normalizedEndpoint.startsWith('//') ||\n normalizedEndpoint.contains('://')) {\n throw ArgumentError(\n 'API endpoint must not be an absolute or protocol-relative URL: $endpoint',\n );\n }\n\n final baseUrl = currentBackendUrl.replaceFirst(RegExp(r'/+$'), '');\n final uri = Uri.parse('$baseUrl$normalizedEndpoint');\n if (queryParameters == null || queryParameters.isEmpty) {\n return uri;\n }\n return uri.replace(queryParameters: queryParameters);\n }\n\n static String buildBackendUrl(\n String endpoint, {\n Map<String, String>? queryParameters,\n }) {\n return buildBackendUri(\n endpoint,\n queryParameters: queryParameters,\n ).toString();\n }\n\n // 应用信息\n static const String appName = '大乘';\n // 保持与 pubspec.yaml 中的 version 同步,供 Web 回退和服务端比对使用。\n static const String appVersion = '1.0.0';\n static const int appBuildNumber = 400;\n\n // 功能开关\n static const bool enableFirebaseAuth = true;\n static const bool enableAlipay = true;\n static const bool enableAppleIAP = true;\n static const bool enableVideoFeed = true;\n static bool get enableDebugMode => !isProduction;\n\n // 传输配置\n static const int fileChunkSize = 1024;\n static const int maxRetryCount = 3;\n static const int timeoutDuration = 5000;\n static const int maxConcurrentTransfers = 5;\n\n // 缓存配置\n static const int cacheMaxAge = 7;\n static const int maxCacheSize = 100;\n\n // 分页配置\n static const int defaultPageSize = 20;\n static const int maxPageSize = 100;\n\n // 超时配置\n static final Duration requestTimeout = const Duration(seconds: 30);\n static final Duration connectTimeout = const Duration(seconds: 10);\n\n // 重试配置\n static const int maxRetries = 3;\n static final Duration retryDelay = const Duration(seconds: 1);\n\n // 日志配置\n static const bool enableLogging = true;\n static bool get enableNetworkLogging => !isProduction;\n static bool get enablePerformanceLogging => !isProduction;\n\n // 存储键名\n static const String tokenStorageKey = 'auth_token';\n static const String userInfoStorageKey = 'user_info';\n static const String backendUrlStorageKey = 'backend_url';\n static const String testModeStorageKey = 'test_mode';\n\n // API端点\n static String get loginUrl => buildBackendUrl('/api/auth/login');\n static String get registerUrl => buildBackendUrl('/api/auth/register');\n static String get verifyUrl => buildBackendUrl('/api/auth/verify');\n static String get logoutUrl => buildBackendUrl('/api/auth/logout');\n static String get deleteAccountUrl => buildBackendUrl('/api/auth/delete');\n static String get sendVerificationCodeUrl =>\n buildBackendUrl('/api/auth/send-verification-code');\n static String get verifyCodeUrl => buildBackendUrl('/api/auth/verify-code');\n static String get forgotPasswordUrl =>\n buildBackendUrl('/api/auth/forgot-password');\n static String get resetPasswordUrl =>\n buildBackendUrl('/api/auth/reset-password');\n static String get userInfoUrl => buildBackendUrl('/api/auth/user-info');\n static String get bindEmailUrl => buildBackendUrl('/api/auth/bind-email');\n static String get appVersionPolicyUrl =>\n buildBackendUrl('/api/app/version-policy');\n\n static String get agentChatUrl => buildBackendUrl('/api/agent/chat');\n static String agentRunEventsUrl(String runId) =>\n buildBackendUrl('/api/agent/runs/$runId/events');\n static String agentRunCancelUrl(String runId) =>\n buildBackendUrl('/api/agent/runs/$runId/cancel');\n static String agentMessageFeedbackUrl(String messageId) =>\n buildBackendUrl('/api/agent/messages/$messageId/feedback');\n\n static String get alipayCreateOrderUrl =>\n buildBackendUrl('/api/alipay/create-order');\n static String get alipayQueryOrderUrl =>\n buildBackendUrl('/api/alipay/query-order');\n static String get alipayMembershipStatusUrl =>\n buildBackendUrl('/api/alipay/check-membership');\n\n static String get stripeMembershipStatusUrl =>\n buildBackendUrl('/api/stripe/membership-status');\n static String get stripeCreateSubscriptionUrl =>\n buildBackendUrl('/api/stripe/create-subscription');\n static String get stripeSessionStatusUrl =>\n buildBackendUrl('/api/stripe/session-status');\n\n static String get appleVerifyReceiptUrl =>\n buildBackendUrl('/api/apple/verify-receipt');\n static String get purchaseEntitlementUrl =>\n buildBackendUrl('/api/purchases/entitlement');\n\n static const String zenBuddhaAssetProductId = 'zen_buddha_asset';\n static const String zenBuddhaAssetDisplayName = '3D佛像素材';\n static const String zenBuddhaAssetPriceLabel = '¥33.00';\n static const String zenBuddhaAssetCardImagePath =\n 'assets/images/zen_buddha_material_card.png';\n\n static String get adminCheckStatusUrl =>\n buildBackendUrl('/api/admin/check-status');\n static String get adminCreateRedeemCodeUrl =>\n buildBackendUrl('/api/admin/create-redeem-code');\n static String get adminRedeemCodesUrl =>\n buildBackendUrl('/api/admin/redeem-codes');\n static String get adminUseRedeemCodeUrl =>\n buildBackendUrl('/api/admin/use-redeem-code');\n static String get adminPurchaseHistoryUrl =>\n buildBackendUrl('/api/admin/purchase-history');\n static String get adminRedeemHistoryUrl =>\n buildBackendUrl('/api/admin/redeem-history');\n\n static String get leaderboardUrl => buildBackendUrl('/api/leaderboard');\n static String get updateTransferDataUrl =>\n buildBackendUrl('/api/leaderboard/update');\n\n static String get healthCheckUrl => buildBackendUrl('/health');\n\n // 3D 佛像模型配置\n // 如果 R2 上需要切换到新的对象键,优先改这里,便于强制绕开旧缓存。\n static const String buddhaModelAssetPath = 'models/buddha_model.model';\n // Android/iOS both use the R2 flutter_scene .model; local cache misses\n // download through AssetLoaderService.\n\n // 当前线上正确模型明显大于 100MB,小于该阈值视为误传/降质文件。\n static const int minBuddhaModelSizeBytes = 100 * 1024 * 1024;\n\n // 请求头\n static Map<String, String> get defaultHeaders => {\n 'Content-Type': 'application/json',\n 'Accept': 'application/json',\n 'User-Agent': 'FabushiApp/${isWeb ? \"Web\" : \"Mobile\"}',\n };\n\n // 错误消息\n static const Map<String, String> errorMessages = {\n 'network_error': '网络连接失败,请检查网络设置',\n 'server_error': '服务器错误,请稍后重试',\n 'timeout_error': '请求超时,请检查网络连接',\n 'auth_error': '认证失败,请重新登录',\n 'permission_error': '权限不足',\n 'validation_error': '数据验证失败',\n 'unknown_error': '未知错误,请联系客服',\n };\n\n // 备用地址\n static List<String> get fallbackUrls {\n return [primaryBackendUrl];\n }\n\n // 调试配置\n static const bool enableApiLogging = true;\n static const bool debugMode = bool.fromEnvironment(\n 'DEBUG',\n defaultValue: false,\n );\n\n static void printConfigInfo() {\n if (kDebugMode) {\n print('=== 应用配置 ===');\n print('环境: ${isProduction ? \"生产\" : \"开发\"}');\n print('平台: ${isWeb ? \"Web\" : \"Native\"}');\n print('API URL: $currentBackendUrl');\n print('================');\n }\n }\n\n static void printCurrentConfig() => printConfigInfo();\n}\n"}, "ec534cf1c276d6da84968ab8d4eb53f160f9257d": {"t": 1785757958.4539776, "n": 9234, "h": "5c0dfbcb18b55c60", "c": "import 'package:flutter/foundation.dart';\n\n/// 应用配置 - 统一管理所有配置项\nclass AppConfig {\n AppConfig._();\n\n static final AppConfig instance = AppConfig._();\n\n // 环境配置\n static const String environment = String.fromEnvironment(\n 'ENV',\n defaultValue: 'production',\n );\n\n static bool get isProduction {\n if (kIsWeb) {\n final currentUrl = Uri.base.toString();\n if (currentUrl.contains('fabushi-flutter-web-dev') ||\n currentUrl.contains('localhost')) {\n return false;\n }\n if (currentUrl.contains('fabushi-flutter-web-prod')) {\n return true;\n }\n }\n return environment == 'production';\n }\n\n static bool get isDevelopment => !isProduction;\n static bool get isStaging => environment == 'staging';\n static bool get isWeb => kIsWeb;\n\n // API配置\n static const String configuredApiBaseUrl = String.fromEnvironment(\n 'API_BASE_URL',\n defaultValue: '',\n );\n static const String primaryBackendUrl = 'https://api.ombhrum.com';\n static const String cloudflareWorkerProdUrl = 'https://api.ombhrum.com';\n static const String cloudflareWorkerDevUrl =\n 'https://fabushi-flutter-web-dev.bhrumom.workers.dev';\n static const String localDevUrl = 'http://localhost:8787';\n static const String publicWebUrl = 'https://flutter.ombhrum.com';\n static const String configuredAiBackendUrl = String.fromEnvironment(\n 'AI_BACKEND_URL',\n defaultValue: '',\n );\n static const String vpsAiBackendUrl = 'https://ai.ombhrum.com';\n\n static String get currentBackendUrl {\n if (configuredApiBaseUrl.isNotEmpty) {\n return configuredApiBaseUrl;\n }\n if (kIsWeb) {\n final currentUrl = Uri.base;\n final host = currentUrl.host;\n if (host.contains('localhost')) {\n return localDevUrl;\n }\n if (host.contains('fabushi-flutter-web-dev')) {\n return cloudflareWorkerDevUrl;\n }\n if (host.contains('fabushi-flutter-web-prod')) {\n return cloudflareWorkerProdUrl;\n }\n }\n return primaryBackendUrl;\n }\n\n static String get apiUrl => currentBackendUrl;\n\n static String get currentAiBackendUrl {\n if (configuredAiBackendUrl.isNotEmpty) {\n return configuredAiBackendUrl;\n }\n return vpsAiBackendUrl;\n }\n\n /// 大厂式 API 网关入口:客户端只允许构造自家后端的相对路径。\n ///\n /// 第三方 API、上游 API、缓存、重试、fallback 都应在后端处理;\n /// App 端不得直接传入 https://... 或 http://... 这类绝对 URL。\n static Uri buildBackendUri(\n String endpoint, {\n Map<String, String>? queryParameters,\n }) {\n final normalizedEndpoint = endpoint.trim();\n if (normalizedEndpoint.isEmpty || !normalizedEndpoint.startsWith('/')) {\n throw ArgumentError(\n 'API endpoint must be a first-party relative path starting with /: $endpoint',\n );\n }\n if (normalizedEndpoint.startsWith('//') ||\n normalizedEndpoint.contains('://')) {\n throw ArgumentError(\n 'API endpoint must not be an absolute or protocol-relative URL: $endpoint',\n );\n }\n\n final baseUrl = currentBackendUrl.replaceFirst(RegExp(r'/+$'), '');\n final uri = Uri.parse('$baseUrl$normalizedEndpoint');\n if (queryParameters == null || queryParameters.isEmpty) {\n return uri;\n }\n return uri.replace(queryParameters: queryParameters);\n }\n\n static String buildBackendUrl(\n String endpoint, {\n Map<String, String>? queryParameters,\n }) {\n return buildBackendUri(\n endpoint,\n queryParameters: queryParameters,\n ).toString();\n }\n\n // 应用信息\n static const String appName = '大乘';\n // 保持与 pubspec.yaml 中的 version 同步,供 Web 回退和服务端比对使用。\n static const String appVersion = '1.0.0';\n static const int appBuildNumber = 400;\n\n // 功能开关\n static const bool enableFirebaseAuth = true;\n static const bool enableAlipay = true;\n static const bool enableAppleIAP = true;\n static const bool enableVideoFeed = true;\n static bool get enableDebugMode => !isProduction;\n\n // 传输配置\n static const int fileChunkSize = 1024;\n static const int maxRetryCount = 3;\n static const int timeoutDuration = 5000;\n static const int maxConcurrentTransfers = 5;\n\n // 缓存配置\n static const int cacheMaxAge = 7;\n static const int maxCacheSize = 100;\n\n // 分页配置\n static const int defaultPageSize = 20;\n static const int maxPageSize = 100;\n\n // 超时配置\n static final Duration requestTimeout = const Duration(seconds: 30);\n static final Duration connectTimeout = const Duration(seconds: 10);\n\n // 重试配置\n static const int maxRetries = 3;\n static final Duration retryDelay = const Duration(seconds: 1);\n\n // 日志配置\n static const bool enableLogging = true;\n static bool get enableNetworkLogging => !isProduction;\n static bool get enablePerformanceLogging => !isProduction;\n\n // 存储键名\n static const String tokenStorageKey = 'auth_token';\n static const String userInfoStorageKey = 'user_info';\n static const String backendUrlStorageKey = 'backend_url';\n static const String testModeStorageKey = 'test_mode';\n\n // API端点\n static String get loginUrl => buildBackendUrl('/api/auth/login');\n static String get registerUrl => buildBackendUrl('/api/auth/register');\n static String get verifyUrl => buildBackendUrl('/api/auth/verify');\n static String get logoutUrl => buildBackendUrl('/api/auth/logout');\n static String get deleteAccountUrl => buildBackendUrl('/api/auth/delete');\n static String get sendVerificationCodeUrl =>\n buildBackendUrl('/api/auth/send-verification-code');\n static String get verifyCodeUrl => buildBackendUrl('/api/auth/verify-code');\n static String get forgotPasswordUrl =>\n buildBackendUrl('/api/auth/forgot-password');\n static String get resetPasswordUrl =>\n buildBackendUrl('/api/auth/reset-password');\n static String get userInfoUrl => buildBackendUrl('/api/auth/user-info');\n static String get bindEmailUrl => buildBackendUrl('/api/auth/bind-email');\n static String get appVersionPolicyUrl =>\n buildBackendUrl('/api/app/version-policy');\n\n static String get alipayCreateOrderUrl =>\n buildBackendUrl('/api/alipay/create-order');\n static String get alipayQueryOrderUrl =>\n buildBackendUrl('/api/alipay/query-order');\n static String get alipayMembershipStatusUrl =>\n buildBackendUrl('/api/alipay/check-membership');\n\n static String get stripeMembershipStatusUrl =>\n buildBackendUrl('/api/stripe/membership-status');\n static String get stripeCreateSubscriptionUrl =>\n buildBackendUrl('/api/stripe/create-subscription');\n static String get stripeSessionStatusUrl =>\n buildBackendUrl('/api/stripe/session-status');\n\n static String get appleVerifyReceiptUrl =>\n buildBackendUrl('/api/apple/verify-receipt');\n static String get purchaseEntitlementUrl =>\n buildBackendUrl('/api/purchases/entitlement');\n\n static const String zenBuddhaAssetProductId = 'zen_buddha_asset';\n static const String zenBuddhaAssetDisplayName = '3D佛像素材';\n static const String zenBuddhaAssetPriceLabel = '¥33.00';\n static const String zenBuddhaAssetCardImagePath =\n 'assets/images/zen_buddha_material_card.png';\n\n static String get adminCheckStatusUrl =>\n buildBackendUrl('/api/admin/check-status');\n static String get adminCreateRedeemCodeUrl =>\n buildBackendUrl('/api/admin/create-redeem-code');\n static String get adminRedeemCodesUrl =>\n buildBackendUrl('/api/admin/redeem-codes');\n static String get adminUseRedeemCodeUrl =>\n buildBackendUrl('/api/admin/use-redeem-code');\n static String get adminPurchaseHistoryUrl =>\n buildBackendUrl('/api/admin/purchase-history');\n static String get adminRedeemHistoryUrl =>\n buildBackendUrl('/api/admin/redeem-history');\n\n static String get leaderboardUrl => buildBackendUrl('/api/leaderboard');\n static String get updateTransferDataUrl =>\n buildBackendUrl('/api/leaderboard/update');\n\n static String get healthCheckUrl => buildBackendUrl('/health');\n\n // 3D 佛像模型配置\n // 如果 R2 上需要切换到新的对象键,优先改这里,便于强制绕开旧缓存。\n static const String buddhaModelAssetPath = 'models/buddha_model.model';\n // Android/iOS both use the R2 flutter_scene .model; local cache misses\n // download through AssetLoaderService.\n\n // 当前线上正确模型明显大于 100MB,小于该阈值视为误传/降质文件。\n static const int minBuddhaModelSizeBytes = 100 * 1024 * 1024;\n\n // 请求头\n static Map<String, String> get defaultHeaders => {\n 'Content-Type': 'application/json',\n 'Accept': 'application/json',\n 'User-Agent': 'FabushiApp/${isWeb ? \"Web\" : \"Mobile\"}',\n };\n\n // 错误消息\n static const Map<String, String> errorMessages = {\n 'network_error': '网络连接失败,请检查网络设置',\n 'server_error': '服务器错误,请稍后重试',\n 'timeout_error': '请求超时,请检查网络连接',\n 'auth_error': '认证失败,请重新登录',\n 'permission_error': '权限不足',\n 'validation_error': '数据验证失败',\n 'unknown_error': '未知错误,请联系客服',\n };\n\n // 备用地址\n static List<String> get fallbackUrls {\n return [primaryBackendUrl];\n }\n\n // 调试配置\n static const bool enableApiLogging = true;\n static const bool debugMode = bool.fromEnvironment(\n 'DEBUG',\n defaultValue: false,\n );\n\n static void printConfigInfo() {\n if (kDebugMode) {\n print('=== 应用配置 ===');\n print('环境: ${isProduction ? \"生产\" : \"开发\"}');\n print('平台: ${isWeb ? \"Web\" : \"Native\"}');\n print('API URL: $currentBackendUrl');\n print('================');\n }\n }\n\n static void printCurrentConfig() => printConfigInfo();\n}\n"}, "7b5358f7f07c445a317052859bf1db8e6d7d7e7c": {"t": 1785757964.410439, "n": 9233, "h": "c65ee3296b10170f", "c": "import 'package:flutter/foundation.dart';\n\n/// 应用配置 - 统一管理所有配置项\nclass AppConfig {\n AppConfig._();\n\n static final AppConfig instance = AppConfig._();\n\n // 环境配置\n static const String environment = String.fromEnvironment(\n 'ENV',\n defaultValue: 'production',\n );\n\n static bool get isProduction {\n if (kIsWeb) {\n final currentUrl = Uri.base.toString();\n if (currentUrl.contains('fabushi-flutter-web-dev') ||\n currentUrl.contains('localhost')) {\n return false;\n }\n if (currentUrl.contains('fabushi-flutter-web-prod')) {\n return true;\n }\n }\n return environment == 'production';\n }\n\n static bool get isDevelopment => !isProduction;\n static bool get isStaging => environment == 'staging';\n static bool get isWeb => kIsWeb;\n\n // API配置\n static const String configuredApiBaseUrl = String.fromEnvironment(\n 'API_BASE_URL',\n defaultValue: '',\n );\n static const String primaryBackendUrl = 'https://api.ombhrum.com';\n static const String cloudflareWorkerProdUrl = 'https://api.ombhrum.com';\n static const String cloudflareWorkerDevUrl =\n 'https://fabushi-flutter-web-dev.bhrumom.workers.dev';\n static const String localDevUrl = 'http://localhost:8787';\n static const String publicWebUrl = 'https://flutter.ombhrum.com';\n static const String configuredAiBackendUrl = String.fromEnvironment(\n 'AI_BACKEND_URL',\n defaultValue: '',\n );\n static const String vpsAiBackendUrl = 'http://141.148.140.39';\n\n static String get currentBackendUrl {\n if (configuredApiBaseUrl.isNotEmpty) {\n return configuredApiBaseUrl;\n }\n if (kIsWeb) {\n final currentUrl = Uri.base;\n final host = currentUrl.host;\n if (host.contains('localhost')) {\n return localDevUrl;\n }\n if (host.contains('fabushi-flutter-web-dev')) {\n return cloudflareWorkerDevUrl;\n }\n if (host.contains('fabushi-flutter-web-prod')) {\n return cloudflareWorkerProdUrl;\n }\n }\n return primaryBackendUrl;\n }\n\n static String get apiUrl => currentBackendUrl;\n\n static String get currentAiBackendUrl {\n if (configuredAiBackendUrl.isNotEmpty) {\n return configuredAiBackendUrl;\n }\n return vpsAiBackendUrl;\n }\n\n /// 大厂式 API 网关入口:客户端只允许构造自家后端的相对路径。\n ///\n /// 第三方 API、上游 API、缓存、重试、fallback 都应在后端处理;\n /// App 端不得直接传入 https://... 或 http://... 这类绝对 URL。\n static Uri buildBackendUri(\n String endpoint, {\n Map<String, String>? queryParameters,\n }) {\n final normalizedEndpoint = endpoint.trim();\n if (normalizedEndpoint.isEmpty || !normalizedEndpoint.startsWith('/')) {\n throw ArgumentError(\n 'API endpoint must be a first-party relative path starting with /: $endpoint',\n );\n }\n if (normalizedEndpoint.startsWith('//') ||\n normalizedEndpoint.contains('://')) {\n throw ArgumentError(\n 'API endpoint must not be an absolute or protocol-relative URL: $endpoint',\n );\n }\n\n final baseUrl = currentBackendUrl.replaceFirst(RegExp(r'/+$'), '');\n final uri = Uri.parse('$baseUrl$normalizedEndpoint');\n if (queryParameters == null || queryParameters.isEmpty) {\n return uri;\n }\n return uri.replace(queryParameters: queryParameters);\n }\n\n static String buildBackendUrl(\n String endpoint, {\n Map<String, String>? queryParameters,\n }) {\n return buildBackendUri(\n endpoint,\n queryParameters: queryParameters,\n ).toString();\n }\n\n // 应用信息\n static const String appName = '大乘';\n // 保持与 pubspec.yaml 中的 version 同步,供 Web 回退和服务端比对使用。\n static const String appVersion = '1.0.0';\n static const int appBuildNumber = 400;\n\n // 功能开关\n static const bool enableFirebaseAuth = true;\n static const bool enableAlipay = true;\n static const bool enableAppleIAP = true;\n static const bool enableVideoFeed = true;\n static bool get enableDebugMode => !isProduction;\n\n // 传输配置\n static const int fileChunkSize = 1024;\n static const int maxRetryCount = 3;\n static const int timeoutDuration = 5000;\n static const int maxConcurrentTransfers = 5;\n\n // 缓存配置\n static const int cacheMaxAge = 7;\n static const int maxCacheSize = 100;\n\n // 分页配置\n static const int defaultPageSize = 20;\n static const int maxPageSize = 100;\n\n // 超时配置\n static final Duration requestTimeout = const Duration(seconds: 30);\n static final Duration connectTimeout = const Duration(seconds: 10);\n\n // 重试配置\n static const int maxRetries = 3;\n static final Duration retryDelay = const Duration(seconds: 1);\n\n // 日志配置\n static const bool enableLogging = true;\n static bool get enableNetworkLogging => !isProduction;\n static bool get enablePerformanceLogging => !isProduction;\n\n // 存储键名\n static const String tokenStorageKey = 'auth_token';\n static const String userInfoStorageKey = 'user_info';\n static const String backendUrlStorageKey = 'backend_url';\n static const String testModeStorageKey = 'test_mode';\n\n // API端点\n static String get loginUrl => buildBackendUrl('/api/auth/login');\n static String get registerUrl => buildBackendUrl('/api/auth/register');\n static String get verifyUrl => buildBackendUrl('/api/auth/verify');\n static String get logoutUrl => buildBackendUrl('/api/auth/logout');\n static String get deleteAccountUrl => buildBackendUrl('/api/auth/delete');\n static String get sendVerificationCodeUrl =>\n buildBackendUrl('/api/auth/send-verification-code');\n static String get verifyCodeUrl => buildBackendUrl('/api/auth/verify-code');\n static String get forgotPasswordUrl =>\n buildBackendUrl('/api/auth/forgot-password');\n static String get resetPasswordUrl =>\n buildBackendUrl('/api/auth/reset-password');\n static String get userInfoUrl => buildBackendUrl('/api/auth/user-info');\n static String get bindEmailUrl => buildBackendUrl('/api/auth/bind-email');\n static String get appVersionPolicyUrl =>\n buildBackendUrl('/api/app/version-policy');\n\n static String get alipayCreateOrderUrl =>\n buildBackendUrl('/api/alipay/create-order');\n static String get alipayQueryOrderUrl =>\n buildBackendUrl('/api/alipay/query-order');\n static String get alipayMembershipStatusUrl =>\n buildBackendUrl('/api/alipay/check-membership');\n\n static String get stripeMembershipStatusUrl =>\n buildBackendUrl('/api/stripe/membership-status');\n static String get stripeCreateSubscriptionUrl =>\n buildBackendUrl('/api/stripe/create-subscription');\n static String get stripeSessionStatusUrl =>\n buildBackendUrl('/api/stripe/session-status');\n\n static String get appleVerifyReceiptUrl =>\n buildBackendUrl('/api/apple/verify-receipt');\n static String get purchaseEntitlementUrl =>\n buildBackendUrl('/api/purchases/entitlement');\n\n static const String zenBuddhaAssetProductId = 'zen_buddha_asset';\n static const String zenBuddhaAssetDisplayName = '3D佛像素材';\n static const String zenBuddhaAssetPriceLabel = '¥33.00';\n static const String zenBuddhaAssetCardImagePath =\n 'assets/images/zen_buddha_material_card.png';\n\n static String get adminCheckStatusUrl =>\n buildBackendUrl('/api/admin/check-status');\n static String get adminCreateRedeemCodeUrl =>\n buildBackendUrl('/api/admin/create-redeem-code');\n static String get adminRedeemCodesUrl =>\n buildBackendUrl('/api/admin/redeem-codes');\n static String get adminUseRedeemCodeUrl =>\n buildBackendUrl('/api/admin/use-redeem-code');\n static String get adminPurchaseHistoryUrl =>\n buildBackendUrl('/api/admin/purchase-history');\n static String get adminRedeemHistoryUrl =>\n buildBackendUrl('/api/admin/redeem-history');\n\n static String get leaderboardUrl => buildBackendUrl('/api/leaderboard');\n static String get updateTransferDataUrl =>\n buildBackendUrl('/api/leaderboard/update');\n\n static String get healthCheckUrl => buildBackendUrl('/health');\n\n // 3D 佛像模型配置\n // 如果 R2 上需要切换到新的对象键,优先改这里,便于强制绕开旧缓存。\n static const String buddhaModelAssetPath = 'models/buddha_model.model';\n // Android/iOS both use the R2 flutter_scene .model; local cache misses\n // download through AssetLoaderService.\n\n // 当前线上正确模型明显大于 100MB,小于该阈值视为误传/降质文件。\n static const int minBuddhaModelSizeBytes = 100 * 1024 * 1024;\n\n // 请求头\n static Map<String, String> get defaultHeaders => {\n 'Content-Type': 'application/json',\n 'Accept': 'application/json',\n 'User-Agent': 'FabushiApp/${isWeb ? \"Web\" : \"Mobile\"}',\n };\n\n // 错误消息\n static const Map<String, String> errorMessages = {\n 'network_error': '网络连接失败,请检查网络设置',\n 'server_error': '服务器错误,请稍后重试',\n 'timeout_error': '请求超时,请检查网络连接',\n 'auth_error': '认证失败,请重新登录',\n 'permission_error': '权限不足',\n 'validation_error': '数据验证失败',\n 'unknown_error': '未知错误,请联系客服',\n };\n\n // 备用地址\n static List<String> get fallbackUrls {\n return [primaryBackendUrl];\n }\n\n // 调试配置\n static const bool enableApiLogging = true;\n static const bool debugMode = bool.fromEnvironment(\n 'DEBUG',\n defaultValue: false,\n );\n\n static void printConfigInfo() {\n if (kDebugMode) {\n print('=== 应用配置 ===');\n print('环境: ${isProduction ? \"生产\" : \"开发\"}');\n print('平台: ${isWeb ? \"Web\" : \"Native\"}');\n print('API URL: $currentBackendUrl');\n print('================');\n }\n }\n\n static void printCurrentConfig() => printConfigInfo();\n}\n"}, "b1acfa3a7a5cc0a4dbba10a87672aa919fcfe6e9": {"t": 1785757965.662462, "n": 9235, "h": "a4e6e1c265213d25", "c": "import 'package:flutter/foundation.dart';\n\n/// 应用配置 - 统一管理所有配置项\nclass AppConfig {\n AppConfig._();\n\n static final AppConfig instance = AppConfig._();\n\n // 环境配置\n static const String environment = String.fromEnvironment(\n 'ENV',\n defaultValue: 'production',\n );\n\n static bool get isProduction {\n if (kIsWeb) {\n final currentUrl = Uri.base.toString();\n if (currentUrl.contains('fabushi-flutter-web-dev') ||\n currentUrl.contains('localhost')) {\n return false;\n }\n if (currentUrl.contains('fabushi-flutter-web-prod')) {\n return true;\n }\n }\n return environment == 'production';\n }\n\n static bool get isDevelopment => !isProduction;\n static bool get isStaging => environment == 'staging';\n static bool get isWeb => kIsWeb;\n\n // API配置\n static const String configuredApiBaseUrl = String.fromEnvironment(\n 'API_BASE_URL',\n defaultValue: '',\n );\n static const String primaryBackendUrl = 'https://api.ombhrum.com';\n static const String cloudflareWorkerProdUrl = 'https://api.ombhrum.com';\n static const String cloudflareWorkerDevUrl =\n 'https://fabushi-flutter-web-dev.bhrumom.workers.dev';\n static const String localDevUrl = 'http://localhost:8787';\n static const String publicWebUrl = 'https://flutter.ombhrum.com';\n static const String configuredAiBackendUrl = String.fromEnvironment(\n 'AI_BACKEND_URL',\n defaultValue: '',\n );\n static const String vpsAiBackendUrl = 'http://141.148.140.39';\n\n static String get currentBackendUrl {\n if (configuredApiBaseUrl.isNotEmpty) {\n return configuredApiBaseUrl;\n }\n if (kIsWeb) {\n final currentUrl = Uri.base;\n final host = currentUrl.host;\n if (host.contains('localhost')) {\n return localDevUrl;\n }\n if (host.contains('fabushi-flutter-web-dev')) {\n return cloudflareWorkerDevUrl;\n }\n if (host.contains('fabushi-flutter-web-prod')) {\n return cloudflareWorkerProdUrl;\n }\n }\n return primaryBackendUrl;\n }\n\n static String get apiUrl => currentBackendUrl;\n\n static String get currentAiBackendUrl {\n if (configuredAiBackendUrl.isNotEmpty) {\n return configuredAiBackendUrl;\n }\n return currentBackendUrl;\n }\n\n /// 大厂式 API 网关入口:客户端只允许构造自家后端的相对路径。\n ///\n /// 第三方 API、上游 API、缓存、重试、fallback 都应在后端处理;\n /// App 端不得直接传入 https://... 或 http://... 这类绝对 URL。\n static Uri buildBackendUri(\n String endpoint, {\n Map<String, String>? queryParameters,\n }) {\n final normalizedEndpoint = endpoint.trim();\n if (normalizedEndpoint.isEmpty || !normalizedEndpoint.startsWith('/')) {\n throw ArgumentError(\n 'API endpoint must be a first-party relative path starting with /: $endpoint',\n );\n }\n if (normalizedEndpoint.startsWith('//') ||\n normalizedEndpoint.contains('://')) {\n throw ArgumentError(\n 'API endpoint must not be an absolute or protocol-relative URL: $endpoint',\n );\n }\n\n final baseUrl = currentBackendUrl.replaceFirst(RegExp(r'/+$'), '');\n final uri = Uri.parse('$baseUrl$normalizedEndpoint');\n if (queryParameters == null || queryParameters.isEmpty) {\n return uri;\n }\n return uri.replace(queryParameters: queryParameters);\n }\n\n static String buildBackendUrl(\n String endpoint, {\n Map<String, String>? queryParameters,\n }) {\n return buildBackendUri(\n endpoint,\n queryParameters: queryParameters,\n ).toString();\n }\n\n // 应用信息\n static const String appName = '大乘';\n // 保持与 pubspec.yaml 中的 version 同步,供 Web 回退和服务端比对使用。\n static const String appVersion = '1.0.0';\n static const int appBuildNumber = 400;\n\n // 功能开关\n static const bool enableFirebaseAuth = true;\n static const bool enableAlipay = true;\n static const bool enableAppleIAP = true;\n static const bool enableVideoFeed = true;\n static bool get enableDebugMode => !isProduction;\n\n // 传输配置\n static const int fileChunkSize = 1024;\n static const int maxRetryCount = 3;\n static const int timeoutDuration = 5000;\n static const int maxConcurrentTransfers = 5;\n\n // 缓存配置\n static const int cacheMaxAge = 7;\n static const int maxCacheSize = 100;\n\n // 分页配置\n static const int defaultPageSize = 20;\n static const int maxPageSize = 100;\n\n // 超时配置\n static final Duration requestTimeout = const Duration(seconds: 30);\n static final Duration connectTimeout = const Duration(seconds: 10);\n\n // 重试配置\n static const int maxRetries = 3;\n static final Duration retryDelay = const Duration(seconds: 1);\n\n // 日志配置\n static const bool enableLogging = true;\n static bool get enableNetworkLogging => !isProduction;\n static bool get enablePerformanceLogging => !isProduction;\n\n // 存储键名\n static const String tokenStorageKey = 'auth_token';\n static const String userInfoStorageKey = 'user_info';\n static const String backendUrlStorageKey = 'backend_url';\n static const String testModeStorageKey = 'test_mode';\n\n // API端点\n static String get loginUrl => buildBackendUrl('/api/auth/login');\n static String get registerUrl => buildBackendUrl('/api/auth/register');\n static String get verifyUrl => buildBackendUrl('/api/auth/verify');\n static String get logoutUrl => buildBackendUrl('/api/auth/logout');\n static String get deleteAccountUrl => buildBackendUrl('/api/auth/delete');\n static String get sendVerificationCodeUrl =>\n buildBackendUrl('/api/auth/send-verification-code');\n static String get verifyCodeUrl => buildBackendUrl('/api/auth/verify-code');\n static String get forgotPasswordUrl =>\n buildBackendUrl('/api/auth/forgot-password');\n static String get resetPasswordUrl =>\n buildBackendUrl('/api/auth/reset-password');\n static String get userInfoUrl => buildBackendUrl('/api/auth/user-info');\n static String get bindEmailUrl => buildBackendUrl('/api/auth/bind-email');\n static String get appVersionPolicyUrl =>\n buildBackendUrl('/api/app/version-policy');\n\n static String get alipayCreateOrderUrl =>\n buildBackendUrl('/api/alipay/create-order');\n static String get alipayQueryOrderUrl =>\n buildBackendUrl('/api/alipay/query-order');\n static String get alipayMembershipStatusUrl =>\n buildBackendUrl('/api/alipay/check-membership');\n\n static String get stripeMembershipStatusUrl =>\n buildBackendUrl('/api/stripe/membership-status');\n static String get stripeCreateSubscriptionUrl =>\n buildBackendUrl('/api/stripe/create-subscription');\n static String get stripeSessionStatusUrl =>\n buildBackendUrl('/api/stripe/session-status');\n\n static String get appleVerifyReceiptUrl =>\n buildBackendUrl('/api/apple/verify-receipt');\n static String get purchaseEntitlementUrl =>\n buildBackendUrl('/api/purchases/entitlement');\n\n static const String zenBuddhaAssetProductId = 'zen_buddha_asset';\n static const String zenBuddhaAssetDisplayName = '禅室佛像素材';\n static const String zenBuddhaAssetPriceLabel = '¥33.00';\n static const String zenBuddhaAssetCardImagePath =\n 'assets/images/zen_buddha_material_card.png';\n\n static String get adminCheckStatusUrl =>\n buildBackendUrl('/api/admin/check-status');\n static String get adminCreateRedeemCodeUrl =>\n buildBackendUrl('/api/admin/create-redeem-code');\n static String get adminRedeemCodesUrl =>\n buildBackendUrl('/api/admin/redeem-codes');\n static String get adminUseRedeemCodeUrl =>\n buildBackendUrl('/api/admin/use-redeem-code');\n static String get adminPurchaseHistoryUrl =>\n buildBackendUrl('/api/admin/purchase-history');\n static String get adminRedeemHistoryUrl =>\n buildBackendUrl('/api/admin/redeem-history');\n\n static String get leaderboardUrl => buildBackendUrl('/api/leaderboard');\n static String get updateTransferDataUrl =>\n buildBackendUrl('/api/leaderboard/update');\n\n static String get healthCheckUrl => buildBackendUrl('/health');\n\n // 3D 佛像模型配置\n // 如果 R2 上需要切换到新的对象键,优先改这里,便于强制绕开旧缓存。\n static const String buddhaModelAssetPath = 'models/buddha_model.model';\n // Android/iOS both use the R2 flutter_scene .model; local cache misses\n // download through AssetLoaderService.\n\n // 当前线上正确模型明显大于 100MB,小于该阈值视为误传/降质文件。\n static const int minBuddhaModelSizeBytes = 100 * 1024 * 1024;\n\n // 请求头\n static Map<String, String> get defaultHeaders => {\n 'Content-Type': 'application/json',\n 'Accept': 'application/json',\n 'User-Agent': 'FabushiApp/${isWeb ? \"Web\" : \"Mobile\"}',\n };\n\n // 错误消息\n static const Map<String, String> errorMessages = {\n 'network_error': '网络连接失败,请检查网络设置',\n 'server_error': '服务器错误,请稍后重试',\n 'timeout_error': '请求超时,请检查网络连接',\n 'auth_error': '认证失败,请重新登录',\n 'permission_error': '权限不足',\n 'validation_error': '数据验证失败',\n 'unknown_error': '未知错误,请联系客服',\n };\n\n // 备用地址\n static List<String> get fallbackUrls {\n return [primaryBackendUrl];\n }\n\n // 调试配置\n static const bool enableApiLogging = true;\n static const bool debugMode = bool.fromEnvironment(\n 'DEBUG',\n defaultValue: false,\n );\n\n static void printConfigInfo() {\n if (kDebugMode) {\n print('=== 应用配置 ===');\n print('环境: ${isProduction ? \"生产\" : \"开发\"}');\n print('平台: ${isWeb ? \"Web\" : \"Native\"}');\n print('API URL: $currentBackendUrl');\n print('================');\n }\n }\n\n static void printCurrentConfig() => printConfigInfo();\n}\n"}, "cf596286c965b8e81a1c3878344a7af5d890ef4e": {"t": 1785757967.2537858, "n": 8885, "h": "628eb5174a824c16", "c": "import 'package:flutter/foundation.dart';\n\n/// 应用配置 - 统一管理所有配置项\nclass AppConfig {\n AppConfig._();\n\n static final AppConfig instance = AppConfig._();\n\n // 环境配置\n static const String environment = String.fromEnvironment(\n 'ENV',\n defaultValue: 'production',\n );\n\n static bool get isProduction {\n if (kIsWeb) {\n final currentUrl = Uri.base.toString();\n if (currentUrl.contains('fabushi-flutter-web-dev') ||\n currentUrl.contains('localhost')) {\n return false;\n }\n if (currentUrl.contains('fabushi-flutter-web-prod')) {\n return true;\n }\n }\n return environment == 'production';\n }\n\n static bool get isDevelopment => !isProduction;\n static bool get isStaging => environment == 'staging';\n static bool get isWeb => kIsWeb;\n\n // API配置\n static const String configuredApiBaseUrl = String.fromEnvironment(\n 'API_BASE_URL',\n defaultValue: '',\n );\n static const String primaryBackendUrl = 'https://api.ombhrum.com';\n static const String cloudflareWorkerProdUrl = 'https://api.ombhrum.com';\n static const String cloudflareWorkerDevUrl =\n 'https://fabushi-flutter-web-dev.bhrumom.workers.dev';\n static const String localDevUrl = 'http://localhost:8787';\n static const String publicWebUrl = 'https://flutter.ombhrum.com';\n\n static String get currentBackendUrl {\n if (configuredApiBaseUrl.isNotEmpty) {\n return configuredApiBaseUrl;\n }\n if (kIsWeb) {\n final currentUrl = Uri.base;\n final host = currentUrl.host;\n if (host.contains('localhost')) {\n return localDevUrl;\n }\n if (host.contains('fabushi-flutter-web-dev')) {\n return cloudflareWorkerDevUrl;\n }\n if (host.contains('fabushi-flutter-web-prod')) {\n return cloudflareWorkerProdUrl;\n }\n }\n return primaryBackendUrl;\n }\n\n static String get apiUrl => currentBackendUrl;\n\n /// 大厂式 API 网关入口:客户端只允许构造自家后端的相对路径。\n ///\n /// 第三方 API、上游 API、缓存、重试、fallback 都应在后端处理;\n /// App 端不得直接传入 https://... 或 http://... 这类绝对 URL。\n static Uri buildBackendUri(\n String endpoint, {\n Map<String, String>? queryParameters,\n }) {\n final normalizedEndpoint = endpoint.trim();\n if (normalizedEndpoint.isEmpty || !normalizedEndpoint.startsWith('/')) {\n throw ArgumentError(\n 'API endpoint must be a first-party relative path starting with /: $endpoint',\n );\n }\n if (normalizedEndpoint.startsWith('//') ||\n normalizedEndpoint.contains('://')) {\n throw ArgumentError(\n 'API endpoint must not be an absolute or protocol-relative URL: $endpoint',\n );\n }\n\n final baseUrl = currentBackendUrl.replaceFirst(RegExp(r'/+$'), '');\n final uri = Uri.parse('$baseUrl$normalizedEndpoint');\n if (queryParameters == null || queryParameters.isEmpty) {\n return uri;\n }\n return uri.replace(queryParameters: queryParameters);\n }\n\n static String buildBackendUrl(\n String endpoint, {\n Map<String, String>? queryParameters,\n }) {\n return buildBackendUri(\n endpoint,\n queryParameters: queryParameters,\n ).toString();\n }\n\n // 应用信息\n static const String appName = '大乘';\n // 保持与 pubspec.yaml 中的 version 同步,供 Web 回退和服务端比对使用。\n static const String appVersion = '1.0.0';\n static const int appBuildNumber = 400;\n\n // 功能开关\n static const bool enableFirebaseAuth = true;\n static const bool enableAlipay = true;\n static const bool enableAppleIAP = true;\n static const bool enableVideoFeed = true;\n static bool get enableDebugMode => !isProduction;\n\n // 传输配置\n static const int fileChunkSize = 1024;\n static const int maxRetryCount = 3;\n static const int timeoutDuration = 5000;\n static const int maxConcurrentTransfers = 5;\n\n // 缓存配置\n static const int cacheMaxAge = 7;\n static const int maxCacheSize = 100;\n\n // 分页配置\n static const int defaultPageSize = 20;\n static const int maxPageSize = 100;\n\n // 超时配置\n static final Duration requestTimeout = const Duration(seconds: 30);\n static final Duration connectTimeout = const Duration(seconds: 10);\n\n // 重试配置\n static const int maxRetries = 3;\n static final Duration retryDelay = const Duration(seconds: 1);\n\n // 日志配置\n static const bool enableLogging = true;\n static bool get enableNetworkLogging => !isProduction;\n static bool get enablePerformanceLogging => !isProduction;\n\n // 存储键名\n static const String tokenStorageKey = 'auth_token';\n static const String userInfoStorageKey = 'user_info';\n static const String backendUrlStorageKey = 'backend_url';\n static const String testModeStorageKey = 'test_mode';\n\n // API端点\n static String get loginUrl => buildBackendUrl('/api/auth/login');\n static String get registerUrl => buildBackendUrl('/api/auth/register');\n static String get verifyUrl => buildBackendUrl('/api/auth/verify');\n static String get logoutUrl => buildBackendUrl('/api/auth/logout');\n static String get deleteAccountUrl => buildBackendUrl('/api/auth/delete');\n static String get sendVerificationCodeUrl =>\n buildBackendUrl('/api/auth/send-verification-code');\n static String get verifyCodeUrl => buildBackendUrl('/api/auth/verify-code');\n static String get forgotPasswordUrl =>\n buildBackendUrl('/api/auth/forgot-password');\n static String get resetPasswordUrl =>\n buildBackendUrl('/api/auth/reset-password');\n static String get userInfoUrl => buildBackendUrl('/api/auth/user-info');\n static String get bindEmailUrl => buildBackendUrl('/api/auth/bind-email');\n static String get appVersionPolicyUrl =>\n buildBackendUrl('/api/app/version-policy');\n\n static String get alipayCreateOrderUrl =>\n buildBackendUrl('/api/alipay/create-order');\n static String get alipayQueryOrderUrl =>\n buildBackendUrl('/api/alipay/query-order');\n static String get alipayMembershipStatusUrl =>\n buildBackendUrl('/api/alipay/check-membership');\n\n static String get stripeMembershipStatusUrl =>\n buildBackendUrl('/api/stripe/membership-status');\n static String get stripeCreateSubscriptionUrl =>\n buildBackendUrl('/api/stripe/create-subscription');\n static String get stripeSessionStatusUrl =>\n buildBackendUrl('/api/stripe/session-status');\n\n static String get appleVerifyReceiptUrl =>\n buildBackendUrl('/api/apple/verify-receipt');\n static String get purchaseEntitlementUrl =>\n buildBackendUrl('/api/purchases/entitlement');\n\n static const String zenBuddhaAssetProductId = 'zen_buddha_asset';\n static const String zenBuddhaAssetDisplayName = '禅室佛像素材';\n static const String zenBuddhaAssetPriceLabel = '¥33.00';\n static const String zenBuddhaAssetCardImagePath =\n 'assets/images/zen_buddha_material_card.png';\n\n static String get adminCheckStatusUrl =>\n buildBackendUrl('/api/admin/check-status');\n static String get adminCreateRedeemCodeUrl =>\n buildBackendUrl('/api/admin/create-redeem-code');\n static String get adminRedeemCodesUrl =>\n buildBackendUrl('/api/admin/redeem-codes');\n static String get adminUseRedeemCodeUrl =>\n buildBackendUrl('/api/admin/use-redeem-code');\n static String get adminPurchaseHistoryUrl =>\n buildBackendUrl('/api/admin/purchase-history');\n static String get adminRedeemHistoryUrl =>\n buildBackendUrl('/api/admin/redeem-history');\n\n static String get leaderboardUrl => buildBackendUrl('/api/leaderboard');\n static String get updateTransferDataUrl =>\n buildBackendUrl('/api/leaderboard/update');\n\n static String get healthCheckUrl => buildBackendUrl('/health');\n\n // 3D 佛像模型配置\n // 如果 R2 上需要切换到新的对象键,优先改这里,便于强制绕开旧缓存。\n static const String buddhaModelAssetPath = 'models/buddha_model.model';\n // Android/iOS both use the R2 flutter_scene .model; local cache misses\n // download through AssetLoaderService.\n\n // 当前线上正确模型明显大于 100MB,小于该阈值视为误传/降质文件。\n static const int minBuddhaModelSizeBytes = 100 * 1024 * 1024;\n\n // 请求头\n static Map<String, String> get defaultHeaders => {\n 'Content-Type': 'application/json',\n 'Accept': 'application/json',\n 'User-Agent': 'FabushiApp/${isWeb ? \"Web\" : \"Mobile\"}',\n };\n\n // 错误消息\n static const Map<String, String> errorMessages = {\n 'network_error': '网络连接失败,请检查网络设置',\n 'server_error': '服务器错误,请稍后重试',\n 'timeout_error': '请求超时,请检查网络连接',\n 'auth_error': '认证失败,请重新登录',\n 'permission_error': '权限不足',\n 'validation_error': '数据验证失败',\n 'unknown_error': '未知错误,请联系客服',\n };\n\n // 备用地址\n static List<String> get fallbackUrls {\n return [primaryBackendUrl];\n }\n\n // 调试配置\n static const bool enableApiLogging = true;\n static const bool debugMode = bool.fromEnvironment(\n 'DEBUG',\n defaultValue: false,\n );\n\n static void printConfigInfo() {\n if (kDebugMode) {\n print('=== 应用配置 ===');\n print('环境: ${isProduction ? \"生产\" : \"开发\"}');\n print('平台: ${isWeb ? \"Web\" : \"Native\"}');\n print('API URL: $currentBackendUrl');\n print('================');\n }\n }\n\n static void printCurrentConfig() => printConfigInfo();\n}\n"}, "7caac8da4c35fb690a8ea1317177a9f48001862b": {"t": 1785757968.6827798, "n": 8781, "h": "328ba02ba42f8fd8", "c": "import 'package:flutter/foundation.dart';\n\n/// 应用配置 - 统一管理所有配置项\nclass AppConfig {\n AppConfig._();\n\n static final AppConfig instance = AppConfig._();\n\n // 环境配置\n static const String environment = String.fromEnvironment(\n 'ENV',\n defaultValue: 'production',\n );\n\n static bool get isProduction {\n if (kIsWeb) {\n final currentUrl = Uri.base.toString();\n if (currentUrl.contains('fabushi-flutter-web-dev') ||\n currentUrl.contains('localhost')) {\n return false;\n }\n if (currentUrl.contains('fabushi-flutter-web-prod')) {\n return true;\n }\n }\n return environment == 'production';\n }\n\n static bool get isDevelopment => !isProduction;\n static bool get isStaging => environment == 'staging';\n static bool get isWeb => kIsWeb;\n\n // API配置\n static const String configuredApiBaseUrl = String.fromEnvironment(\n 'API_BASE_URL',\n defaultValue: '',\n );\n static const String primaryBackendUrl = 'https://api.ombhrum.com';\n static const String cloudflareWorkerProdUrl = 'https://api.ombhrum.com';\n static const String cloudflareWorkerDevUrl =\n 'https://fabushi-flutter-web-dev.bhrumom.workers.dev';\n static const String localDevUrl = 'http://localhost:8787';\n static const String publicWebUrl = 'https://flutter.ombhrum.com';\n\n static String get currentBackendUrl {\n if (configuredApiBaseUrl.isNotEmpty) {\n return configuredApiBaseUrl;\n }\n if (kIsWeb) {\n final currentUrl = Uri.base;\n final host = currentUrl.host;\n if (host.contains('localhost')) {\n return localDevUrl;\n }\n if (host.contains('fabushi-flutter-web-dev')) {\n return cloudflareWorkerDevUrl;\n }\n if (host.contains('fabushi-flutter-web-prod')) {\n return cloudflareWorkerProdUrl;\n }\n }\n return primaryBackendUrl;\n }\n\n static String get apiUrl => currentBackendUrl;\n\n /// 大厂式 API 网关入口:客户端只允许构造自家后端的相对路径。\n ///\n /// 第三方 API、上游 API、缓存、重试、fallback 都应在后端处理;\n /// App 端不得直接传入 https://... 或 http://... 这类绝对 URL。\n static Uri buildBackendUri(\n String endpoint, {\n Map<String, String>? queryParameters,\n }) {\n final normalizedEndpoint = endpoint.trim();\n if (normalizedEndpoint.isEmpty || !normalizedEndpoint.startsWith('/')) {\n throw ArgumentError(\n 'API endpoint must be a first-party relative path starting with /: $endpoint',\n );\n }\n if (normalizedEndpoint.startsWith('//') ||\n normalizedEndpoint.contains('://')) {\n throw ArgumentError(\n 'API endpoint must not be an absolute or protocol-relative URL: $endpoint',\n );\n }\n\n final baseUrl = currentBackendUrl.replaceFirst(RegExp(r'/+$'), '');\n final uri = Uri.parse('$baseUrl$normalizedEndpoint');\n if (queryParameters == null || queryParameters.isEmpty) {\n return uri;\n }\n return uri.replace(queryParameters: queryParameters);\n }\n\n static String buildBackendUrl(\n String endpoint, {\n Map<String, String>? queryParameters,\n }) {\n return buildBackendUri(\n endpoint,\n queryParameters: queryParameters,\n ).toString();\n }\n\n // 应用信息\n static const String appName = '大乘';\n // 保持与 pubspec.yaml 中的 version 同步,供 Web 回退和服务端比对使用。\n static const String appVersion = '1.0.0';\n static const int appBuildNumber = 400;\n\n // 功能开关\n static const bool enableFirebaseAuth = true;\n static const bool enableAlipay = true;\n static const bool enableAppleIAP = true;\n static const bool enableVideoFeed = true;\n static bool get enableDebugMode => !isProduction;\n\n // 传输配置\n static const int fileChunkSize = 1024;\n static const int maxRetryCount = 3;\n static const int timeoutDuration = 5000;\n static const int maxConcurrentTransfers = 5;\n\n // 缓存配置\n static const int cacheMaxAge = 7;\n static const int maxCacheSize = 100;\n\n // 分页配置\n static const int defaultPageSize = 20;\n static const int maxPageSize = 100;\n\n // 超时配置\n static final Duration requestTimeout = const Duration(seconds: 30);\n static final Duration connectTimeout = const Duration(seconds: 10);\n\n // 重试配置\n static const int maxRetries = 3;\n static final Duration retryDelay = const Duration(seconds: 1);\n\n // 日志配置\n static const bool enableLogging = true;\n static bool get enableNetworkLogging => !isProduction;\n static bool get enablePerformanceLogging => !isProduction;\n\n // 存储键名\n static const String tokenStorageKey = 'auth_token';\n static const String userInfoStorageKey = 'user_info';\n static const String backendUrlStorageKey = 'backend_url';\n static const String testModeStorageKey = 'test_mode';\n\n // API端点\n static String get loginUrl => buildBackendUrl('/api/auth/login');\n static String get registerUrl => buildBackendUrl('/api/auth/register');\n static String get verifyUrl => buildBackendUrl('/api/auth/verify');\n static String get logoutUrl => buildBackendUrl('/api/auth/logout');\n static String get deleteAccountUrl => buildBackendUrl('/api/auth/delete');\n static String get sendVerificationCodeUrl =>\n buildBackendUrl('/api/auth/send-verification-code');\n static String get verifyCodeUrl => buildBackendUrl('/api/auth/verify-code');\n static String get forgotPasswordUrl =>\n buildBackendUrl('/api/auth/forgot-password');\n static String get resetPasswordUrl =>\n buildBackendUrl('/api/auth/reset-password');\n static String get userInfoUrl => buildBackendUrl('/api/auth/user-info');\n static String get bindEmailUrl => buildBackendUrl('/api/auth/bind-email');\n static String get appVersionPolicyUrl =>\n buildBackendUrl('/api/app/version-policy');\n\n static String get alipayCreateOrderUrl =>\n buildBackendUrl('/api/alipay/create-order');\n static String get alipayQueryOrderUrl =>\n buildBackendUrl('/api/alipay/query-order');\n static String get alipayMembershipStatusUrl =>\n buildBackendUrl('/api/alipay/check-membership');\n\n static String get stripeMembershipStatusUrl =>\n buildBackendUrl('/api/stripe/membership-status');\n static String get stripeCreateSubscriptionUrl =>\n buildBackendUrl('/api/stripe/create-subscription');\n static String get stripeSessionStatusUrl =>\n buildBackendUrl('/api/stripe/session-status');\n\n static String get appleVerifyReceiptUrl =>\n buildBackendUrl('/api/apple/verify-receipt');\n static String get purchaseEntitlementUrl =>\n buildBackendUrl('/api/purchases/entitlement');\n\n static const String zenBuddhaAssetProductId = 'zen_buddha_asset';\n static const String zenBuddhaAssetDisplayName = '禅室佛像素材';\n static const String zenBuddhaAssetPriceLabel = '¥33.00';\n\n static String get adminCheckStatusUrl =>\n buildBackendUrl('/api/admin/check-status');\n static String get adminCreateRedeemCodeUrl =>\n buildBackendUrl('/api/admin/create-redeem-code');\n static String get adminRedeemCodesUrl =>\n buildBackendUrl('/api/admin/redeem-codes');\n static String get adminUseRedeemCodeUrl =>\n buildBackendUrl('/api/admin/use-redeem-code');\n static String get adminPurchaseHistoryUrl =>\n buildBackendUrl('/api/admin/purchase-history');\n static String get adminRedeemHistoryUrl =>\n buildBackendUrl('/api/admin/redeem-history');\n\n static String get leaderboardUrl => buildBackendUrl('/api/leaderboard');\n static String get updateTransferDataUrl =>\n buildBackendUrl('/api/leaderboard/update');\n\n static String get healthCheckUrl => buildBackendUrl('/health');\n\n // 3D 佛像模型配置\n // 如果 R2 上需要切换到新的对象键,优先改这里,便于强制绕开旧缓存。\n static const String buddhaModelAssetPath = 'models/buddha_model.model';\n // Android/iOS both use the R2 flutter_scene .model; local cache misses\n // download through AssetLoaderService.\n\n // 当前线上正确模型明显大于 100MB,小于该阈值视为误传/降质文件。\n static const int minBuddhaModelSizeBytes = 100 * 1024 * 1024;\n\n // 请求头\n static Map<String, String> get defaultHeaders => {\n 'Content-Type': 'application/json',\n 'Accept': 'application/json',\n 'User-Agent': 'FabushiApp/${isWeb ? \"Web\" : \"Mobile\"}',\n };\n\n // 错误消息\n static const Map<String, String> errorMessages = {\n 'network_error': '网络连接失败,请检查网络设置',\n 'server_error': '服务器错误,请稍后重试',\n 'timeout_error': '请求超时,请检查网络连接',\n 'auth_error': '认证失败,请重新登录',\n 'permission_error': '权限不足',\n 'validation_error': '数据验证失败',\n 'unknown_error': '未知错误,请联系客服',\n };\n\n // 备用地址\n static List<String> get fallbackUrls {\n return [primaryBackendUrl];\n }\n\n // 调试配置\n static const bool enableApiLogging = true;\n static const bool debugMode = bool.fromEnvironment(\n 'DEBUG',\n defaultValue: false,\n );\n\n static void printConfigInfo() {\n if (kDebugMode) {\n print('=== 应用配置 ===');\n print('环境: ${isProduction ? \"生产\" : \"开发\"}');\n print('平台: ${isWeb ? \"Web\" : \"Native\"}');\n print('API URL: $currentBackendUrl');\n print('================');\n }\n }\n\n static void printCurrentConfig() => printConfigInfo();\n}\n"}, "13ff72664380a90100529faae37d769c8f62b9ec": {"t": 1785757969.9416692, "n": 8493, "h": "727192b134553daa", "c": "import 'package:flutter/foundation.dart';\n\n/// 应用配置 - 统一管理所有配置项\nclass AppConfig {\n AppConfig._();\n\n static final AppConfig instance = AppConfig._();\n\n // 环境配置\n static const String environment = String.fromEnvironment(\n 'ENV',\n defaultValue: 'production',\n );\n\n static bool get isProduction {\n if (kIsWeb) {\n final currentUrl = Uri.base.toString();\n if (currentUrl.contains('fabushi-flutter-web-dev') ||\n currentUrl.contains('localhost')) {\n return false;\n }\n if (currentUrl.contains('fabushi-flutter-web-prod')) {\n return true;\n }\n }\n return environment == 'production';\n }\n\n static bool get isDevelopment => !isProduction;\n static bool get isStaging => environment == 'staging';\n static bool get isWeb => kIsWeb;\n\n // API配置\n static const String configuredApiBaseUrl = String.fromEnvironment(\n 'API_BASE_URL',\n defaultValue: '',\n );\n static const String primaryBackendUrl = 'https://api.ombhrum.com';\n static const String cloudflareWorkerProdUrl = 'https://api.ombhrum.com';\n static const String cloudflareWorkerDevUrl =\n 'https://fabushi-flutter-web-dev.bhrumom.workers.dev';\n static const String localDevUrl = 'http://localhost:8787';\n static const String publicWebUrl = 'https://flutter.ombhrum.com';\n\n static String get currentBackendUrl {\n if (configuredApiBaseUrl.isNotEmpty) {\n return configuredApiBaseUrl;\n }\n if (kIsWeb) {\n final currentUrl = Uri.base;\n final host = currentUrl.host;\n if (host.contains('localhost')) {\n return localDevUrl;\n }\n if (host.contains('fabushi-flutter-web-dev')) {\n return cloudflareWorkerDevUrl;\n }\n if (host.contains('fabushi-flutter-web-prod')) {\n return cloudflareWorkerProdUrl;\n }\n }\n return primaryBackendUrl;\n }\n\n static String get apiUrl => currentBackendUrl;\n\n /// 大厂式 API 网关入口:客户端只允许构造自家后端的相对路径。\n ///\n /// 第三方 API、上游 API、缓存、重试、fallback 都应在后端处理;\n /// App 端不得直接传入 https://... 或 http://... 这类绝对 URL。\n static Uri buildBackendUri(\n String endpoint, {\n Map<String, String>? queryParameters,\n }) {\n final normalizedEndpoint = endpoint.trim();\n if (normalizedEndpoint.isEmpty || !normalizedEndpoint.startsWith('/')) {\n throw ArgumentError(\n 'API endpoint must be a first-party relative path starting with /: $endpoint',\n );\n }\n if (normalizedEndpoint.startsWith('//') ||\n normalizedEndpoint.contains('://')) {\n throw ArgumentError(\n 'API endpoint must not be an absolute or protocol-relative URL: $endpoint',\n );\n }\n\n final baseUrl = currentBackendUrl.replaceFirst(RegExp(r'/+$'), '');\n final uri = Uri.parse('$baseUrl$normalizedEndpoint');\n if (queryParameters == null || queryParameters.isEmpty) {\n return uri;\n }\n return uri.replace(queryParameters: queryParameters);\n }\n\n static String buildBackendUrl(\n String endpoint, {\n Map<String, String>? queryParameters,\n }) {\n return buildBackendUri(\n endpoint,\n queryParameters: queryParameters,\n ).toString();\n }\n\n // 应用信息\n static const String appName = '大乘';\n // 保持与 pubspec.yaml 中的 version 同步,供 Web 回退和服务端比对使用。\n static const String appVersion = '1.0.0';\n static const int appBuildNumber = 16;\n\n // 功能开关\n static const bool enableFirebaseAuth = true;\n static const bool enableAlipay = true;\n static const bool enableAppleIAP = true;\n static const bool enableVideoFeed = true;\n static bool get enableDebugMode => !isProduction;\n\n // 传输配置\n static const int fileChunkSize = 1024;\n static const int maxRetryCount = 3;\n static const int timeoutDuration = 5000;\n static const int maxConcurrentTransfers = 5;\n\n // 缓存配置\n static const int cacheMaxAge = 7;\n static const int maxCacheSize = 100;\n\n // 分页配置\n static const int defaultPageSize = 20;\n static const int maxPageSize = 100;\n\n // 超时配置\n static final Duration requestTimeout = const Duration(seconds: 30);\n static final Duration connectTimeout = const Duration(seconds: 10);\n\n // 重试配置\n static const int maxRetries = 3;\n static final Duration retryDelay = const Duration(seconds: 1);\n\n // 日志配置\n static const bool enableLogging = true;\n static bool get enableNetworkLogging => !isProduction;\n static bool get enablePerformanceLogging => !isProduction;\n\n // 存储键名\n static const String tokenStorageKey = 'auth_token';\n static const String userInfoStorageKey = 'user_info';\n static const String backendUrlStorageKey = 'backend_url';\n static const String testModeStorageKey = 'test_mode';\n\n // API端点\n static String get loginUrl => buildBackendUrl('/api/auth/login');\n static String get registerUrl => buildBackendUrl('/api/auth/register');\n static String get verifyUrl => buildBackendUrl('/api/auth/verify');\n static String get logoutUrl => buildBackendUrl('/api/auth/logout');\n static String get deleteAccountUrl => buildBackendUrl('/api/auth/delete');\n static String get sendVerificationCodeUrl =>\n buildBackendUrl('/api/auth/send-verification-code');\n static String get verifyCodeUrl => buildBackendUrl('/api/auth/verify-code');\n static String get forgotPasswordUrl =>\n buildBackendUrl('/api/auth/forgot-password');\n static String get resetPasswordUrl =>\n buildBackendUrl('/api/auth/reset-password');\n static String get userInfoUrl => buildBackendUrl('/api/auth/user-info');\n static String get bindEmailUrl => buildBackendUrl('/api/auth/bind-email');\n static String get appVersionPolicyUrl =>\n buildBackendUrl('/api/app/version-policy');\n\n static String get alipayCreateOrderUrl =>\n buildBackendUrl('/api/alipay/create-order');\n static String get alipayQueryOrderUrl =>\n buildBackendUrl('/api/alipay/query-order');\n static String get alipayMembershipStatusUrl =>\n buildBackendUrl('/api/alipay/check-membership');\n\n static String get stripeMembershipStatusUrl =>\n buildBackendUrl('/api/stripe/membership-status');\n static String get stripeCreateSubscriptionUrl =>\n buildBackendUrl('/api/stripe/create-subscription');\n static String get stripeSessionStatusUrl =>\n buildBackendUrl('/api/stripe/session-status');\n\n static String get appleVerifyReceiptUrl =>\n buildBackendUrl('/api/apple/verify-receipt');\n\n static String get adminCheckStatusUrl =>\n buildBackendUrl('/api/admin/check-status');\n static String get adminCreateRedeemCodeUrl =>\n buildBackendUrl('/api/admin/create-redeem-code');\n static String get adminRedeemCodesUrl =>\n buildBackendUrl('/api/admin/redeem-codes');\n static String get adminUseRedeemCodeUrl =>\n buildBackendUrl('/api/admin/use-redeem-code');\n static String get adminPurchaseHistoryUrl =>\n buildBackendUrl('/api/admin/purchase-history');\n static String get adminRedeemHistoryUrl =>\n buildBackendUrl('/api/admin/redeem-history');\n\n static String get leaderboardUrl => buildBackendUrl('/api/leaderboard');\n static String get updateTransferDataUrl =>\n buildBackendUrl('/api/leaderboard/update');\n\n static String get healthCheckUrl => buildBackendUrl('/health');\n\n // 3D 佛像模型配置\n // 如果 R2 上需要切换到新的对象键,优先改这里,便于强制绕开旧缓存。\n static const String buddhaModelAssetPath = 'models/buddha_model.model';\n // Android/iOS both use the R2 flutter_scene .model; local cache misses\n // download through AssetLoaderService.\n\n // 当前线上正确模型明显大于 100MB,小于该阈值视为误传/降质文件。\n static const int minBuddhaModelSizeBytes = 100 * 1024 * 1024;\n\n // 请求头\n static Map<String, String> get defaultHeaders => {\n 'Content-Type': 'application/json',\n 'Accept': 'application/json',\n 'User-Agent': 'FabushiApp/${isWeb ? \"Web\" : \"Mobile\"}',\n };\n\n // 错误消息\n static const Map<String, String> errorMessages = {\n 'network_error': '网络连接失败,请检查网络设置',\n 'server_error': '服务器错误,请稍后重试',\n 'timeout_error': '请求超时,请检查网络连接',\n 'auth_error': '认证失败,请重新登录',\n 'permission_error': '权限不足',\n 'validation_error': '数据验证失败',\n 'unknown_error': '未知错误,请联系客服',\n };\n\n // 备用地址\n static List<String> get fallbackUrls {\n return [primaryBackendUrl];\n }\n\n // 调试配置\n static const bool enableApiLogging = true;\n static const bool debugMode = bool.fromEnvironment(\n 'DEBUG',\n defaultValue: false,\n );\n\n static void printConfigInfo() {\n if (kDebugMode) {\n print('=== 应用配置 ===');\n print('环境: ${isProduction ? \"生产\" : \"开发\"}');\n print('平台: ${isWeb ? \"Web\" : \"Native\"}');\n print('API URL: $currentBackendUrl');\n print('================');\n }\n }\n\n static void printCurrentConfig() => printConfigInfo();\n}\n"}, "df5f5d7d1570a12487ec873b93fd92a07b905b89": {"t": 1785757972.1275828, "n": 8603, "h": "84c7caeed47f7d0b", "c": "import 'package:flutter/foundation.dart';\n\n/// 应用配置 - 统一管理所有配置项\nclass AppConfig {\n AppConfig._();\n\n static final AppConfig instance = AppConfig._();\n\n // 环境配置\n static const String environment = String.fromEnvironment(\n 'ENV',\n defaultValue: 'production',\n );\n\n static bool get isProduction {\n if (kIsWeb) {\n final currentUrl = Uri.base.toString();\n if (currentUrl.contains('fabushi-flutter-web-dev') ||\n currentUrl.contains('localhost')) {\n return false;\n }\n if (currentUrl.contains('fabushi-flutter-web-prod')) {\n return true;\n }\n }\n return environment == 'production';\n }\n\n static bool get isDevelopment => !isProduction;\n static bool get isStaging => environment == 'staging';\n static bool get isWeb => kIsWeb;\n\n // API配置\n static const String configuredApiBaseUrl = String.fromEnvironment(\n 'API_BASE_URL',\n defaultValue: '',\n );\n static const String primaryBackendUrl = 'https://api.ombhrum.com';\n static const String cloudflareWorkerProdUrl = 'https://api.ombhrum.com';\n static const String cloudflareWorkerDevUrl =\n 'https://fabushi-flutter-web-dev.bhrumom.workers.dev';\n static const String localDevUrl = 'http://localhost:8787';\n static const String publicWebUrl = 'https://flutter.ombhrum.com';\n\n static String get currentBackendUrl {\n if (configuredApiBaseUrl.isNotEmpty) {\n return configuredApiBaseUrl;\n }\n if (kIsWeb) {\n final currentUrl = Uri.base;\n final host = currentUrl.host;\n if (host.contains('localhost')) {\n return localDevUrl;\n }\n if (host.contains('fabushi-flutter-web-dev')) {\n return cloudflareWorkerDevUrl;\n }\n if (host.contains('fabushi-flutter-web-prod')) {\n return cloudflareWorkerProdUrl;\n }\n }\n return primaryBackendUrl;\n }\n\n static String get apiUrl => currentBackendUrl;\n\n /// 大厂式 API 网关入口:客户端只允许构造自家后端的相对路径。\n ///\n /// 第三方 API、上游 API、缓存、重试、fallback 都应在后端处理;\n /// App 端不得直接传入 https://... 或 http://... 这类绝对 URL。\n static Uri buildBackendUri(\n String endpoint, {\n Map<String, String>? queryParameters,\n }) {\n final normalizedEndpoint = endpoint.trim();\n if (normalizedEndpoint.isEmpty || !normalizedEndpoint.startsWith('/')) {\n throw ArgumentError(\n 'API endpoint must be a first-party relative path starting with /: $endpoint',\n );\n }\n if (normalizedEndpoint.startsWith('//') ||\n normalizedEndpoint.contains('://')) {\n throw ArgumentError(\n 'API endpoint must not be an absolute or protocol-relative URL: $endpoint',\n );\n }\n\n final baseUrl = currentBackendUrl.replaceFirst(RegExp(r'/+$'), '');\n final uri = Uri.parse('$baseUrl$normalizedEndpoint');\n if (queryParameters == null || queryParameters.isEmpty) {\n return uri;\n }\n return uri.replace(queryParameters: queryParameters);\n }\n\n static String buildBackendUrl(\n String endpoint, {\n Map<String, String>? queryParameters,\n }) {\n return buildBackendUri(\n endpoint,\n queryParameters: queryParameters,\n ).toString();\n }\n\n // 应用信息\n static const String appName = '大乘';\n // 保持与 pubspec.yaml 中的 version 同步,供 Web 回退和服务端比对使用。\n static const String appVersion = '1.0.0';\n static const int appBuildNumber = 16;\n\n // 功能开关\n static const bool enableFirebaseAuth = true;\n static const bool enableAlipay = true;\n static const bool enableAppleIAP = true;\n static const bool enableVideoFeed = true;\n static bool get enableDebugMode => !isProduction;\n\n // 传输配置\n static const int fileChunkSize = 1024;\n static const int maxRetryCount = 3;\n static const int timeoutDuration = 5000;\n static const int maxConcurrentTransfers = 5;\n\n // 缓存配置\n static const int cacheMaxAge = 7;\n static const int maxCacheSize = 100;\n\n // 分页配置\n static const int defaultPageSize = 20;\n static const int maxPageSize = 100;\n\n // 超时配置\n static final Duration requestTimeout = const Duration(seconds: 30);\n static final Duration connectTimeout = const Duration(seconds: 10);\n\n // 重试配置\n static const int maxRetries = 3;\n static final Duration retryDelay = const Duration(seconds: 1);\n\n // 日志配置\n static const bool enableLogging = true;\n static bool get enableNetworkLogging => !isProduction;\n static bool get enablePerformanceLogging => !isProduction;\n\n // 存储键名\n static const String tokenStorageKey = 'auth_token';\n static const String userInfoStorageKey = 'user_info';\n static const String backendUrlStorageKey = 'backend_url';\n static const String testModeStorageKey = 'test_mode';\n\n // API端点\n static String get loginUrl => buildBackendUrl('/api/auth/login');\n static String get registerUrl => buildBackendUrl('/api/auth/register');\n static String get verifyUrl => buildBackendUrl('/api/auth/verify');\n static String get logoutUrl => buildBackendUrl('/api/auth/logout');\n static String get deleteAccountUrl => buildBackendUrl('/api/auth/delete');\n static String get sendVerificationCodeUrl =>\n buildBackendUrl('/api/auth/send-verification-code');\n static String get verifyCodeUrl => buildBackendUrl('/api/auth/verify-code');\n static String get forgotPasswordUrl =>\n buildBackendUrl('/api/auth/forgot-password');\n static String get resetPasswordUrl =>\n buildBackendUrl('/api/auth/reset-password');\n static String get userInfoUrl => buildBackendUrl('/api/auth/user-info');\n static String get bindEmailUrl => buildBackendUrl('/api/auth/bind-email');\n static String get appVersionPolicyUrl =>\n buildBackendUrl('/api/app/version-policy');\n\n static String get alipayCreateOrderUrl =>\n buildBackendUrl('/api/alipay/create-order');\n static String get alipayQueryOrderUrl =>\n buildBackendUrl('/api/alipay/query-order');\n static String get alipayMembershipStatusUrl =>\n buildBackendUrl('/api/alipay/check-membership');\n\n static String get stripeMembershipStatusUrl =>\n buildBackendUrl('/api/stripe/membership-status');\n static String get stripeCreateSubscriptionUrl =>\n buildBackendUrl('/api/stripe/create-subscription');\n static String get stripeSessionStatusUrl =>\n buildBackendUrl('/api/stripe/session-status');\n\n static String get appleVerifyReceiptUrl =>\n buildBackendUrl('/api/apple/verify-receipt');\n\n static String get adminCheckStatusUrl =>\n buildBackendUrl('/api/admin/check-status');\n static String get adminCreateRedeemCodeUrl =>\n buildBackendUrl('/api/admin/create-redeem-code');\n static String get adminRedeemCodesUrl =>\n buildBackendUrl('/api/admin/redeem-codes');\n static String get adminUseRedeemCodeUrl =>\n buildBackendUrl('/api/admin/use-redeem-code');\n static String get adminPurchaseHistoryUrl =>\n buildBackendUrl('/api/admin/purchase-history');\n static String get adminRedeemHistoryUrl =>\n buildBackendUrl('/api/admin/redeem-history');\n\n static String get leaderboardUrl => buildBackendUrl('/api/leaderboard');\n static String get updateTransferDataUrl =>\n buildBackendUrl('/api/leaderboard/update');\n\n static String get healthCheckUrl => buildBackendUrl('/health');\n\n // iOS 后台保活静音音频\n static String get silenceAudioUrl =>\n buildBackendUrl('/static/audio/silence.mp3');\n\n // 3D 佛像模型配置\n // 如果 R2 上需要切换到新的对象键,优先改这里,便于强制绕开旧缓存。\n static const String buddhaModelAssetPath = 'models/buddha_model.model';\n // Android/iOS both use the R2 flutter_scene .model; local cache misses\n // download through AssetLoaderService.\n\n // 当前线上正确模型明显大于 100MB,小于该阈值视为误传/降质文件。\n static const int minBuddhaModelSizeBytes = 100 * 1024 * 1024;\n\n // 请求头\n static Map<String, String> get defaultHeaders => {\n 'Content-Type': 'application/json',\n 'Accept': 'application/json',\n 'User-Agent': 'FabushiApp/${isWeb ? \"Web\" : \"Mobile\"}',\n };\n\n // 错误消息\n static const Map<String, String> errorMessages = {\n 'network_error': '网络连接失败,请检查网络设置',\n 'server_error': '服务器错误,请稍后重试',\n 'timeout_error': '请求超时,请检查网络连接',\n 'auth_error': '认证失败,请重新登录',\n 'permission_error': '权限不足',\n 'validation_error': '数据验证失败',\n 'unknown_error': '未知错误,请联系客服',\n };\n\n // 备用地址\n static List<String> get fallbackUrls {\n return [primaryBackendUrl];\n }\n\n // 调试配置\n static const bool enableApiLogging = true;\n static const bool debugMode = bool.fromEnvironment(\n 'DEBUG',\n defaultValue: false,\n );\n\n static void printConfigInfo() {\n if (kDebugMode) {\n print('=== 应用配置 ===');\n print('环境: ${isProduction ? \"生产\" : \"开发\"}');\n print('平台: ${isWeb ? \"Web\" : \"Native\"}');\n print('API URL: $currentBackendUrl');\n print('================');\n }\n }\n\n static void printCurrentConfig() => printConfigInfo();\n}\n"}, "530f5ea77ad8dd243179d8d369dd006bc5e3a46e": {"t": 1785757973.47409, "n": 8676, "h": "6b377d78b3f02ee4", "c": "import 'package:flutter/foundation.dart';\n\n/// 应用配置 - 统一管理所有配置项\nclass AppConfig {\n AppConfig._();\n\n static final AppConfig instance = AppConfig._();\n\n // 环境配置\n static const String environment = String.fromEnvironment(\n 'ENV',\n defaultValue: 'production',\n );\n\n static bool get isProduction {\n if (kIsWeb) {\n final currentUrl = Uri.base.toString();\n if (currentUrl.contains('fabushi-flutter-web-dev') ||\n currentUrl.contains('localhost')) {\n return false;\n }\n if (currentUrl.contains('fabushi-flutter-web-prod')) {\n return true;\n }\n }\n return environment == 'production';\n }\n\n static bool get isDevelopment => !isProduction;\n static bool get isStaging => environment == 'staging';\n static bool get isWeb => kIsWeb;\n\n // API配置\n static const String configuredApiBaseUrl = String.fromEnvironment(\n 'API_BASE_URL',\n defaultValue: '',\n );\n static const String primaryBackendUrl = 'https://api.ombhrum.com';\n static const String cloudflareWorkerProdUrl = 'https://api.ombhrum.com';\n static const String cloudflareWorkerDevUrl =\n 'https://fabushi-flutter-web-dev.bhrumom.workers.dev';\n static const String localDevUrl = 'http://localhost:8787';\n static const String publicWebUrl = 'https://flutter.ombhrum.com';\n\n static String get currentBackendUrl {\n if (configuredApiBaseUrl.isNotEmpty) {\n return configuredApiBaseUrl;\n }\n if (kIsWeb) {\n final currentUrl = Uri.base;\n final host = currentUrl.host;\n if (host.contains('localhost')) {\n return localDevUrl;\n }\n if (host.contains('fabushi-flutter-web-dev')) {\n return cloudflareWorkerDevUrl;\n }\n if (host.contains('fabushi-flutter-web-prod')) {\n return cloudflareWorkerProdUrl;\n }\n }\n return primaryBackendUrl;\n }\n\n static String get apiUrl => currentBackendUrl;\n\n /// 大厂式 API 网关入口:客户端只允许构造自家后端的相对路径。\n ///\n /// 第三方 API、上游 API、缓存、重试、fallback 都应在后端处理;\n /// App 端不得直接传入 https://... 或 http://... 这类绝对 URL。\n static Uri buildBackendUri(\n String endpoint, {\n Map<String, String>? queryParameters,\n }) {\n final normalizedEndpoint = endpoint.trim();\n if (normalizedEndpoint.isEmpty || !normalizedEndpoint.startsWith('/')) {\n throw ArgumentError(\n 'API endpoint must be a first-party relative path starting with /: $endpoint',\n );\n }\n if (normalizedEndpoint.startsWith('//') ||\n normalizedEndpoint.contains('://')) {\n throw ArgumentError(\n 'API endpoint must not be an absolute or protocol-relative URL: $endpoint',\n );\n }\n\n final baseUrl = currentBackendUrl.replaceFirst(RegExp(r'/+$'), '');\n final uri = Uri.parse('$baseUrl$normalizedEndpoint');\n if (queryParameters == null || queryParameters.isEmpty) {\n return uri;\n }\n return uri.replace(queryParameters: queryParameters);\n }\n\n static String buildBackendUrl(\n String endpoint, {\n Map<String, String>? queryParameters,\n }) {\n return buildBackendUri(\n endpoint,\n queryParameters: queryParameters,\n ).toString();\n }\n\n // 应用信息\n static const String appName = '大乘';\n // 保持与 pubspec.yaml 中的 version 同步,供 Web 回退和服务端比对使用。\n static const String appVersion = '1.0.0';\n static const int appBuildNumber = 16;\n\n // 功能开关\n static const bool enableFirebaseAuth = true;\n static const bool enableAlipay = true;\n static const bool enableAppleIAP = true;\n static const bool enableVideoFeed = true;\n static bool get enableDebugMode => !isProduction;\n\n // 传输配置\n static const int fileChunkSize = 1024;\n static const int maxRetryCount = 3;\n static const int timeoutDuration = 5000;\n static const int maxConcurrentTransfers = 5;\n\n // 缓存配置\n static const int cacheMaxAge = 7;\n static const int maxCacheSize = 100;\n\n // 分页配置\n static const int defaultPageSize = 20;\n static const int maxPageSize = 100;\n\n // 超时配置\n static final Duration requestTimeout = const Duration(seconds: 30);\n static final Duration connectTimeout = const Duration(seconds: 10);\n\n // 重试配置\n static const int maxRetries = 3;\n static final Duration retryDelay = const Duration(seconds: 1);\n\n // 日志配置\n static const bool enableLogging = true;\n static bool get enableNetworkLogging => !isProduction;\n static bool get enablePerformanceLogging => !isProduction;\n\n // 存储键名\n static const String tokenStorageKey = 'auth_token';\n static const String userInfoStorageKey = 'user_info';\n static const String backendUrlStorageKey = 'backend_url';\n static const String testModeStorageKey = 'test_mode';\n\n // API端点\n static const String cbetaSendTextsEndpoint = '/api/cbeta/send-texts';\n\n static String get loginUrl => buildBackendUrl('/api/auth/login');\n static String get registerUrl => buildBackendUrl('/api/auth/register');\n static String get verifyUrl => buildBackendUrl('/api/auth/verify');\n static String get logoutUrl => buildBackendUrl('/api/auth/logout');\n static String get deleteAccountUrl => buildBackendUrl('/api/auth/delete');\n static String get sendVerificationCodeUrl =>\n buildBackendUrl('/api/auth/send-verification-code');\n static String get verifyCodeUrl => buildBackendUrl('/api/auth/verify-code');\n static String get forgotPasswordUrl =>\n buildBackendUrl('/api/auth/forgot-password');\n static String get resetPasswordUrl =>\n buildBackendUrl('/api/auth/reset-password');\n static String get userInfoUrl => buildBackendUrl('/api/auth/user-info');\n static String get bindEmailUrl => buildBackendUrl('/api/auth/bind-email');\n static String get appVersionPolicyUrl =>\n buildBackendUrl('/api/app/version-policy');\n\n static String get alipayCreateOrderUrl =>\n buildBackendUrl('/api/alipay/create-order');\n static String get alipayQueryOrderUrl =>\n buildBackendUrl('/api/alipay/query-order');\n static String get alipayMembershipStatusUrl =>\n buildBackendUrl('/api/alipay/check-membership');\n\n static String get stripeMembershipStatusUrl =>\n buildBackendUrl('/api/stripe/membership-status');\n static String get stripeCreateSubscriptionUrl =>\n buildBackendUrl('/api/stripe/create-subscription');\n static String get stripeSessionStatusUrl =>\n buildBackendUrl('/api/stripe/session-status');\n\n static String get appleVerifyReceiptUrl =>\n buildBackendUrl('/api/apple/verify-receipt');\n\n static String get adminCheckStatusUrl =>\n buildBackendUrl('/api/admin/check-status');\n static String get adminCreateRedeemCodeUrl =>\n buildBackendUrl('/api/admin/create-redeem-code');\n static String get adminRedeemCodesUrl =>\n buildBackendUrl('/api/admin/redeem-codes');\n static String get adminUseRedeemCodeUrl =>\n buildBackendUrl('/api/admin/use-redeem-code');\n static String get adminPurchaseHistoryUrl =>\n buildBackendUrl('/api/admin/purchase-history');\n static String get adminRedeemHistoryUrl =>\n buildBackendUrl('/api/admin/redeem-history');\n\n static String get leaderboardUrl => buildBackendUrl('/api/leaderboard');\n static String get updateTransferDataUrl =>\n buildBackendUrl('/api/leaderboard/update');\n\n static String get healthCheckUrl => buildBackendUrl('/health');\n\n // iOS 后台保活静音音频\n static String get silenceAudioUrl =>\n buildBackendUrl('/static/audio/silence.mp3');\n\n // 3D 佛像模型配置\n // 如果 R2 上需要切换到新的对象键,优先改这里,便于强制绕开旧缓存。\n static const String buddhaModelAssetPath = 'models/buddha_model.model';\n // Android/iOS both use the R2 flutter_scene .model; local cache misses\n // download through AssetLoaderService.\n\n // 当前线上正确模型明显大于 100MB,小于该阈值视为误传/降质文件。\n static const int minBuddhaModelSizeBytes = 100 * 1024 * 1024;\n\n // 请求头\n static Map<String, String> get defaultHeaders => {\n 'Content-Type': 'application/json',\n 'Accept': 'application/json',\n 'User-Agent': 'FabushiApp/${isWeb ? \"Web\" : \"Mobile\"}',\n };\n\n // 错误消息\n static const Map<String, String> errorMessages = {\n 'network_error': '网络连接失败,请检查网络设置',\n 'server_error': '服务器错误,请稍后重试',\n 'timeout_error': '请求超时,请检查网络连接',\n 'auth_error': '认证失败,请重新登录',\n 'permission_error': '权限不足',\n 'validation_error': '数据验证失败',\n 'unknown_error': '未知错误,请联系客服',\n };\n\n // 备用地址\n static List<String> get fallbackUrls {\n return [primaryBackendUrl];\n }\n\n // 调试配置\n static const bool enableApiLogging = true;\n static const bool debugMode = bool.fromEnvironment(\n 'DEBUG',\n defaultValue: false,\n );\n\n static void printConfigInfo() {\n if (kDebugMode) {\n print('=== 应用配置 ===');\n print('环境: ${isProduction ? \"生产\" : \"开发\"}');\n print('平台: ${isWeb ? \"Web\" : \"Native\"}');\n print('API URL: $currentBackendUrl');\n print('================');\n }\n }\n\n static void printCurrentConfig() => printConfigInfo();\n}\n"}, "003d24cfcae5c66034c330696a0ce185bf62f261": {"t": 1785757974.92226, "n": 9063, "h": "c648a76639bfdfe9", "c": "import 'package:flutter/foundation.dart';\n\n/// 应用配置 - 统一管理所有配置项\nclass AppConfig {\n AppConfig._();\n\n static final AppConfig instance = AppConfig._();\n\n // 环境配置\n static const String environment = String.fromEnvironment(\n 'ENV',\n defaultValue: 'production',\n );\n\n static bool get isProduction {\n if (kIsWeb) {\n final currentUrl = Uri.base.toString();\n if (currentUrl.contains('fabushi-flutter-web-dev') ||\n currentUrl.contains('localhost')) {\n return false;\n }\n if (currentUrl.contains('fabushi-flutter-web-prod')) {\n return true;\n }\n }\n return environment == 'production';\n }\n\n static bool get isDevelopment => !isProduction;\n static bool get isStaging => environment == 'staging';\n static bool get isWeb => kIsWeb;\n\n // API配置\n static const String configuredApiBaseUrl = String.fromEnvironment(\n 'API_BASE_URL',\n defaultValue: '',\n );\n static const String primaryBackendUrl = 'https://api.ombhrum.com';\n static const String cloudflareWorkerProdUrl = 'https://api.ombhrum.com';\n static const String cloudflareWorkerDevUrl =\n 'https://fabushi-flutter-web-dev.bhrumom.workers.dev';\n static const String localDevUrl = 'http://localhost:8787';\n static const String publicWebUrl = 'https://flutter.ombhrum.com';\n\n static String get currentBackendUrl {\n if (configuredApiBaseUrl.isNotEmpty) {\n return configuredApiBaseUrl;\n }\n if (kIsWeb) {\n final currentUrl = Uri.base;\n final host = currentUrl.host;\n if (host.contains('localhost')) {\n return localDevUrl;\n }\n if (host.contains('fabushi-flutter-web-dev')) {\n return cloudflareWorkerDevUrl;\n }\n if (host.contains('fabushi-flutter-web-prod')) {\n return cloudflareWorkerProdUrl;\n }\n }\n return primaryBackendUrl;\n }\n\n static String get apiUrl => currentBackendUrl;\n\n /// 大厂式 API 网关入口:客户端只允许构造自家后端的相对路径。\n ///\n /// 第三方 API、上游 API、缓存、重试、fallback 都应在后端处理;\n /// App 端不得直接传入 https://... 或 http://... 这类绝对 URL。\n static Uri buildBackendUri(\n String endpoint, {\n Map<String, String>? queryParameters,\n }) {\n final normalizedEndpoint = endpoint.trim();\n if (normalizedEndpoint.isEmpty || !normalizedEndpoint.startsWith('/')) {\n throw ArgumentError(\n 'API endpoint must be a first-party relative path starting with /: $endpoint',\n );\n }\n if (normalizedEndpoint.startsWith('//') ||\n normalizedEndpoint.contains('://')) {\n throw ArgumentError(\n 'API endpoint must not be an absolute or protocol-relative URL: $endpoint',\n );\n }\n\n final baseUrl = currentBackendUrl.replaceFirst(RegExp(r'/+$'), '');\n final uri = Uri.parse('$baseUrl$normalizedEndpoint');\n if (queryParameters == null || queryParameters.isEmpty) {\n return uri;\n }\n return uri.replace(queryParameters: queryParameters);\n }\n\n static String buildBackendUrl(\n String endpoint, {\n Map<String, String>? queryParameters,\n }) {\n return buildBackendUri(\n endpoint,\n queryParameters: queryParameters,\n ).toString();\n }\n\n // 应用信息\n static const String appName = '大乘';\n // 保持与 pubspec.yaml 中的 version 同步,供 Web 回退和服务端比对使用。\n static const String appVersion = '1.0.0';\n static const int appBuildNumber = 16;\n\n // 功能开关\n static const bool enableFirebaseAuth = true;\n static const bool enableAlipay = true;\n static const bool enableAppleIAP = true;\n static const bool enableVideoFeed = true;\n static bool get enableDebugMode => !isProduction;\n\n // 传输配置\n static const int fileChunkSize = 1024;\n static const int maxRetryCount = 3;\n static const int timeoutDuration = 5000;\n static const int maxConcurrentTransfers = 5;\n\n // 缓存配置\n static const int cacheMaxAge = 7;\n static const int maxCacheSize = 100;\n\n // 分页配置\n static const int defaultPageSize = 20;\n static const int maxPageSize = 100;\n\n // 超时配置\n static final Duration requestTimeout = const Duration(seconds: 30);\n static final Duration connectTimeout = const Duration(seconds: 10);\n\n // 重试配置\n static const int maxRetries = 3;\n static final Duration retryDelay = const Duration(seconds: 1);\n\n // 日志配置\n static const bool enableLogging = true;\n static bool get enableNetworkLogging => !isProduction;\n static bool get enablePerformanceLogging => !isProduction;\n\n // 存储键名\n static const String tokenStorageKey = 'auth_token';\n static const String userInfoStorageKey = 'user_info';\n static const String backendUrlStorageKey = 'backend_url';\n static const String testModeStorageKey = 'test_mode';\n\n // API端点\n static const String cbetaSendTextsEndpoint = '/api/cbeta/send-texts';\n\n static String get loginUrl => buildBackendUrl('/api/auth/login');\n static String get registerUrl => buildBackendUrl('/api/auth/register');\n static String get verifyUrl => buildBackendUrl('/api/auth/verify');\n static String get logoutUrl => buildBackendUrl('/api/auth/logout');\n static String get deleteAccountUrl => buildBackendUrl('/api/auth/delete');\n static String get sendVerificationCodeUrl =>\n buildBackendUrl('/api/auth/send-verification-code');\n static String get verifyCodeUrl => buildBackendUrl('/api/auth/verify-code');\n static String get forgotPasswordUrl =>\n buildBackendUrl('/api/auth/forgot-password');\n static String get resetPasswordUrl =>\n buildBackendUrl('/api/auth/reset-password');\n static String get userInfoUrl => buildBackendUrl('/api/auth/user-info');\n static String get bindEmailUrl => buildBackendUrl('/api/auth/bind-email');\n static String get appVersionPolicyUrl =>\n buildBackendUrl('/api/app/version-policy');\n\n static String get alipayCreateOrderUrl =>\n buildBackendUrl('/api/alipay/create-order');\n static String get alipayQueryOrderUrl =>\n buildBackendUrl('/api/alipay/query-order');\n static String get alipayMembershipStatusUrl =>\n buildBackendUrl('/api/alipay/check-membership');\n\n static String get stripeMembershipStatusUrl =>\n buildBackendUrl('/api/stripe/membership-status');\n static String get stripeCreateSubscriptionUrl =>\n buildBackendUrl('/api/stripe/create-subscription');\n static String get stripeSessionStatusUrl =>\n buildBackendUrl('/api/stripe/session-status');\n\n static String get appleVerifyReceiptUrl =>\n buildBackendUrl('/api/apple/verify-receipt');\n\n static String get adminCheckStatusUrl =>\n buildBackendUrl('/api/admin/check-status');\n static String get adminCreateRedeemCodeUrl =>\n buildBackendUrl('/api/admin/create-redeem-code');\n static String get adminRedeemCodesUrl =>\n buildBackendUrl('/api/admin/redeem-codes');\n static String get adminUseRedeemCodeUrl =>\n buildBackendUrl('/api/admin/use-redeem-code');\n static String get adminPurchaseHistoryUrl =>\n buildBackendUrl('/api/admin/purchase-history');\n static String get adminRedeemHistoryUrl =>\n buildBackendUrl('/api/admin/redeem-history');\n\n static String get leaderboardUrl => buildBackendUrl('/api/leaderboard');\n static String get updateTransferDataUrl =>\n buildBackendUrl('/api/leaderboard/update');\n\n static String get healthCheckUrl => buildBackendUrl('/health');\n\n // iOS 后台保活静音音频\n static String get silenceAudioUrl =>\n buildBackendUrl('/static/audio/silence.mp3');\n\n // 3D 佛像模型配置\n // 如果 R2 上需要切换到新的对象键,优先改这里,便于强制绕开旧缓存。\n static const String buddhaModelAssetPath = 'models/buddha_model.model';\n // Android 使用 three_dart 直接渲染 GLB;iOS 继续使用 flutter_scene .model。\n static const String legacyBuddhaGlbAssetPath = 'models/佛像模型.glb';\n static const String androidThreeBuddhaGlbAssetPath =\n 'web/assets/models/佛像模型.glb';\n static String get legacyBuddhaGlbUrl {\n final encodedPath = legacyBuddhaGlbAssetPath\n .split('/')\n .map(Uri.encodeComponent)\n .join('/');\n return '$publicWebUrl/assets/$encodedPath';\n }\n\n // 当前线上正确模型明显大于 100MB,小于该阈值视为误传/降质文件。\n static const int minBuddhaModelSizeBytes = 100 * 1024 * 1024;\n static const int minBuddhaGlbSizeBytes = 10 * 1024 * 1024;\n\n // 请求头\n static Map<String, String> get defaultHeaders => {\n 'Content-Type': 'application/json',\n 'Accept': 'application/json',\n 'User-Agent': 'FabushiApp/${isWeb ? \"Web\" : \"Mobile\"}',\n };\n\n // 错误消息\n static const Map<String, String> errorMessages = {\n 'network_error': '网络连接失败,请检查网络设置',\n 'server_error': '服务器错误,请稍后重试',\n 'timeout_error': '请求超时,请检查网络连接',\n 'auth_error': '认证失败,请重新登录',\n 'permission_error': '权限不足',\n 'validation_error': '数据验证失败',\n 'unknown_error': '未知错误,请联系客服',\n };\n\n // 备用地址\n static List<String> get fallbackUrls {\n return [primaryBackendUrl];\n }\n\n // 调试配置\n static const bool enableApiLogging = true;\n static const bool debugMode = bool.fromEnvironment(\n 'DEBUG',\n defaultValue: false,\n );\n\n static void printConfigInfo() {\n if (kDebugMode) {\n print('=== 应用配置 ===');\n print('环境: ${isProduction ? \"生产\" : \"开发\"}');\n print('平台: ${isWeb ? \"Web\" : \"Native\"}');\n print('API URL: $currentBackendUrl');\n print('================');\n }\n }\n\n static void printCurrentConfig() => printConfigInfo();\n}\n"}, "3dd4e4dc806f549fa65b642076a9261d629c6e2c": {"t": 1785757976.2724688, "n": 8915, "h": "c507b390f5ad9fa4", "c": "import 'package:flutter/foundation.dart';\n\n/// 应用配置 - 统一管理所有配置项\nclass AppConfig {\n AppConfig._();\n\n static final AppConfig instance = AppConfig._();\n\n // 环境配置\n static const String environment = String.fromEnvironment(\n 'ENV',\n defaultValue: 'production',\n );\n\n static bool get isProduction {\n if (kIsWeb) {\n final currentUrl = Uri.base.toString();\n if (currentUrl.contains('fabushi-flutter-web-dev') ||\n currentUrl.contains('localhost')) {\n return false;\n }\n if (currentUrl.contains('fabushi-flutter-web-prod')) {\n return true;\n }\n }\n return environment == 'production';\n }\n\n static bool get isDevelopment => !isProduction;\n static bool get isStaging => environment == 'staging';\n static bool get isWeb => kIsWeb;\n\n // API配置\n static const String configuredApiBaseUrl = String.fromEnvironment(\n 'API_BASE_URL',\n defaultValue: '',\n );\n static const String primaryBackendUrl = 'https://api.ombhrum.com';\n static const String cloudflareWorkerProdUrl = 'https://api.ombhrum.com';\n static const String cloudflareWorkerDevUrl =\n 'https://fabushi-flutter-web-dev.bhrumom.workers.dev';\n static const String localDevUrl = 'http://localhost:8787';\n static const String publicWebUrl = 'https://flutter.ombhrum.com';\n\n static String get currentBackendUrl {\n if (configuredApiBaseUrl.isNotEmpty) {\n return configuredApiBaseUrl;\n }\n if (kIsWeb) {\n final currentUrl = Uri.base;\n final host = currentUrl.host;\n if (host.contains('localhost')) {\n return localDevUrl;\n }\n if (host.contains('fabushi-flutter-web-dev')) {\n return cloudflareWorkerDevUrl;\n }\n if (host.contains('fabushi-flutter-web-prod')) {\n return cloudflareWorkerProdUrl;\n }\n }\n return primaryBackendUrl;\n }\n\n static String get apiUrl => currentBackendUrl;\n\n /// 大厂式 API 网关入口:客户端只允许构造自家后端的相对路径。\n ///\n /// 第三方 API、上游 API、缓存、重试、fallback 都应在后端处理;\n /// App 端不得直接传入 https://... 或 http://... 这类绝对 URL。\n static Uri buildBackendUri(\n String endpoint, {\n Map<String, String>? queryParameters,\n }) {\n final normalizedEndpoint = endpoint.trim();\n if (normalizedEndpoint.isEmpty || !normalizedEndpoint.startsWith('/')) {\n throw ArgumentError(\n 'API endpoint must be a first-party relative path starting with /: $endpoint',\n );\n }\n if (normalizedEndpoint.startsWith('//') ||\n normalizedEndpoint.contains('://')) {\n throw ArgumentError(\n 'API endpoint must not be an absolute or protocol-relative URL: $endpoint',\n );\n }\n\n final baseUrl = currentBackendUrl.replaceFirst(RegExp(r'/+$'), '');\n final uri = Uri.parse('$baseUrl$normalizedEndpoint');\n if (queryParameters == null || queryParameters.isEmpty) {\n return uri;\n }\n return uri.replace(queryParameters: queryParameters);\n }\n\n static String buildBackendUrl(\n String endpoint, {\n Map<String, String>? queryParameters,\n }) {\n return buildBackendUri(\n endpoint,\n queryParameters: queryParameters,\n ).toString();\n }\n\n // 应用信息\n static const String appName = '大乘';\n static const String appVersion = '1.0.0';\n static const int appBuildNumber = 1;\n\n // 功能开关\n static const bool enableFirebaseAuth = true;\n static const bool enableAlipay = true;\n static const bool enableAppleIAP = true;\n static const bool enableVideoFeed = true;\n static bool get enableDebugMode => !isProduction;\n\n // 传输配置\n static const int fileChunkSize = 1024;\n static const int maxRetryCount = 3;\n static const int timeoutDuration = 5000;\n static const int maxConcurrentTransfers = 5;\n\n // 缓存配置\n static const int cacheMaxAge = 7;\n static const int maxCacheSize = 100;\n\n // 分页配置\n static const int defaultPageSize = 20;\n static const int maxPageSize = 100;\n\n // 超时配置\n static final Duration requestTimeout = const Duration(seconds: 30);\n static final Duration connectTimeout = const Duration(seconds: 10);\n\n // 重试配置\n static const int maxRetries = 3;\n static final Duration retryDelay = const Duration(seconds: 1);\n\n // 日志配置\n static const bool enableLogging = true;\n static bool get enableNetworkLogging => !isProduction;\n static bool get enablePerformanceLogging => !isProduction;\n\n // 存储键名\n static const String tokenStorageKey = 'auth_token';\n static const String userInfoStorageKey = 'user_info';\n static const String backendUrlStorageKey = 'backend_url';\n static const String testModeStorageKey = 'test_mode';\n\n // API端点\n static const String cbetaSendTextsEndpoint = '/api/cbeta/send-texts';\n\n static String get loginUrl => buildBackendUrl('/api/auth/login');\n static String get registerUrl => buildBackendUrl('/api/auth/register');\n static String get verifyUrl => buildBackendUrl('/api/auth/verify');\n static String get logoutUrl => buildBackendUrl('/api/auth/logout');\n static String get deleteAccountUrl => buildBackendUrl('/api/auth/delete');\n static String get sendVerificationCodeUrl =>\n buildBackendUrl('/api/auth/send-verification-code');\n static String get verifyCodeUrl => buildBackendUrl('/api/auth/verify-code');\n static String get forgotPasswordUrl =>\n buildBackendUrl('/api/auth/forgot-password');\n static String get resetPasswordUrl =>\n buildBackendUrl('/api/auth/reset-password');\n static String get userInfoUrl => buildBackendUrl('/api/auth/user-info');\n static String get bindEmailUrl => buildBackendUrl('/api/auth/bind-email');\n\n static String get alipayCreateOrderUrl =>\n buildBackendUrl('/api/alipay/create-order');\n static String get alipayQueryOrderUrl =>\n buildBackendUrl('/api/alipay/query-order');\n static String get alipayMembershipStatusUrl =>\n buildBackendUrl('/api/alipay/check-membership');\n\n static String get stripeMembershipStatusUrl =>\n buildBackendUrl('/api/stripe/membership-status');\n static String get stripeCreateSubscriptionUrl =>\n buildBackendUrl('/api/stripe/create-subscription');\n static String get stripeSessionStatusUrl =>\n buildBackendUrl('/api/stripe/session-status');\n\n static String get appleVerifyReceiptUrl =>\n buildBackendUrl('/api/apple/verify-receipt');\n\n static String get adminCheckStatusUrl =>\n buildBackendUrl('/api/admin/check-status');\n static String get adminCreateRedeemCodeUrl =>\n buildBackendUrl('/api/admin/create-redeem-code');\n static String get adminRedeemCodesUrl =>\n buildBackendUrl('/api/admin/redeem-codes');\n static String get adminUseRedeemCodeUrl =>\n buildBackendUrl('/api/admin/use-redeem-code');\n static String get adminPurchaseHistoryUrl =>\n buildBackendUrl('/api/admin/purchase-history');\n static String get adminRedeemHistoryUrl =>\n buildBackendUrl('/api/admin/redeem-history');\n\n static String get leaderboardUrl => buildBackendUrl('/api/leaderboard');\n static String get updateTransferDataUrl =>\n buildBackendUrl('/api/leaderboard/update');\n\n static String get healthCheckUrl => buildBackendUrl('/health');\n\n // iOS 后台保活静音音频\n static String get silenceAudioUrl =>\n buildBackendUrl('/static/audio/silence.mp3');\n\n // 3D 佛像模型配置\n // 如果 R2 上需要切换到新的对象键,优先改这里,便于强制绕开旧缓存。\n static const String buddhaModelAssetPath = 'models/buddha_model.model';\n // Android 使用 three_dart 直接渲染 GLB;iOS 继续使用 flutter_scene .model。\n static const String legacyBuddhaGlbAssetPath = 'models/佛像模型.glb';\n static const String androidThreeBuddhaGlbAssetPath =\n 'web/assets/models/佛像模型.glb';\n static String get legacyBuddhaGlbUrl {\n final encodedPath = legacyBuddhaGlbAssetPath\n .split('/')\n .map(Uri.encodeComponent)\n .join('/');\n return '$publicWebUrl/assets/$encodedPath';\n }\n\n // 当前线上正确模型明显大于 100MB,小于该阈值视为误传/降质文件。\n static const int minBuddhaModelSizeBytes = 100 * 1024 * 1024;\n static const int minBuddhaGlbSizeBytes = 10 * 1024 * 1024;\n\n // 请求头\n static Map<String, String> get defaultHeaders => {\n 'Content-Type': 'application/json',\n 'Accept': 'application/json',\n 'User-Agent': 'FabushiApp/${isWeb ? \"Web\" : \"Mobile\"}',\n };\n\n // 错误消息\n static const Map<String, String> errorMessages = {\n 'network_error': '网络连接失败,请检查网络设置',\n 'server_error': '服务器错误,请稍后重试',\n 'timeout_error': '请求超时,请检查网络连接',\n 'auth_error': '认证失败,请重新登录',\n 'permission_error': '权限不足',\n 'validation_error': '数据验证失败',\n 'unknown_error': '未知错误,请联系客服',\n };\n\n // 备用地址\n static List<String> get fallbackUrls {\n return [primaryBackendUrl];\n }\n\n // 调试配置\n static const bool enableApiLogging = true;\n static const bool debugMode = bool.fromEnvironment(\n 'DEBUG',\n defaultValue: false,\n );\n\n static void printConfigInfo() {\n if (kDebugMode) {\n print('=== 应用配置 ===');\n print('环境: ${isProduction ? \"生产\" : \"开发\"}');\n print('平台: ${isWeb ? \"Web\" : \"Native\"}');\n print('API URL: $currentBackendUrl');\n print('================');\n }\n }\n\n static void printCurrentConfig() => printConfigInfo();\n}\n"}, "b2140e6db1e92aef545dc4f48811ff21d7ce6ce9": {"t": 1785757977.9279854, "n": 8473, "h": "4c0a906b68a3ad97", "c": "import 'package:flutter/foundation.dart';\n\n/// 应用配置 - 统一管理所有配置项\nclass AppConfig {\n AppConfig._();\n\n static final AppConfig instance = AppConfig._();\n\n // 环境配置\n static const String environment = String.fromEnvironment(\n 'ENV',\n defaultValue: 'production',\n );\n\n static bool get isProduction {\n if (kIsWeb) {\n final currentUrl = Uri.base.toString();\n if (currentUrl.contains('fabushi-flutter-web-dev') ||\n currentUrl.contains('localhost')) {\n return false;\n }\n if (currentUrl.contains('fabushi-flutter-web-prod')) {\n return true;\n }\n }\n return environment == 'production';\n }\n\n static bool get isDevelopment => !isProduction;\n static bool get isStaging => environment == 'staging';\n static bool get isWeb => kIsWeb;\n\n // API配置\n static const String configuredApiBaseUrl = String.fromEnvironment(\n 'API_BASE_URL',\n defaultValue: '',\n );\n static const String primaryBackendUrl = 'https://api.ombhrum.com';\n static const String cloudflareWorkerProdUrl = 'https://api.ombhrum.com';\n static const String cloudflareWorkerDevUrl =\n 'https://fabushi-flutter-web-dev.bhrumom.workers.dev';\n static const String localDevUrl = 'http://localhost:8787';\n static const String publicWebUrl = 'https://flutter.ombhrum.com';\n\n static String get currentBackendUrl {\n if (configuredApiBaseUrl.isNotEmpty) {\n return configuredApiBaseUrl;\n }\n if (kIsWeb) {\n final currentUrl = Uri.base;\n final host = currentUrl.host;\n if (host.contains('localhost')) {\n return localDevUrl;\n }\n if (host.contains('fabushi-flutter-web-dev')) {\n return cloudflareWorkerDevUrl;\n }\n if (host.contains('fabushi-flutter-web-prod')) {\n return cloudflareWorkerProdUrl;\n }\n }\n return primaryBackendUrl;\n }\n\n static String get apiUrl => currentBackendUrl;\n\n /// 大厂式 API 网关入口:客户端只允许构造自家后端的相对路径。\n ///\n /// 第三方 API、上游 API、缓存、重试、fallback 都应在后端处理;\n /// App 端不得直接传入 https://... 或 http://... 这类绝对 URL。\n static Uri buildBackendUri(\n String endpoint, {\n Map<String, String>? queryParameters,\n }) {\n final normalizedEndpoint = endpoint.trim();\n if (normalizedEndpoint.isEmpty || !normalizedEndpoint.startsWith('/')) {\n throw ArgumentError(\n 'API endpoint must be a first-party relative path starting with /: $endpoint',\n );\n }\n if (normalizedEndpoint.startsWith('//') ||\n normalizedEndpoint.contains('://')) {\n throw ArgumentError(\n 'API endpoint must not be an absolute or protocol-relative URL: $endpoint',\n );\n }\n\n final baseUrl = currentBackendUrl.replaceFirst(RegExp(r'/+$'), '');\n final uri = Uri.parse('$baseUrl$normalizedEndpoint');\n if (queryParameters == null || queryParameters.isEmpty) {\n return uri;\n }\n return uri.replace(queryParameters: queryParameters);\n }\n\n static String buildBackendUrl(\n String endpoint, {\n Map<String, String>? queryParameters,\n }) {\n return buildBackendUri(\n endpoint,\n queryParameters: queryParameters,\n ).toString();\n }\n\n // 应用信息\n static const String appName = '大乘';\n static const String appVersion = '1.0.0';\n static const int appBuildNumber = 1;\n\n // 功能开关\n static const bool enableFirebaseAuth = true;\n static const bool enableAlipay = true;\n static const bool enableAppleIAP = true;\n static const bool enableVideoFeed = true;\n static bool get enableDebugMode => !isProduction;\n\n // 传输配置\n static const int fileChunkSize = 1024;\n static const int maxRetryCount = 3;\n static const int timeoutDuration = 5000;\n static const int maxConcurrentTransfers = 5;\n\n // 缓存配置\n static const int cacheMaxAge = 7;\n static const int maxCacheSize = 100;\n\n // 分页配置\n static const int defaultPageSize = 20;\n static const int maxPageSize = 100;\n\n // 超时配置\n static final Duration requestTimeout = const Duration(seconds: 30);\n static final Duration connectTimeout = const Duration(seconds: 10);\n\n // 重试配置\n static const int maxRetries = 3;\n static final Duration retryDelay = const Duration(seconds: 1);\n\n // 日志配置\n static const bool enableLogging = true;\n static bool get enableNetworkLogging => !isProduction;\n static bool get enablePerformanceLogging => !isProduction;\n\n // 存储键名\n static const String tokenStorageKey = 'auth_token';\n static const String userInfoStorageKey = 'user_info';\n static const String backendUrlStorageKey = 'backend_url';\n static const String testModeStorageKey = 'test_mode';\n\n // API端点\n static const String cbetaSendTextsEndpoint = '/api/cbeta/send-texts';\n\n static String get loginUrl => buildBackendUrl('/api/auth/login');\n static String get registerUrl => buildBackendUrl('/api/auth/register');\n static String get verifyUrl => buildBackendUrl('/api/auth/verify');\n static String get logoutUrl => buildBackendUrl('/api/auth/logout');\n static String get deleteAccountUrl => buildBackendUrl('/api/auth/delete');\n static String get sendVerificationCodeUrl =>\n buildBackendUrl('/api/auth/send-verification-code');\n static String get verifyCodeUrl => buildBackendUrl('/api/auth/verify-code');\n static String get forgotPasswordUrl =>\n buildBackendUrl('/api/auth/forgot-password');\n static String get resetPasswordUrl =>\n buildBackendUrl('/api/auth/reset-password');\n static String get userInfoUrl => buildBackendUrl('/api/auth/user-info');\n static String get bindEmailUrl => buildBackendUrl('/api/auth/bind-email');\n\n static String get alipayCreateOrderUrl =>\n buildBackendUrl('/api/alipay/create-order');\n static String get alipayQueryOrderUrl =>\n buildBackendUrl('/api/alipay/query-order');\n static String get alipayMembershipStatusUrl =>\n buildBackendUrl('/api/alipay/check-membership');\n\n static String get stripeMembershipStatusUrl =>\n buildBackendUrl('/api/stripe/membership-status');\n static String get stripeCreateSubscriptionUrl =>\n buildBackendUrl('/api/stripe/create-subscription');\n static String get stripeSessionStatusUrl =>\n buildBackendUrl('/api/stripe/session-status');\n\n static String get appleVerifyReceiptUrl =>\n buildBackendUrl('/api/apple/verify-receipt');\n\n static String get adminCheckStatusUrl =>\n buildBackendUrl('/api/admin/check-status');\n static String get adminCreateRedeemCodeUrl =>\n buildBackendUrl('/api/admin/create-redeem-code');\n static String get adminRedeemCodesUrl =>\n buildBackendUrl('/api/admin/redeem-codes');\n static String get adminUseRedeemCodeUrl =>\n buildBackendUrl('/api/admin/use-redeem-code');\n static String get adminPurchaseHistoryUrl =>\n buildBackendUrl('/api/admin/purchase-history');\n static String get adminRedeemHistoryUrl =>\n buildBackendUrl('/api/admin/redeem-history');\n\n static String get leaderboardUrl => buildBackendUrl('/api/leaderboard');\n static String get updateTransferDataUrl =>\n buildBackendUrl('/api/leaderboard/update');\n\n static String get healthCheckUrl => buildBackendUrl('/health');\n\n // iOS 后台保活静音音频\n static String get silenceAudioUrl =>\n buildBackendUrl('/static/audio/silence.mp3');\n\n // 3D 佛像模型配置\n // 如果 R2 上需要切换到新的对象键,优先改这里,便于强制绕开旧缓存。\n static const String buddhaModelAssetPath = 'models/buddha_model.model';\n // Android/iOS 均只使用 R2 上的 flutter_scene .model;本地无有效缓存时再下载。\n // 当前线上正确模型明显大于 100MB,小于该阈值视为误传/降质文件。\n static const int minBuddhaModelSizeBytes = 100 * 1024 * 1024;\n\n // 请求头\n static Map<String, String> get defaultHeaders => {\n 'Content-Type': 'application/json',\n 'Accept': 'application/json',\n 'User-Agent': 'FabushiApp/${isWeb ? \"Web\" : \"Mobile\"}',\n };\n\n // 错误消息\n static const Map<String, String> errorMessages = {\n 'network_error': '网络连接失败,请检查网络设置',\n 'server_error': '服务器错误,请稍后重试',\n 'timeout_error': '请求超时,请检查网络连接',\n 'auth_error': '认证失败,请重新登录',\n 'permission_error': '权限不足',\n 'validation_error': '数据验证失败',\n 'unknown_error': '未知错误,请联系客服',\n };\n\n // 备用地址\n static List<String> get fallbackUrls {\n return [primaryBackendUrl];\n }\n\n // 调试配置\n static const bool enableApiLogging = true;\n static const bool debugMode = bool.fromEnvironment(\n 'DEBUG',\n defaultValue: false,\n );\n\n static void printConfigInfo() {\n if (kDebugMode) {\n print('=== 应用配置 ===');\n print('环境: ${isProduction ? \"生产\" : \"开发\"}');\n print('平台: ${isWeb ? \"Web\" : \"Native\"}');\n print('API URL: $currentBackendUrl');\n print('================');\n }\n }\n\n static void printCurrentConfig() => printConfigInfo();\n}\n"}, "a7b8b7e8acf5939b58f55eaeecc63b739752e305": {"t": 1785757979.5582879, "n": 7219, "h": "411cf9c24cfbd0f6", "c": "import 'package:flutter/foundation.dart';\n\n/// 应用配置 - 统一管理所有配置项\nclass AppConfig {\n AppConfig._();\n\n static final AppConfig instance = AppConfig._();\n\n // 环境配置\n static const String environment = String.fromEnvironment(\n 'ENV',\n defaultValue: 'production',\n );\n\n static bool get isProduction {\n if (kIsWeb) {\n final currentUrl = Uri.base.toString();\n if (currentUrl.contains('fabushi-flutter-web-dev') ||\n currentUrl.contains('localhost')) {\n return false;\n }\n if (currentUrl.contains('fabushi-flutter-web-prod')) {\n return true;\n }\n }\n return environment == 'production';\n }\n\n static bool get isDevelopment => !isProduction;\n static bool get isStaging => environment == 'staging';\n static bool get isWeb => kIsWeb;\n\n // API配置\n static const String configuredApiBaseUrl = String.fromEnvironment(\n 'API_BASE_URL',\n defaultValue: '',\n );\n static const String primaryBackendUrl = 'https://api.ombhrum.com';\n static const String cloudflareWorkerProdUrl = 'https://api.ombhrum.com';\n static const String cloudflareWorkerDevUrl =\n 'https://fabushi-flutter-web-dev.bhrumom.workers.dev';\n static const String localDevUrl = 'http://localhost:8787';\n static const String publicWebUrl = 'https://flutter.ombhrum.com';\n\n static String get currentBackendUrl {\n if (configuredApiBaseUrl.isNotEmpty) {\n return configuredApiBaseUrl;\n }\n if (kIsWeb) {\n final currentUrl = Uri.base;\n final host = currentUrl.host;\n if (host.contains('localhost')) {\n return localDevUrl;\n }\n if (host.contains('fabushi-flutter-web-dev')) {\n return cloudflareWorkerDevUrl;\n }\n if (host.contains('fabushi-flutter-web-prod')) {\n return cloudflareWorkerProdUrl;\n }\n }\n return primaryBackendUrl;\n }\n\n static String get apiUrl => currentBackendUrl;\n\n // 应用信息\n static const String appName = '大乘';\n static const String appVersion = '1.0.0';\n static const int appBuildNumber = 1;\n\n // 功能开关\n static const bool enableFirebaseAuth = true;\n static const bool enableAlipay = true;\n static const bool enableAppleIAP = true;\n static const bool enableVideoFeed = true;\n static bool get enableDebugMode => !isProduction;\n\n // 传输配置\n static const int fileChunkSize = 1024;\n static const int maxRetryCount = 3;\n static const int timeoutDuration = 5000;\n static const int maxConcurrentTransfers = 5;\n\n // 缓存配置\n static const int cacheMaxAge = 7;\n static const int maxCacheSize = 100;\n\n // 分页配置\n static const int defaultPageSize = 20;\n static const int maxPageSize = 100;\n\n // 超时配置\n static final Duration requestTimeout = const Duration(seconds: 30);\n static final Duration connectTimeout = const Duration(seconds: 10);\n\n // 重试配置\n static const int maxRetries = 3;\n static final Duration retryDelay = const Duration(seconds: 1);\n\n // 日志配置\n static const bool enableLogging = true;\n static bool get enableNetworkLogging => !isProduction;\n static bool get enablePerformanceLogging => !isProduction;\n\n // 存储键名\n static const String tokenStorageKey = 'auth_token';\n static const String userInfoStorageKey = 'user_info';\n static const String backendUrlStorageKey = 'backend_url';\n static const String testModeStorageKey = 'test_mode';\n\n // API端点\n static String get loginUrl => '$currentBackendUrl/api/auth/login';\n static String get registerUrl => '$currentBackendUrl/api/auth/register';\n static String get verifyUrl => '$currentBackendUrl/api/auth/verify';\n static String get logoutUrl => '$currentBackendUrl/api/auth/logout';\n static String get deleteAccountUrl => '$currentBackendUrl/api/auth/delete';\n static String get sendVerificationCodeUrl =>\n '$currentBackendUrl/api/auth/send-verification-code';\n static String get verifyCodeUrl => '$currentBackendUrl/api/auth/verify-code';\n static String get forgotPasswordUrl =>\n '$currentBackendUrl/api/auth/forgot-password';\n static String get resetPasswordUrl =>\n '$currentBackendUrl/api/auth/reset-password';\n static String get userInfoUrl => '$currentBackendUrl/api/auth/user-info';\n static String get bindEmailUrl => '$currentBackendUrl/api/auth/bind-email';\n\n static String get alipayCreateOrderUrl =>\n '$currentBackendUrl/api/alipay/create-order';\n static String get alipayQueryOrderUrl =>\n '$currentBackendUrl/api/alipay/query-order';\n static String get alipayMembershipStatusUrl =>\n '$currentBackendUrl/api/alipay/check-membership';\n\n static String get stripeMembershipStatusUrl =>\n '$currentBackendUrl/api/stripe/membership-status';\n static String get stripeCreateSubscriptionUrl =>\n '$currentBackendUrl/api/stripe/create-subscription';\n static String get stripeSessionStatusUrl =>\n '$currentBackendUrl/api/stripe/session-status';\n\n static String get appleVerifyReceiptUrl =>\n '$currentBackendUrl/api/apple/verify-receipt';\n\n static String get adminCheckStatusUrl =>\n '$currentBackendUrl/api/admin/check-status';\n static String get adminCreateRedeemCodeUrl =>\n '$currentBackendUrl/api/admin/create-redeem-code';\n static String get adminRedeemCodesUrl =>\n '$currentBackendUrl/api/admin/redeem-codes';\n static String get adminUseRedeemCodeUrl =>\n '$currentBackendUrl/api/admin/use-redeem-code';\n static String get adminPurchaseHistoryUrl =>\n '$currentBackendUrl/api/admin/purchase-history';\n static String get adminRedeemHistoryUrl =>\n '$currentBackendUrl/api/admin/redeem-history';\n\n static String get leaderboardUrl => '$currentBackendUrl/api/leaderboard';\n static String get updateTransferDataUrl =>\n '$currentBackendUrl/api/leaderboard/update';\n\n static String get healthCheckUrl => '$currentBackendUrl/health';\n\n // iOS 后台保活静音音频\n static String get silenceAudioUrl =>\n '$currentBackendUrl/static/audio/silence.mp3';\n\n // 3D 佛像模型配置\n // 如果 R2 上需要切换到新的对象键,优先改这里,便于强制绕开旧缓存。\n static const String buddhaModelAssetPath = 'models/buddha_model.model';\n // Android/iOS 均只使用 R2 上的 flutter_scene .model;本地无有效缓存时再下载。\n // 当前线上正确模型明显大于 100MB,小于该阈值视为误传/降质文件。\n static const int minBuddhaModelSizeBytes = 100 * 1024 * 1024;\n\n // 请求头\n static Map<String, String> get defaultHeaders => {\n 'Content-Type': 'application/json',\n 'Accept': 'application/json',\n 'User-Agent': 'FabushiApp/${isWeb ? \"Web\" : \"Mobile\"}',\n };\n\n // 错误消息\n static const Map<String, String> errorMessages = {\n 'network_error': '网络连接失败,请检查网络设置',\n 'server_error': '服务器错误,请稍后重试',\n 'timeout_error': '请求超时,请检查网络连接',\n 'auth_error': '认证失败,请重新登录',\n 'permission_error': '权限不足',\n 'validation_error': '数据验证失败',\n 'unknown_error': '未知错误,请联系客服',\n };\n\n // 备用地址\n static List<String> get fallbackUrls {\n return [primaryBackendUrl];\n }\n\n // 调试配置\n static const bool enableApiLogging = true;\n static const bool debugMode = bool.fromEnvironment(\n 'DEBUG',\n defaultValue: false,\n );\n\n static void printConfigInfo() {\n if (kDebugMode) {\n print('=== 应用配置 ===');\n print('环境: ${isProduction ? \"生产\" : \"开发\"}');\n print('平台: ${isWeb ? \"Web\" : \"Native\"}');\n print('API URL: $currentBackendUrl');\n print('================');\n }\n }\n\n static void printCurrentConfig() => printConfigInfo();\n}\n"}, "33e5d5f9d6f2803c9dedf7b9e78e7972073210b2": {"t": 1785757981.3295853, "n": 7674, "h": "83cdbe841606ccc5", "c": "import 'package:flutter/foundation.dart';\n\n/// 应用配置 - 统一管理所有配置项\nclass AppConfig {\n AppConfig._();\n\n static final AppConfig instance = AppConfig._();\n\n // 环境配置\n static const String environment = String.fromEnvironment(\n 'ENV',\n defaultValue: 'production',\n );\n\n static bool get isProduction {\n if (kIsWeb) {\n final currentUrl = Uri.base.toString();\n if (currentUrl.contains('fabushi-flutter-web-dev') ||\n currentUrl.contains('localhost')) {\n return false;\n }\n if (currentUrl.contains('fabushi-flutter-web-prod')) {\n return true;\n }\n }\n return environment == 'production';\n }\n\n static bool get isDevelopment => !isProduction;\n static bool get isStaging => environment == 'staging';\n static bool get isWeb => kIsWeb;\n\n // API配置\n static const String configuredApiBaseUrl = String.fromEnvironment(\n 'API_BASE_URL',\n defaultValue: '',\n );\n static const String primaryBackendUrl = 'https://api.ombhrum.com';\n static const String cloudflareWorkerProdUrl = 'https://api.ombhrum.com';\n static const String cloudflareWorkerDevUrl =\n 'https://fabushi-flutter-web-dev.bhrumom.workers.dev';\n static const String localDevUrl = 'http://localhost:8787';\n static const String publicWebUrl = 'https://flutter.ombhrum.com';\n\n static String get currentBackendUrl {\n if (configuredApiBaseUrl.isNotEmpty) {\n return configuredApiBaseUrl;\n }\n if (kIsWeb) {\n final currentUrl = Uri.base;\n final host = currentUrl.host;\n if (host.contains('localhost')) {\n return localDevUrl;\n }\n if (host.contains('fabushi-flutter-web-dev')) {\n return cloudflareWorkerDevUrl;\n }\n if (host.contains('fabushi-flutter-web-prod')) {\n return cloudflareWorkerProdUrl;\n }\n }\n return primaryBackendUrl;\n }\n\n static String get apiUrl => currentBackendUrl;\n\n // 应用信息\n static const String appName = '大乘';\n static const String appVersion = '1.0.0';\n static const int appBuildNumber = 1;\n\n // 功能开关\n static const bool enableFirebaseAuth = true;\n static const bool enableAlipay = true;\n static const bool enableAppleIAP = true;\n static const bool enableVideoFeed = true;\n static bool get enableDebugMode => !isProduction;\n\n // 传输配置\n static const int fileChunkSize = 1024;\n static const int maxRetryCount = 3;\n static const int timeoutDuration = 5000;\n static const int maxConcurrentTransfers = 5;\n\n // 缓存配置\n static const int cacheMaxAge = 7;\n static const int maxCacheSize = 100;\n\n // 分页配置\n static const int defaultPageSize = 20;\n static const int maxPageSize = 100;\n\n // 超时配置\n static final Duration requestTimeout = const Duration(seconds: 30);\n static final Duration connectTimeout = const Duration(seconds: 10);\n\n // 重试配置\n static const int maxRetries = 3;\n static final Duration retryDelay = const Duration(seconds: 1);\n\n // 日志配置\n static const bool enableLogging = true;\n static bool get enableNetworkLogging => !isProduction;\n static bool get enablePerformanceLogging => !isProduction;\n\n // 存储键名\n static const String tokenStorageKey = 'auth_token';\n static const String userInfoStorageKey = 'user_info';\n static const String backendUrlStorageKey = 'backend_url';\n static const String testModeStorageKey = 'test_mode';\n\n // API端点\n static String get loginUrl => '$currentBackendUrl/api/auth/login';\n static String get registerUrl => '$currentBackendUrl/api/auth/register';\n static String get verifyUrl => '$currentBackendUrl/api/auth/verify';\n static String get logoutUrl => '$currentBackendUrl/api/auth/logout';\n static String get deleteAccountUrl => '$currentBackendUrl/api/auth/delete';\n static String get sendVerificationCodeUrl =>\n '$currentBackendUrl/api/auth/send-verification-code';\n static String get verifyCodeUrl => '$currentBackendUrl/api/auth/verify-code';\n static String get forgotPasswordUrl =>\n '$currentBackendUrl/api/auth/forgot-password';\n static String get resetPasswordUrl =>\n '$currentBackendUrl/api/auth/reset-password';\n static String get userInfoUrl => '$currentBackendUrl/api/auth/user-info';\n static String get bindEmailUrl => '$currentBackendUrl/api/auth/bind-email';\n\n static String get alipayCreateOrderUrl =>\n '$currentBackendUrl/api/alipay/create-order';\n static String get alipayQueryOrderUrl =>\n '$currentBackendUrl/api/alipay/query-order';\n static String get alipayMembershipStatusUrl =>\n '$currentBackendUrl/api/alipay/check-membership';\n\n static String get stripeMembershipStatusUrl =>\n '$currentBackendUrl/api/stripe/membership-status';\n static String get stripeCreateSubscriptionUrl =>\n '$currentBackendUrl/api/stripe/create-subscription';\n static String get stripeSessionStatusUrl =>\n '$currentBackendUrl/api/stripe/session-status';\n\n static String get appleVerifyReceiptUrl =>\n '$currentBackendUrl/api/apple/verify-receipt';\n\n static String get adminCheckStatusUrl =>\n '$currentBackendUrl/api/admin/check-status';\n static String get adminCreateRedeemCodeUrl =>\n '$currentBackendUrl/api/admin/create-redeem-code';\n static String get adminRedeemCodesUrl =>\n '$currentBackendUrl/api/admin/redeem-codes';\n static String get adminUseRedeemCodeUrl =>\n '$currentBackendUrl/api/admin/use-redeem-code';\n static String get adminPurchaseHistoryUrl =>\n '$currentBackendUrl/api/admin/purchase-history';\n static String get adminRedeemHistoryUrl =>\n '$currentBackendUrl/api/admin/redeem-history';\n\n static String get leaderboardUrl => '$currentBackendUrl/api/leaderboard';\n static String get updateTransferDataUrl =>\n '$currentBackendUrl/api/leaderboard/update';\n\n static String get healthCheckUrl => '$currentBackendUrl/health';\n\n // iOS 后台保活静音音频\n static String get silenceAudioUrl =>\n '$currentBackendUrl/static/audio/silence.mp3';\n\n // 3D 佛像模型配置\n // 如果 R2 上需要切换到新的对象键,优先改这里,便于强制绕开旧缓存。\n static const String buddhaModelAssetPath = 'models/buddha_model.model';\n // Android/iOS 优先使用 R2 上的 flutter_scene .model;Android 失败时再回退到 three_dart GLB。\n static const String legacyBuddhaGlbAssetPath = 'models/佛像模型.glb';\n static const String androidThreeBuddhaGlbAssetPath =\n 'web/assets/models/佛像模型.glb';\n static String get legacyBuddhaGlbUrl {\n final encodedPath = legacyBuddhaGlbAssetPath\n .split('/')\n .map(Uri.encodeComponent)\n .join('/');\n return '$publicWebUrl/assets/$encodedPath';\n }\n\n // 当前线上正确模型明显大于 48MB,小于该阈值视为误传/降质文件。\n static const int minBuddhaModelSizeBytes = 100 * 1024 * 1024;\n static const int minBuddhaGlbSizeBytes = 10 * 1024 * 1024;\n\n // 请求头\n static Map<String, String> get defaultHeaders => {\n 'Content-Type': 'application/json',\n 'Accept': 'application/json',\n 'User-Agent': 'FabushiApp/${isWeb ? \"Web\" : \"Mobile\"}',\n };\n\n // 错误消息\n static const Map<String, String> errorMessages = {\n 'network_error': '网络连接失败,请检查网络设置',\n 'server_error': '服务器错误,请稍后重试',\n 'timeout_error': '请求超时,请检查网络连接',\n 'auth_error': '认证失败,请重新登录',\n 'permission_error': '权限不足',\n 'validation_error': '数据验证失败',\n 'unknown_error': '未知错误,请联系客服',\n };\n\n // 备用地址\n static List<String> get fallbackUrls {\n return [primaryBackendUrl];\n }\n\n // 调试配置\n static const bool enableApiLogging = true;\n static const bool debugMode = bool.fromEnvironment(\n 'DEBUG',\n defaultValue: false,\n );\n\n static void printConfigInfo() {\n if (kDebugMode) {\n print('=== 应用配置 ===');\n print('环境: ${isProduction ? \"生产\" : \"开发\"}');\n print('平台: ${isWeb ? \"Web\" : \"Native\"}');\n print('API URL: $currentBackendUrl');\n print('================');\n }\n }\n\n static void printCurrentConfig() => printConfigInfo();\n}\n"}, "1e24c83bcf0057378ada80518e15ac5bd729caae": {"t": 1785757983.5859025, "n": 7660, "h": "3285f618b54939c3", "c": "import 'package:flutter/foundation.dart';\n\n/// 应用配置 - 统一管理所有配置项\nclass AppConfig {\n AppConfig._();\n\n static final AppConfig instance = AppConfig._();\n\n // 环境配置\n static const String environment = String.fromEnvironment(\n 'ENV',\n defaultValue: 'production',\n );\n\n static bool get isProduction {\n if (kIsWeb) {\n final currentUrl = Uri.base.toString();\n if (currentUrl.contains('fabushi-flutter-web-dev') ||\n currentUrl.contains('localhost')) {\n return false;\n }\n if (currentUrl.contains('fabushi-flutter-web-prod')) {\n return true;\n }\n }\n return environment == 'production';\n }\n\n static bool get isDevelopment => !isProduction;\n static bool get isStaging => environment == 'staging';\n static bool get isWeb => kIsWeb;\n\n // API配置\n static const String configuredApiBaseUrl = String.fromEnvironment(\n 'API_BASE_URL',\n defaultValue: '',\n );\n static const String primaryBackendUrl = 'https://api.ombhrum.com';\n static const String cloudflareWorkerProdUrl = 'https://api.ombhrum.com';\n static const String cloudflareWorkerDevUrl =\n 'https://fabushi-flutter-web-dev.bhrumom.workers.dev';\n static const String localDevUrl = 'http://localhost:8787';\n static const String publicWebUrl = 'https://flutter.ombhrum.com';\n\n static String get currentBackendUrl {\n if (configuredApiBaseUrl.isNotEmpty) {\n return configuredApiBaseUrl;\n }\n if (kIsWeb) {\n final currentUrl = Uri.base;\n final host = currentUrl.host;\n if (host.contains('localhost')) {\n return localDevUrl;\n }\n if (host.contains('fabushi-flutter-web-dev')) {\n return cloudflareWorkerDevUrl;\n }\n if (host.contains('fabushi-flutter-web-prod')) {\n return cloudflareWorkerProdUrl;\n }\n }\n return primaryBackendUrl;\n }\n\n static String get apiUrl => currentBackendUrl;\n\n // 应用信息\n static const String appName = '大乘';\n static const String appVersion = '1.0.0';\n static const int appBuildNumber = 1;\n\n // 功能开关\n static const bool enableFirebaseAuth = true;\n static const bool enableAlipay = true;\n static const bool enableAppleIAP = true;\n static const bool enableVideoFeed = true;\n static bool get enableDebugMode => !isProduction;\n\n // 传输配置\n static const int fileChunkSize = 1024;\n static const int maxRetryCount = 3;\n static const int timeoutDuration = 5000;\n static const int maxConcurrentTransfers = 5;\n\n // 缓存配置\n static const int cacheMaxAge = 7;\n static const int maxCacheSize = 100;\n\n // 分页配置\n static const int defaultPageSize = 20;\n static const int maxPageSize = 100;\n\n // 超时配置\n static final Duration requestTimeout = const Duration(seconds: 30);\n static final Duration connectTimeout = const Duration(seconds: 10);\n\n // 重试配置\n static const int maxRetries = 3;\n static final Duration retryDelay = const Duration(seconds: 1);\n\n // 日志配置\n static const bool enableLogging = true;\n static bool get enableNetworkLogging => !isProduction;\n static bool get enablePerformanceLogging => !isProduction;\n\n // 存储键名\n static const String tokenStorageKey = 'auth_token';\n static const String userInfoStorageKey = 'user_info';\n static const String backendUrlStorageKey = 'backend_url';\n static const String testModeStorageKey = 'test_mode';\n\n // API端点\n static String get loginUrl => '$currentBackendUrl/api/auth/login';\n static String get registerUrl => '$currentBackendUrl/api/auth/register';\n static String get verifyUrl => '$currentBackendUrl/api/auth/verify';\n static String get logoutUrl => '$currentBackendUrl/api/auth/logout';\n static String get deleteAccountUrl => '$currentBackendUrl/api/auth/delete';\n static String get sendVerificationCodeUrl =>\n '$currentBackendUrl/api/auth/send-verification-code';\n static String get verifyCodeUrl => '$currentBackendUrl/api/auth/verify-code';\n static String get forgotPasswordUrl =>\n '$currentBackendUrl/api/auth/forgot-password';\n static String get resetPasswordUrl =>\n '$currentBackendUrl/api/auth/reset-password';\n static String get userInfoUrl => '$currentBackendUrl/api/auth/user-info';\n static String get bindEmailUrl => '$currentBackendUrl/api/auth/bind-email';\n\n static String get alipayCreateOrderUrl =>\n '$currentBackendUrl/api/alipay/create-order';\n static String get alipayQueryOrderUrl =>\n '$currentBackendUrl/api/alipay/query-order';\n static String get alipayMembershipStatusUrl =>\n '$currentBackendUrl/api/alipay/check-membership';\n\n static String get stripeMembershipStatusUrl =>\n '$currentBackendUrl/api/stripe/membership-status';\n static String get stripeCreateSubscriptionUrl =>\n '$currentBackendUrl/api/stripe/create-subscription';\n static String get stripeSessionStatusUrl =>\n '$currentBackendUrl/api/stripe/session-status';\n\n static String get appleVerifyReceiptUrl =>\n '$currentBackendUrl/api/apple/verify-receipt';\n\n static String get adminCheckStatusUrl =>\n '$currentBackendUrl/api/admin/check-status';\n static String get adminCreateRedeemCodeUrl =>\n '$currentBackendUrl/api/admin/create-redeem-code';\n static String get adminRedeemCodesUrl =>\n '$currentBackendUrl/api/admin/redeem-codes';\n static String get adminUseRedeemCodeUrl =>\n '$currentBackendUrl/api/admin/use-redeem-code';\n static String get adminPurchaseHistoryUrl =>\n '$currentBackendUrl/api/admin/purchase-history';\n static String get adminRedeemHistoryUrl =>\n '$currentBackendUrl/api/admin/redeem-history';\n\n static String get leaderboardUrl => '$currentBackendUrl/api/leaderboard';\n static String get updateTransferDataUrl =>\n '$currentBackendUrl/api/leaderboard/update';\n\n static String get healthCheckUrl => '$currentBackendUrl/health';\n\n // iOS 后台保活静音音频\n static String get silenceAudioUrl =>\n '$currentBackendUrl/static/audio/silence.mp3';\n\n // 3D 佛像模型配置\n // 如果 R2 上需要切换到新的对象键,优先改这里,便于强制绕开旧缓存。\n static const String buddhaModelAssetPath = 'models/buddha_model.model';\n // Android 使用 three_dart 直接渲染 GLB;iOS 继续使用 flutter_scene .model。\n static const String legacyBuddhaGlbAssetPath = 'models/佛像模型.glb';\n static const String androidThreeBuddhaGlbAssetPath =\n 'web/assets/models/佛像模型.glb';\n static String get legacyBuddhaGlbUrl {\n final encodedPath = legacyBuddhaGlbAssetPath\n .split('/')\n .map(Uri.encodeComponent)\n .join('/');\n return '$publicWebUrl/assets/$encodedPath';\n }\n\n // 当前线上正确模型明显大于 48MB,小于该阈值视为误传/降质文件。\n static const int minBuddhaModelSizeBytes = 100 * 1024 * 1024;\n static const int minBuddhaGlbSizeBytes = 10 * 1024 * 1024;\n\n // 请求头\n static Map<String, String> get defaultHeaders => {\n 'Content-Type': 'application/json',\n 'Accept': 'application/json',\n 'User-Agent': 'FabushiApp/${isWeb ? \"Web\" : \"Mobile\"}',\n };\n\n // 错误消息\n static const Map<String, String> errorMessages = {\n 'network_error': '网络连接失败,请检查网络设置',\n 'server_error': '服务器错误,请稍后重试',\n 'timeout_error': '请求超时,请检查网络连接',\n 'auth_error': '认证失败,请重新登录',\n 'permission_error': '权限不足',\n 'validation_error': '数据验证失败',\n 'unknown_error': '未知错误,请联系客服',\n };\n\n // 备用地址\n static List<String> get fallbackUrls {\n return [primaryBackendUrl];\n }\n\n // 调试配置\n static const bool enableApiLogging = true;\n static const bool debugMode = bool.fromEnvironment(\n 'DEBUG',\n defaultValue: false,\n );\n\n static void printConfigInfo() {\n if (kDebugMode) {\n print('=== 应用配置 ===');\n print('环境: ${isProduction ? \"生产\" : \"开发\"}');\n print('平台: ${isWeb ? \"Web\" : \"Native\"}');\n print('API URL: $currentBackendUrl');\n print('================');\n }\n }\n\n static void printCurrentConfig() => printConfigInfo();\n}\n"}, "56c0652a910da067b046222d815b3a9e9da158e2": {"t": 1785757986.0757053, "n": 7672, "h": "45c9895c4b637634", "c": "import 'package:flutter/foundation.dart';\n\n/// 应用配置 - 统一管理所有配置项\nclass AppConfig {\n AppConfig._();\n\n static final AppConfig instance = AppConfig._();\n\n // 环境配置\n static const String environment = String.fromEnvironment(\n 'ENV',\n defaultValue: 'production',\n );\n\n static bool get isProduction {\n if (kIsWeb) {\n final currentUrl = Uri.base.toString();\n if (currentUrl.contains('fabushi-flutter-web-dev') ||\n currentUrl.contains('localhost')) {\n return false;\n }\n if (currentUrl.contains('fabushi-flutter-web-prod')) {\n return true;\n }\n }\n return environment == 'production';\n }\n\n static bool get isDevelopment => !isProduction;\n static bool get isStaging => environment == 'staging';\n static bool get isWeb => kIsWeb;\n\n // API配置\n static const String configuredApiBaseUrl = String.fromEnvironment(\n 'API_BASE_URL',\n defaultValue: '',\n );\n static const String primaryBackendUrl = 'https://api.ombhrum.com';\n static const String cloudflareWorkerProdUrl = 'https://api.ombhrum.com';\n static const String cloudflareWorkerDevUrl =\n 'https://fabushi-flutter-web-dev.bhrumom.workers.dev';\n static const String localDevUrl = 'http://localhost:8787';\n static const String publicWebUrl = 'https://flutter.ombhrum.com';\n\n static String get currentBackendUrl {\n if (configuredApiBaseUrl.isNotEmpty) {\n return configuredApiBaseUrl;\n }\n if (kIsWeb) {\n final currentUrl = Uri.base;\n final host = currentUrl.host;\n if (host.contains('localhost')) {\n return localDevUrl;\n }\n if (host.contains('fabushi-flutter-web-dev')) {\n return cloudflareWorkerDevUrl;\n }\n if (host.contains('fabushi-flutter-web-prod')) {\n return cloudflareWorkerProdUrl;\n }\n }\n return primaryBackendUrl;\n }\n\n static String get apiUrl => currentBackendUrl;\n\n // 应用信息\n static const String appName = '大乘';\n static const String appVersion = '1.0.0';\n static const int appBuildNumber = 1;\n\n // 功能开关\n static const bool enableFirebaseAuth = true;\n static const bool enableAlipay = true;\n static const bool enableAppleIAP = true;\n static const bool enableVideoFeed = true;\n static bool get enableDebugMode => !isProduction;\n\n // 传输配置\n static const int fileChunkSize = 1024;\n static const int maxRetryCount = 3;\n static const int timeoutDuration = 5000;\n static const int maxConcurrentTransfers = 5;\n\n // 缓存配置\n static const int cacheMaxAge = 7;\n static const int maxCacheSize = 100;\n\n // 分页配置\n static const int defaultPageSize = 20;\n static const int maxPageSize = 100;\n\n // 超时配置\n static final Duration requestTimeout = const Duration(seconds: 30);\n static final Duration connectTimeout = const Duration(seconds: 10);\n\n // 重试配置\n static const int maxRetries = 3;\n static final Duration retryDelay = const Duration(seconds: 1);\n\n // 日志配置\n static const bool enableLogging = true;\n static bool get enableNetworkLogging => !isProduction;\n static bool get enablePerformanceLogging => !isProduction;\n\n // 存储键名\n static const String tokenStorageKey = 'auth_token';\n static const String userInfoStorageKey = 'user_info';\n static const String backendUrlStorageKey = 'backend_url';\n static const String testModeStorageKey = 'test_mode';\n\n // API端点\n static String get loginUrl => '$currentBackendUrl/api/auth/login';\n static String get registerUrl => '$currentBackendUrl/api/auth/register';\n static String get verifyUrl => '$currentBackendUrl/api/auth/verify';\n static String get logoutUrl => '$currentBackendUrl/api/auth/logout';\n static String get deleteAccountUrl => '$currentBackendUrl/api/auth/delete';\n static String get sendVerificationCodeUrl =>\n '$currentBackendUrl/api/auth/send-verification-code';\n static String get verifyCodeUrl => '$currentBackendUrl/api/auth/verify-code';\n static String get forgotPasswordUrl =>\n '$currentBackendUrl/api/auth/forgot-password';\n static String get resetPasswordUrl =>\n '$currentBackendUrl/api/auth/reset-password';\n static String get userInfoUrl => '$currentBackendUrl/api/auth/user-info';\n static String get bindEmailUrl => '$currentBackendUrl/api/auth/bind-email';\n\n static String get alipayCreateOrderUrl =>\n '$currentBackendUrl/api/alipay/create-order';\n static String get alipayQueryOrderUrl =>\n '$currentBackendUrl/api/alipay/query-order';\n static String get alipayMembershipStatusUrl =>\n '$currentBackendUrl/api/alipay/check-membership';\n\n static String get stripeMembershipStatusUrl =>\n '$currentBackendUrl/api/stripe/membership-status';\n static String get stripeCreateSubscriptionUrl =>\n '$currentBackendUrl/api/stripe/create-subscription';\n static String get stripeSessionStatusUrl =>\n '$currentBackendUrl/api/stripe/session-status';\n\n static String get appleVerifyReceiptUrl =>\n '$currentBackendUrl/api/apple/verify-receipt';\n\n static String get adminCheckStatusUrl =>\n '$currentBackendUrl/api/admin/check-status';\n static String get adminCreateRedeemCodeUrl =>\n '$currentBackendUrl/api/admin/create-redeem-code';\n static String get adminRedeemCodesUrl =>\n '$currentBackendUrl/api/admin/redeem-codes';\n static String get adminUseRedeemCodeUrl =>\n '$currentBackendUrl/api/admin/use-redeem-code';\n static String get adminPurchaseHistoryUrl =>\n '$currentBackendUrl/api/admin/purchase-history';\n static String get adminRedeemHistoryUrl =>\n '$currentBackendUrl/api/admin/redeem-history';\n\n static String get leaderboardUrl => '$currentBackendUrl/api/leaderboard';\n static String get updateTransferDataUrl =>\n '$currentBackendUrl/api/leaderboard/update';\n\n static String get healthCheckUrl => '$currentBackendUrl/health';\n\n // iOS 后台保活静音音频\n static String get silenceAudioUrl =>\n '$currentBackendUrl/static/audio/silence.mp3';\n\n // 3D 佛像模型配置\n // 如果 R2 上需要切换到新的对象键,优先改这里,便于强制绕开旧缓存。\n static const String buddhaModelAssetPath = 'models/buddha_model.model';\n // 兼容旧链路:移动端在 .model 无法渲染时,可退回到官网静态资源上的远端 GLB。\n static const String legacyBuddhaGlbAssetPath = 'models/佛像模型.glb';\n static const String bundledBuddhaFallbackHtmlAssetPath =\n 'assets/buddha_fallback/index.html';\n static const String bundledBuddhaGlbAssetPath = 'web/assets/models/佛像模型.glb';\n static String get legacyBuddhaGlbUrl {\n final encodedPath = legacyBuddhaGlbAssetPath\n .split('/')\n .map(Uri.encodeComponent)\n .join('/');\n return '$publicWebUrl/assets/$encodedPath';\n }\n\n // 当前线上正确模型明显大于 48MB,小于该阈值视为误传/降质文件。\n static const int minBuddhaModelSizeBytes = 100 * 1024 * 1024;\n\n // 请求头\n static Map<String, String> get defaultHeaders => {\n 'Content-Type': 'application/json',\n 'Accept': 'application/json',\n 'User-Agent': 'FabushiApp/${isWeb ? \"Web\" : \"Mobile\"}',\n };\n\n // 错误消息\n static const Map<String, String> errorMessages = {\n 'network_error': '网络连接失败,请检查网络设置',\n 'server_error': '服务器错误,请稍后重试',\n 'timeout_error': '请求超时,请检查网络连接',\n 'auth_error': '认证失败,请重新登录',\n 'permission_error': '权限不足',\n 'validation_error': '数据验证失败',\n 'unknown_error': '未知错误,请联系客服',\n };\n\n // 备用地址\n static List<String> get fallbackUrls {\n return [primaryBackendUrl];\n }\n\n // 调试配置\n static const bool enableApiLogging = true;\n static const bool debugMode = bool.fromEnvironment(\n 'DEBUG',\n defaultValue: false,\n );\n\n static void printConfigInfo() {\n if (kDebugMode) {\n print('=== 应用配置 ===');\n print('环境: ${isProduction ? \"生产\" : \"开发\"}');\n print('平台: ${isWeb ? \"Web\" : \"Native\"}');\n print('API URL: $currentBackendUrl');\n print('================');\n }\n }\n\n static void printCurrentConfig() => printConfigInfo();\n}\n"}, "e1968fe70e4696301d059ac88158686d93c3d80f": {"t": 1785757988.0680478, "n": 7490, "h": "f23330d7ff1625e3", "c": "import 'package:flutter/foundation.dart';\n\n/// 应用配置 - 统一管理所有配置项\nclass AppConfig {\n AppConfig._();\n\n static final AppConfig instance = AppConfig._();\n\n // 环境配置\n static const String environment = String.fromEnvironment(\n 'ENV',\n defaultValue: 'production',\n );\n\n static bool get isProduction {\n if (kIsWeb) {\n final currentUrl = Uri.base.toString();\n if (currentUrl.contains('fabushi-flutter-web-dev') ||\n currentUrl.contains('localhost')) {\n return false;\n }\n if (currentUrl.contains('fabushi-flutter-web-prod')) {\n return true;\n }\n }\n return environment == 'production';\n }\n\n static bool get isDevelopment => !isProduction;\n static bool get isStaging => environment == 'staging';\n static bool get isWeb => kIsWeb;\n\n // API配置\n static const String configuredApiBaseUrl = String.fromEnvironment(\n 'API_BASE_URL',\n defaultValue: '',\n );\n static const String primaryBackendUrl = 'https://api.ombhrum.com';\n static const String cloudflareWorkerProdUrl = 'https://api.ombhrum.com';\n static const String cloudflareWorkerDevUrl =\n 'https://fabushi-flutter-web-dev.bhrumom.workers.dev';\n static const String localDevUrl = 'http://localhost:8787';\n static const String publicWebUrl = 'https://flutter.ombhrum.com';\n\n static String get currentBackendUrl {\n if (configuredApiBaseUrl.isNotEmpty) {\n return configuredApiBaseUrl;\n }\n if (kIsWeb) {\n final currentUrl = Uri.base;\n final host = currentUrl.host;\n if (host.contains('localhost')) {\n return localDevUrl;\n }\n if (host.contains('fabushi-flutter-web-dev')) {\n return cloudflareWorkerDevUrl;\n }\n if (host.contains('fabushi-flutter-web-prod')) {\n return cloudflareWorkerProdUrl;\n }\n }\n return primaryBackendUrl;\n }\n\n static String get apiUrl => currentBackendUrl;\n\n // 应用信息\n static const String appName = '大乘';\n static const String appVersion = '1.0.0';\n static const int appBuildNumber = 1;\n\n // 功能开关\n static const bool enableFirebaseAuth = true;\n static const bool enableAlipay = true;\n static const bool enableAppleIAP = true;\n static const bool enableVideoFeed = true;\n static bool get enableDebugMode => !isProduction;\n\n // 传输配置\n static const int fileChunkSize = 1024;\n static const int maxRetryCount = 3;\n static const int timeoutDuration = 5000;\n static const int maxConcurrentTransfers = 5;\n\n // 缓存配置\n static const int cacheMaxAge = 7;\n static const int maxCacheSize = 100;\n\n // 分页配置\n static const int defaultPageSize = 20;\n static const int maxPageSize = 100;\n\n // 超时配置\n static final Duration requestTimeout = const Duration(seconds: 30);\n static final Duration connectTimeout = const Duration(seconds: 10);\n\n // 重试配置\n static const int maxRetries = 3;\n static final Duration retryDelay = const Duration(seconds: 1);\n\n // 日志配置\n static const bool enableLogging = true;\n static bool get enableNetworkLogging => !isProduction;\n static bool get enablePerformanceLogging => !isProduction;\n\n // 存储键名\n static const String tokenStorageKey = 'auth_token';\n static const String userInfoStorageKey = 'user_info';\n static const String backendUrlStorageKey = 'backend_url';\n static const String testModeStorageKey = 'test_mode';\n\n // API端点\n static String get loginUrl => '$currentBackendUrl/api/auth/login';\n static String get registerUrl => '$currentBackendUrl/api/auth/register';\n static String get verifyUrl => '$currentBackendUrl/api/auth/verify';\n static String get logoutUrl => '$currentBackendUrl/api/auth/logout';\n static String get deleteAccountUrl => '$currentBackendUrl/api/auth/delete';\n static String get sendVerificationCodeUrl =>\n '$currentBackendUrl/api/auth/send-verification-code';\n static String get verifyCodeUrl => '$currentBackendUrl/api/auth/verify-code';\n static String get forgotPasswordUrl =>\n '$currentBackendUrl/api/auth/forgot-password';\n static String get resetPasswordUrl =>\n '$currentBackendUrl/api/auth/reset-password';\n static String get userInfoUrl => '$currentBackendUrl/api/auth/user-info';\n static String get bindEmailUrl => '$currentBackendUrl/api/auth/bind-email';\n\n static String get alipayCreateOrderUrl =>\n '$currentBackendUrl/api/alipay/create-order';\n static String get alipayQueryOrderUrl =>\n '$currentBackendUrl/api/alipay/query-order';\n static String get alipayMembershipStatusUrl =>\n '$currentBackendUrl/api/alipay/check-membership';\n\n static String get stripeMembershipStatusUrl =>\n '$currentBackendUrl/api/stripe/membership-status';\n static String get stripeCreateSubscriptionUrl =>\n '$currentBackendUrl/api/stripe/create-subscription';\n static String get stripeSessionStatusUrl =>\n '$currentBackendUrl/api/stripe/session-status';\n\n static String get appleVerifyReceiptUrl =>\n '$currentBackendUrl/api/apple/verify-receipt';\n\n static String get adminCheckStatusUrl =>\n '$currentBackendUrl/api/admin/check-status';\n static String get adminCreateRedeemCodeUrl =>\n '$currentBackendUrl/api/admin/create-redeem-code';\n static String get adminRedeemCodesUrl =>\n '$currentBackendUrl/api/admin/redeem-codes';\n static String get adminUseRedeemCodeUrl =>\n '$currentBackendUrl/api/admin/use-redeem-code';\n static String get adminPurchaseHistoryUrl =>\n '$currentBackendUrl/api/admin/purchase-history';\n static String get adminRedeemHistoryUrl =>\n '$currentBackendUrl/api/admin/redeem-history';\n\n static String get leaderboardUrl => '$currentBackendUrl/api/leaderboard';\n static String get updateTransferDataUrl =>\n '$currentBackendUrl/api/leaderboard/update';\n\n static String get healthCheckUrl => '$currentBackendUrl/health';\n\n // iOS 后台保活静音音频\n static String get silenceAudioUrl =>\n '$currentBackendUrl/static/audio/silence.mp3';\n\n // 3D 佛像模型配置\n // 如果 R2 上需要切换到新的对象键,优先改这里,便于强制绕开旧缓存。\n static const String buddhaModelAssetPath = 'models/buddha_model.model';\n // 兼容旧链路:移动端在 .model 无法渲染时,可退回到官网静态资源上的远端 GLB。\n static const String legacyBuddhaGlbAssetPath = 'models/佛像模型.glb';\n static String get legacyBuddhaGlbUrl {\n final encodedPath = legacyBuddhaGlbAssetPath\n .split('/')\n .map(Uri.encodeComponent)\n .join('/');\n return '$publicWebUrl/assets/$encodedPath';\n }\n\n // 当前线上正确模型明显大于 48MB,小于该阈值视为误传/降质文件。\n static const int minBuddhaModelSizeBytes = 100 * 1024 * 1024;\n\n // 请求头\n static Map<String, String> get defaultHeaders => {\n 'Content-Type': 'application/json',\n 'Accept': 'application/json',\n 'User-Agent': 'FabushiApp/${isWeb ? \"Web\" : \"Mobile\"}',\n };\n\n // 错误消息\n static const Map<String, String> errorMessages = {\n 'network_error': '网络连接失败,请检查网络设置',\n 'server_error': '服务器错误,请稍后重试',\n 'timeout_error': '请求超时,请检查网络连接',\n 'auth_error': '认证失败,请重新登录',\n 'permission_error': '权限不足',\n 'validation_error': '数据验证失败',\n 'unknown_error': '未知错误,请联系客服',\n };\n\n // 备用地址\n static List<String> get fallbackUrls {\n return [primaryBackendUrl];\n }\n\n // 调试配置\n static const bool enableApiLogging = true;\n static const bool debugMode = bool.fromEnvironment(\n 'DEBUG',\n defaultValue: false,\n );\n\n static void printConfigInfo() {\n if (kDebugMode) {\n print('=== 应用配置 ===');\n print('环境: ${isProduction ? \"生产\" : \"开发\"}');\n print('平台: ${isWeb ? \"Web\" : \"Native\"}');\n print('API URL: $currentBackendUrl');\n print('================');\n }\n }\n\n static void printCurrentConfig() => printConfigInfo();\n}\n"}, "ceafd2e4926b590f31ad8dcff1c943adb67acfe3": {"t": 1785757990.726552, "n": 7273, "h": "d787d434c8c6e9e4", "c": "import 'package:flutter/foundation.dart';\n\n/// 应用配置 - 统一管理所有配置项\nclass AppConfig {\n AppConfig._();\n\n static final AppConfig instance = AppConfig._();\n\n // 环境配置\n static const String environment = String.fromEnvironment(\n 'ENV',\n defaultValue: 'production',\n );\n\n static bool get isProduction {\n if (kIsWeb) {\n final currentUrl = Uri.base.toString();\n if (currentUrl.contains('fabushi-flutter-web-dev') ||\n currentUrl.contains('localhost')) {\n return false;\n }\n if (currentUrl.contains('fabushi-flutter-web-prod')) {\n return true;\n }\n }\n return environment == 'production';\n }\n\n static bool get isDevelopment => !isProduction;\n static bool get isStaging => environment == 'staging';\n static bool get isWeb => kIsWeb;\n\n // API配置\n static const String configuredApiBaseUrl = String.fromEnvironment(\n 'API_BASE_URL',\n defaultValue: '',\n );\n static const String primaryBackendUrl = 'https://api.ombhrum.com';\n static const String cloudflareWorkerProdUrl = 'https://api.ombhrum.com';\n static const String cloudflareWorkerDevUrl =\n 'https://fabushi-flutter-web-dev.bhrumom.workers.dev';\n static const String localDevUrl = 'http://localhost:8787';\n static const String publicWebUrl = 'https://flutter.ombhrum.com';\n\n static String get currentBackendUrl {\n if (configuredApiBaseUrl.isNotEmpty) {\n return configuredApiBaseUrl;\n }\n if (kIsWeb) {\n final currentUrl = Uri.base;\n final host = currentUrl.host;\n if (host.contains('localhost')) {\n return localDevUrl;\n }\n if (host.contains('fabushi-flutter-web-dev')) {\n return cloudflareWorkerDevUrl;\n }\n if (host.contains('fabushi-flutter-web-prod')) {\n return cloudflareWorkerProdUrl;\n }\n }\n return primaryBackendUrl;\n }\n\n static String get apiUrl => currentBackendUrl;\n\n // 应用信息\n static const String appName = '大乘';\n static const String appVersion = '1.0.0';\n static const int appBuildNumber = 1;\n\n // 功能开关\n static const bool enableFirebaseAuth = true;\n static const bool enableAlipay = true;\n static const bool enableAppleIAP = true;\n static const bool enableVideoFeed = true;\n static bool get enableDebugMode => !isProduction;\n\n // 传输配置\n static const int fileChunkSize = 1024;\n static const int maxRetryCount = 3;\n static const int timeoutDuration = 5000;\n static const int maxConcurrentTransfers = 5;\n\n // 缓存配置\n static const int cacheMaxAge = 7;\n static const int maxCacheSize = 100;\n\n // 分页配置\n static const int defaultPageSize = 20;\n static const int maxPageSize = 100;\n\n // 超时配置\n static final Duration requestTimeout = const Duration(seconds: 30);\n static final Duration connectTimeout = const Duration(seconds: 10);\n\n // 重试配置\n static const int maxRetries = 3;\n static final Duration retryDelay = const Duration(seconds: 1);\n\n // 日志配置\n static const bool enableLogging = true;\n static bool get enableNetworkLogging => !isProduction;\n static bool get enablePerformanceLogging => !isProduction;\n\n // 存储键名\n static const String tokenStorageKey = 'auth_token';\n static const String userInfoStorageKey = 'user_info';\n static const String backendUrlStorageKey = 'backend_url';\n static const String testModeStorageKey = 'test_mode';\n\n // API端点\n static String get loginUrl => '$currentBackendUrl/api/auth/login';\n static String get registerUrl => '$currentBackendUrl/api/auth/register';\n static String get verifyUrl => '$currentBackendUrl/api/auth/verify';\n static String get logoutUrl => '$currentBackendUrl/api/auth/logout';\n static String get deleteAccountUrl => '$currentBackendUrl/api/auth/delete';\n static String get sendVerificationCodeUrl =>\n '$currentBackendUrl/api/auth/send-verification-code';\n static String get verifyCodeUrl => '$currentBackendUrl/api/auth/verify-code';\n static String get forgotPasswordUrl =>\n '$currentBackendUrl/api/auth/forgot-password';\n static String get resetPasswordUrl =>\n '$currentBackendUrl/api/auth/reset-password';\n static String get userInfoUrl => '$currentBackendUrl/api/auth/user-info';\n static String get bindEmailUrl => '$currentBackendUrl/api/auth/bind-email';\n\n static String get alipayCreateOrderUrl =>\n '$currentBackendUrl/api/alipay/create-order';\n static String get alipayQueryOrderUrl =>\n '$currentBackendUrl/api/alipay/query-order';\n static String get alipayMembershipStatusUrl =>\n '$currentBackendUrl/api/alipay/check-membership';\n\n static String get stripeMembershipStatusUrl =>\n '$currentBackendUrl/api/stripe/membership-status';\n static String get stripeCreateSubscriptionUrl =>\n '$currentBackendUrl/api/stripe/create-subscription';\n static String get stripeSessionStatusUrl =>\n '$currentBackendUrl/api/stripe/session-status';\n\n static String get appleVerifyReceiptUrl =>\n '$currentBackendUrl/api/apple/verify-receipt';\n\n static String get adminCheckStatusUrl =>\n '$currentBackendUrl/api/admin/check-status';\n static String get adminCreateRedeemCodeUrl =>\n '$currentBackendUrl/api/admin/create-redeem-code';\n static String get adminRedeemCodesUrl =>\n '$currentBackendUrl/api/admin/redeem-codes';\n static String get adminUseRedeemCodeUrl =>\n '$currentBackendUrl/api/admin/use-redeem-code';\n static String get adminPurchaseHistoryUrl =>\n '$currentBackendUrl/api/admin/purchase-history';\n static String get adminRedeemHistoryUrl =>\n '$currentBackendUrl/api/admin/redeem-history';\n\n static String get leaderboardUrl => '$currentBackendUrl/api/leaderboard';\n static String get updateTransferDataUrl =>\n '$currentBackendUrl/api/leaderboard/update';\n\n static String get healthCheckUrl => '$currentBackendUrl/health';\n\n // iOS 后台保活静音音频\n static String get silenceAudioUrl =>\n '$currentBackendUrl/static/audio/silence.mp3';\n\n // 3D 佛像模型配置\n // 如果 R2 上需要切换到新的对象键,优先改这里,便于强制绕开旧缓存。\n static const String buddhaModelAssetPath = 'models/buddha_model.model';\n // 兼容旧链路:移动端在 .model 缺失时,可退回到仍在线上的 legacy GLB。\n static const String legacyBuddhaGlbAssetPath = 'models/佛像模型.glb';\n // 当前线上正确模型明显大于 48MB,小于该阈值视为误传/降质文件。\n static const int minBuddhaModelSizeBytes = 100 * 1024 * 1024;\n\n // 请求头\n static Map<String, String> get defaultHeaders => {\n 'Content-Type': 'application/json',\n 'Accept': 'application/json',\n 'User-Agent': 'FabushiApp/${isWeb ? \"Web\" : \"Mobile\"}',\n };\n\n // 错误消息\n static const Map<String, String> errorMessages = {\n 'network_error': '网络连接失败,请检查网络设置',\n 'server_error': '服务器错误,请稍后重试',\n 'timeout_error': '请求超时,请检查网络连接',\n 'auth_error': '认证失败,请重新登录',\n 'permission_error': '权限不足',\n 'validation_error': '数据验证失败',\n 'unknown_error': '未知错误,请联系客服',\n };\n\n // 备用地址\n static List<String> get fallbackUrls {\n return [primaryBackendUrl];\n }\n\n // 调试配置\n static const bool enableApiLogging = true;\n static const bool debugMode = bool.fromEnvironment(\n 'DEBUG',\n defaultValue: false,\n );\n\n static void printConfigInfo() {\n if (kDebugMode) {\n print('=== 应用配置 ===');\n print('环境: ${isProduction ? \"生产\" : \"开发\"}');\n print('平台: ${isWeb ? \"Web\" : \"Native\"}');\n print('API URL: $currentBackendUrl');\n print('================');\n }\n }\n\n static void printCurrentConfig() => printConfigInfo();\n}\n"}, "3e18395a215efd0b5e5c6f83e6cfd8ea7891797e": {"t": 1785757992.83336, "n": 7156, "h": "e7e18530a0f49e1f", "c": "import 'package:flutter/foundation.dart';\n\n/// 应用配置 - 统一管理所有配置项\nclass AppConfig {\n AppConfig._();\n\n static final AppConfig instance = AppConfig._();\n\n // 环境配置\n static const String environment = String.fromEnvironment(\n 'ENV',\n defaultValue: 'production',\n );\n\n static bool get isProduction {\n if (kIsWeb) {\n final currentUrl = Uri.base.toString();\n if (currentUrl.contains('fabushi-flutter-web-dev') ||\n currentUrl.contains('localhost')) {\n return false;\n }\n if (currentUrl.contains('fabushi-flutter-web-prod')) {\n return true;\n }\n }\n return environment == 'production';\n }\n\n static bool get isDevelopment => !isProduction;\n static bool get isStaging => environment == 'staging';\n static bool get isWeb => kIsWeb;\n\n // API配置\n static const String configuredApiBaseUrl = String.fromEnvironment(\n 'API_BASE_URL',\n defaultValue: '',\n );\n static const String primaryBackendUrl = 'https://api.ombhrum.com';\n static const String cloudflareWorkerProdUrl = 'https://api.ombhrum.com';\n static const String cloudflareWorkerDevUrl =\n 'https://fabushi-flutter-web-dev.bhrumom.workers.dev';\n static const String localDevUrl = 'http://localhost:8787';\n static const String publicWebUrl = 'https://flutter.ombhrum.com';\n\n static String get currentBackendUrl {\n if (configuredApiBaseUrl.isNotEmpty) {\n return configuredApiBaseUrl;\n }\n if (kIsWeb) {\n final currentUrl = Uri.base;\n final host = currentUrl.host;\n if (host.contains('localhost')) {\n return localDevUrl;\n }\n if (host.contains('fabushi-flutter-web-dev')) {\n return cloudflareWorkerDevUrl;\n }\n if (host.contains('fabushi-flutter-web-prod')) {\n return cloudflareWorkerProdUrl;\n }\n }\n return primaryBackendUrl;\n }\n\n static String get apiUrl => currentBackendUrl;\n\n // 应用信息\n static const String appName = '大乘';\n static const String appVersion = '1.0.0';\n static const int appBuildNumber = 1;\n\n // 功能开关\n static const bool enableFirebaseAuth = true;\n static const bool enableAlipay = true;\n static const bool enableAppleIAP = true;\n static const bool enableVideoFeed = true;\n static bool get enableDebugMode => !isProduction;\n\n // 传输配置\n static const int fileChunkSize = 1024;\n static const int maxRetryCount = 3;\n static const int timeoutDuration = 5000;\n static const int maxConcurrentTransfers = 5;\n\n // 缓存配置\n static const int cacheMaxAge = 7;\n static const int maxCacheSize = 100;\n\n // 分页配置\n static const int defaultPageSize = 20;\n static const int maxPageSize = 100;\n\n // 超时配置\n static final Duration requestTimeout = const Duration(seconds: 30);\n static final Duration connectTimeout = const Duration(seconds: 10);\n\n // 重试配置\n static const int maxRetries = 3;\n static final Duration retryDelay = const Duration(seconds: 1);\n\n // 日志配置\n static const bool enableLogging = true;\n static bool get enableNetworkLogging => !isProduction;\n static bool get enablePerformanceLogging => !isProduction;\n\n // 存储键名\n static const String tokenStorageKey = 'auth_token';\n static const String userInfoStorageKey = 'user_info';\n static const String backendUrlStorageKey = 'backend_url';\n static const String testModeStorageKey = 'test_mode';\n\n // API端点\n static String get loginUrl => '$currentBackendUrl/api/auth/login';\n static String get registerUrl => '$currentBackendUrl/api/auth/register';\n static String get verifyUrl => '$currentBackendUrl/api/auth/verify';\n static String get logoutUrl => '$currentBackendUrl/api/auth/logout';\n static String get deleteAccountUrl => '$currentBackendUrl/api/auth/delete';\n static String get sendVerificationCodeUrl =>\n '$currentBackendUrl/api/auth/send-verification-code';\n static String get verifyCodeUrl => '$currentBackendUrl/api/auth/verify-code';\n static String get forgotPasswordUrl =>\n '$currentBackendUrl/api/auth/forgot-password';\n static String get resetPasswordUrl =>\n '$currentBackendUrl/api/auth/reset-password';\n static String get userInfoUrl => '$currentBackendUrl/api/auth/user-info';\n static String get bindEmailUrl => '$currentBackendUrl/api/auth/bind-email';\n\n static String get alipayCreateOrderUrl =>\n '$currentBackendUrl/api/alipay/create-order';\n static String get alipayQueryOrderUrl =>\n '$currentBackendUrl/api/alipay/query-order';\n static String get alipayMembershipStatusUrl =>\n '$currentBackendUrl/api/alipay/check-membership';\n\n static String get stripeMembershipStatusUrl =>\n '$currentBackendUrl/api/stripe/membership-status';\n static String get stripeCreateSubscriptionUrl =>\n '$currentBackendUrl/api/stripe/create-subscription';\n static String get stripeSessionStatusUrl =>\n '$currentBackendUrl/api/stripe/session-status';\n\n static String get appleVerifyReceiptUrl =>\n '$currentBackendUrl/api/apple/verify-receipt';\n\n static String get adminCheckStatusUrl =>\n '$currentBackendUrl/api/admin/check-status';\n static String get adminCreateRedeemCodeUrl =>\n '$currentBackendUrl/api/admin/create-redeem-code';\n static String get adminRedeemCodesUrl =>\n '$currentBackendUrl/api/admin/redeem-codes';\n static String get adminUseRedeemCodeUrl =>\n '$currentBackendUrl/api/admin/use-redeem-code';\n static String get adminPurchaseHistoryUrl =>\n '$currentBackendUrl/api/admin/purchase-history';\n static String get adminRedeemHistoryUrl =>\n '$currentBackendUrl/api/admin/redeem-history';\n\n static String get leaderboardUrl => '$currentBackendUrl/api/leaderboard';\n static String get updateTransferDataUrl =>\n '$currentBackendUrl/api/leaderboard/update';\n\n static String get healthCheckUrl => '$currentBackendUrl/health';\n\n // iOS 后台保活静音音频\n static String get silenceAudioUrl =>\n '$currentBackendUrl/static/audio/silence.mp3';\n\n // 3D 佛像模型配置\n // 如果 R2 上需要切换到新的对象键,优先改这里,便于强制绕开旧缓存。\n static const String buddhaModelAssetPath = 'models/buddha_model.model';\n // 当前线上正确模型明显大于 48MB,小于该阈值视为误传/降质文件。\n static const int minBuddhaModelSizeBytes = 100 * 1024 * 1024;\n\n // 请求头\n static Map<String, String> get defaultHeaders => {\n 'Content-Type': 'application/json',\n 'Accept': 'application/json',\n 'User-Agent': 'FabushiApp/${isWeb ? \"Web\" : \"Mobile\"}',\n };\n\n // 错误消息\n static const Map<String, String> errorMessages = {\n 'network_error': '网络连接失败,请检查网络设置',\n 'server_error': '服务器错误,请稍后重试',\n 'timeout_error': '请求超时,请检查网络连接',\n 'auth_error': '认证失败,请重新登录',\n 'permission_error': '权限不足',\n 'validation_error': '数据验证失败',\n 'unknown_error': '未知错误,请联系客服',\n };\n\n // 备用地址\n static List<String> get fallbackUrls {\n return [primaryBackendUrl];\n }\n\n // 调试配置\n static const bool enableApiLogging = true;\n static const bool debugMode = bool.fromEnvironment(\n 'DEBUG',\n defaultValue: false,\n );\n\n static void printConfigInfo() {\n if (kDebugMode) {\n print('=== 应用配置 ===');\n print('环境: ${isProduction ? \"生产\" : \"开发\"}');\n print('平台: ${isWeb ? \"Web\" : \"Native\"}');\n print('API URL: $currentBackendUrl');\n print('================');\n }\n }\n\n static void printCurrentConfig() => printConfigInfo();\n}\n"}, "6141c458c7146ef77a67ebcde0450529c5ab6c9f": {"t": 1785757994.949445, "n": 6922, "h": "778049ac15905f13", "c": "import 'package:flutter/foundation.dart';\n\n/// 应用配置 - 统一管理所有配置项\nclass AppConfig {\n AppConfig._();\n\n static final AppConfig instance = AppConfig._();\n\n // 环境配置\n static const String environment = String.fromEnvironment(\n 'ENV',\n defaultValue: 'production',\n );\n\n static bool get isProduction {\n if (kIsWeb) {\n final currentUrl = Uri.base.toString();\n if (currentUrl.contains('fabushi-flutter-web-dev') ||\n currentUrl.contains('localhost')) {\n return false;\n }\n if (currentUrl.contains('fabushi-flutter-web-prod')) {\n return true;\n }\n }\n return environment == 'production';\n }\n\n static bool get isDevelopment => !isProduction;\n static bool get isStaging => environment == 'staging';\n static bool get isWeb => kIsWeb;\n\n // API配置\n static const String primaryBackendUrl = 'https://flutter.ombhrum.com';\n static const String cloudflareWorkerProdUrl = 'https://flutter.ombhrum.com';\n static const String cloudflareWorkerDevUrl =\n 'https://fabushi-flutter-web-dev.bhrumom.workers.dev';\n static const String localDevUrl = 'http://localhost:8787';\n\n static String get currentBackendUrl {\n if (kIsWeb) {\n final currentUrl = Uri.base;\n final host = currentUrl.host;\n if (host.contains('localhost')) {\n return localDevUrl;\n }\n if (host.contains('fabushi-flutter-web-dev')) {\n return '${currentUrl.scheme}://${currentUrl.authority}';\n }\n if (host.contains('fabushi-flutter-web-prod')) {\n return cloudflareWorkerProdUrl;\n }\n }\n return primaryBackendUrl;\n }\n\n static String get apiUrl => currentBackendUrl;\n\n // 应用信息\n static const String appName = '大乘';\n static const String appVersion = '1.0.0';\n static const int appBuildNumber = 1;\n\n // 功能开关\n static const bool enableFirebaseAuth = true;\n static const bool enableAlipay = true;\n static const bool enableAppleIAP = true;\n static const bool enableVideoFeed = true;\n static bool get enableDebugMode => !isProduction;\n\n // 传输配置\n static const int fileChunkSize = 1024;\n static const int maxRetryCount = 3;\n static const int timeoutDuration = 5000;\n static const int maxConcurrentTransfers = 5;\n\n // 缓存配置\n static const int cacheMaxAge = 7;\n static const int maxCacheSize = 100;\n\n // 分页配置\n static const int defaultPageSize = 20;\n static const int maxPageSize = 100;\n\n // 超时配置\n static final Duration requestTimeout = const Duration(seconds: 30);\n static final Duration connectTimeout = const Duration(seconds: 10);\n\n // 重试配置\n static const int maxRetries = 3;\n static final Duration retryDelay = const Duration(seconds: 1);\n\n // 日志配置\n static const bool enableLogging = true;\n static bool get enableNetworkLogging => !isProduction;\n static bool get enablePerformanceLogging => !isProduction;\n\n // 存储键名\n static const String tokenStorageKey = 'auth_token';\n static const String userInfoStorageKey = 'user_info';\n static const String backendUrlStorageKey = 'backend_url';\n static const String testModeStorageKey = 'test_mode';\n\n // API端点\n static String get loginUrl => '$currentBackendUrl/api/auth/login';\n static String get registerUrl => '$currentBackendUrl/api/auth/register';\n static String get verifyUrl => '$currentBackendUrl/api/auth/verify';\n static String get logoutUrl => '$currentBackendUrl/api/auth/logout';\n static String get deleteAccountUrl => '$currentBackendUrl/api/auth/delete';\n static String get sendVerificationCodeUrl =>\n '$currentBackendUrl/api/auth/send-verification-code';\n static String get verifyCodeUrl => '$currentBackendUrl/api/auth/verify-code';\n static String get forgotPasswordUrl =>\n '$currentBackendUrl/api/auth/forgot-password';\n static String get resetPasswordUrl =>\n '$currentBackendUrl/api/auth/reset-password';\n static String get userInfoUrl => '$currentBackendUrl/api/auth/user-info';\n static String get bindEmailUrl => '$currentBackendUrl/api/auth/bind-email';\n\n static String get alipayCreateOrderUrl =>\n '$currentBackendUrl/api/alipay/create-order';\n static String get alipayQueryOrderUrl =>\n '$currentBackendUrl/api/alipay/query-order';\n static String get alipayMembershipStatusUrl =>\n '$currentBackendUrl/api/alipay/check-membership';\n\n static String get stripeMembershipStatusUrl =>\n '$currentBackendUrl/api/stripe/membership-status';\n static String get stripeCreateSubscriptionUrl =>\n '$currentBackendUrl/api/stripe/create-subscription';\n static String get stripeSessionStatusUrl =>\n '$currentBackendUrl/api/stripe/session-status';\n\n static String get appleVerifyReceiptUrl =>\n '$currentBackendUrl/api/apple/verify-receipt';\n\n static String get adminCheckStatusUrl =>\n '$currentBackendUrl/api/admin/check-status';\n static String get adminCreateRedeemCodeUrl =>\n '$currentBackendUrl/api/admin/create-redeem-code';\n static String get adminRedeemCodesUrl =>\n '$currentBackendUrl/api/admin/redeem-codes';\n static String get adminUseRedeemCodeUrl =>\n '$currentBackendUrl/api/admin/use-redeem-code';\n static String get adminPurchaseHistoryUrl =>\n '$currentBackendUrl/api/admin/purchase-history';\n static String get adminRedeemHistoryUrl =>\n '$currentBackendUrl/api/admin/redeem-history';\n\n static String get leaderboardUrl => '$currentBackendUrl/api/leaderboard';\n static String get updateTransferDataUrl =>\n '$currentBackendUrl/api/leaderboard/update';\n\n static String get healthCheckUrl => '$currentBackendUrl/health';\n\n // iOS 后台保活静音音频\n static String get silenceAudioUrl =>\n '$currentBackendUrl/static/audio/silence.mp3';\n\n // 3D 佛像模型配置\n // 如果 R2 上需要切换到新的对象键,优先改这里,便于强制绕开旧缓存。\n static const String buddhaModelAssetPath = 'models/buddha_model.model';\n // 当前线上正确模型明显大于 48MB,小于该阈值视为误传/降质文件。\n static const int minBuddhaModelSizeBytes = 100 * 1024 * 1024;\n\n // 请求头\n static Map<String, String> get defaultHeaders => {\n 'Content-Type': 'application/json',\n 'Accept': 'application/json',\n 'User-Agent': 'FabushiApp/${isWeb ? \"Web\" : \"Mobile\"}',\n };\n\n // 错误消息\n static const Map<String, String> errorMessages = {\n 'network_error': '网络连接失败,请检查网络设置',\n 'server_error': '服务器错误,请稍后重试',\n 'timeout_error': '请求超时,请检查网络连接',\n 'auth_error': '认证失败,请重新登录',\n 'permission_error': '权限不足',\n 'validation_error': '数据验证失败',\n 'unknown_error': '未知错误,请联系客服',\n };\n\n // 备用地址\n static List<String> get fallbackUrls {\n return [primaryBackendUrl];\n }\n\n // 调试配置\n static const bool enableApiLogging = true;\n static const bool debugMode = bool.fromEnvironment(\n 'DEBUG',\n defaultValue: false,\n );\n\n static void printConfigInfo() {\n if (kDebugMode) {\n print('=== 应用配置 ===');\n print('环境: ${isProduction ? \"生产\" : \"开发\"}');\n print('平台: ${isWeb ? \"Web\" : \"Native\"}');\n print('API URL: $currentBackendUrl');\n print('================');\n }\n }\n\n static void printCurrentConfig() => printConfigInfo();\n}\n"}, "8650f6378fcf1552a6ea70854e6be7f603dc50a6": {"t": 1785757996.7964249, "n": 6916, "h": "612ae92cdeced913", "c": "import 'package:flutter/foundation.dart';\n\n/// 应用配置 - 统一管理所有配置项\nclass AppConfig {\n AppConfig._();\n\n static final AppConfig instance = AppConfig._();\n\n // 环境配置\n static const String environment = String.fromEnvironment(\n 'ENV',\n defaultValue: 'production',\n );\n\n static bool get isProduction {\n if (kIsWeb) {\n final currentUrl = Uri.base.toString();\n if (currentUrl.contains('fabushi-flutter-web-dev') ||\n currentUrl.contains('localhost')) {\n return false;\n }\n if (currentUrl.contains('fabushi-flutter-web-prod')) {\n return true;\n }\n }\n return environment == 'production';\n }\n\n static bool get isDevelopment => !isProduction;\n static bool get isStaging => environment == 'staging';\n static bool get isWeb => kIsWeb;\n\n // API配置\n static const String primaryBackendUrl = 'https://flutter.ombhrum.com';\n static const String cloudflareWorkerProdUrl = 'https://flutter.ombhrum.com';\n static const String cloudflareWorkerDevUrl = 'https://fabushi-flutter-web-dev.bhrumom.workers.dev';\n static const String localDevUrl = 'http://localhost:8787';\n\n static String get currentBackendUrl {\n if (kIsWeb) {\n final currentUrl = Uri.base;\n final host = currentUrl.host;\n if (host.contains('localhost')) {\n return localDevUrl;\n }\n if (host.contains('fabushi-flutter-web-dev')) {\n return '${currentUrl.scheme}://${currentUrl.authority}';\n }\n if (host.contains('fabushi-flutter-web-prod')) {\n return cloudflareWorkerProdUrl;\n }\n }\n return primaryBackendUrl;\n }\n\n static String get apiUrl => currentBackendUrl;\n\n // 应用信息\n static const String appName = '大乘';\n static const String appVersion = '1.0.0';\n static const int appBuildNumber = 1;\n\n // 功能开关\n static const bool enableFirebaseAuth = true;\n static const bool enableAlipay = true;\n static const bool enableAppleIAP = true;\n static const bool enableVideoFeed = true;\n static bool get enableDebugMode => !isProduction;\n\n // 传输配置\n static const int fileChunkSize = 1024;\n static const int maxRetryCount = 3;\n static const int timeoutDuration = 5000;\n static const int maxConcurrentTransfers = 5;\n\n // 缓存配置\n static const int cacheMaxAge = 7;\n static const int maxCacheSize = 100;\n\n // 分页配置\n static const int defaultPageSize = 20;\n static const int maxPageSize = 100;\n\n // 超时配置\n static final Duration requestTimeout = const Duration(seconds: 30);\n static final Duration connectTimeout = const Duration(seconds: 10);\n\n // 重试配置\n static const int maxRetries = 3;\n static final Duration retryDelay = const Duration(seconds: 1);\n\n // 日志配置\n static const bool enableLogging = true;\n static bool get enableNetworkLogging => !isProduction;\n static bool get enablePerformanceLogging => !isProduction;\n\n // 存储键名\n static const String tokenStorageKey = 'auth_token';\n static const String userInfoStorageKey = 'user_info';\n static const String backendUrlStorageKey = 'backend_url';\n static const String testModeStorageKey = 'test_mode';\n\n // API端点\n static String get loginUrl => '$currentBackendUrl/api/auth/login';\n static String get registerUrl => '$currentBackendUrl/api/auth/register';\n static String get verifyUrl => '$currentBackendUrl/api/auth/verify';\n static String get logoutUrl => '$currentBackendUrl/api/auth/logout';\n static String get deleteAccountUrl => '$currentBackendUrl/api/auth/delete';\n static String get sendVerificationCodeUrl =>\n '$currentBackendUrl/api/auth/send-verification-code';\n static String get verifyCodeUrl => '$currentBackendUrl/api/auth/verify-code';\n static String get forgotPasswordUrl =>\n '$currentBackendUrl/api/auth/forgot-password';\n static String get resetPasswordUrl =>\n '$currentBackendUrl/api/auth/reset-password';\n static String get userInfoUrl => '$currentBackendUrl/api/auth/user-info';\n static String get bindEmailUrl => '$currentBackendUrl/api/auth/bind-email';\n\n static String get alipayCreateOrderUrl =>\n '$currentBackendUrl/api/alipay/create-order';\n static String get alipayQueryOrderUrl =>\n '$currentBackendUrl/api/alipay/query-order';\n static String get alipayMembershipStatusUrl =>\n '$currentBackendUrl/api/alipay/check-membership';\n\n static String get stripeMembershipStatusUrl =>\n '$currentBackendUrl/api/stripe/membership-status';\n static String get stripeCreateSubscriptionUrl =>\n '$currentBackendUrl/api/stripe/create-subscription';\n static String get stripeSessionStatusUrl =>\n '$currentBackendUrl/api/stripe/session-status';\n\n static String get appleVerifyReceiptUrl =>\n '$currentBackendUrl/api/apple/verify-receipt';\n\n static String get adminCheckStatusUrl =>\n '$currentBackendUrl/api/admin/check-status';\n static String get adminCreateRedeemCodeUrl =>\n '$currentBackendUrl/api/admin/create-redeem-code';\n static String get adminRedeemCodesUrl =>\n '$currentBackendUrl/api/admin/redeem-codes';\n static String get adminUseRedeemCodeUrl =>\n '$currentBackendUrl/api/admin/use-redeem-code';\n static String get adminPurchaseHistoryUrl =>\n '$currentBackendUrl/api/admin/purchase-history';\n static String get adminRedeemHistoryUrl =>\n '$currentBackendUrl/api/admin/redeem-history';\n\n static String get leaderboardUrl => '$currentBackendUrl/api/leaderboard';\n static String get updateTransferDataUrl =>\n '$currentBackendUrl/api/leaderboard/update';\n\n static String get healthCheckUrl => '$currentBackendUrl/health';\n\n // iOS 后台保活静音音频\n static String get silenceAudioUrl =>\n '$currentBackendUrl/static/audio/silence.mp3';\n\n // 3D 佛像模型配置\n // 如果 R2 上需要切换到新的对象键,优先改这里,便于强制绕开旧缓存。\n static const String buddhaModelAssetPath = 'models/buddha_model.model';\n // 当前线上正确模型明显大于 48MB,小于该阈值视为误传/降质文件。\n static const int minBuddhaModelSizeBytes = 100 * 1024 * 1024;\n\n // 请求头\n static Map<String, String> get defaultHeaders => {\n 'Content-Type': 'application/json',\n 'Accept': 'application/json',\n 'User-Agent': 'FabushiApp/${isWeb ? \"Web\" : \"Mobile\"}',\n };\n\n // 错误消息\n static const Map<String, String> errorMessages = {\n 'network_error': '网络连接失败,请检查网络设置',\n 'server_error': '服务器错误,请稍后重试',\n 'timeout_error': '请求超时,请检查网络连接',\n 'auth_error': '认证失败,请重新登录',\n 'permission_error': '权限不足',\n 'validation_error': '数据验证失败',\n 'unknown_error': '未知错误,请联系客服',\n };\n\n // 备用地址\n static List<String> get fallbackUrls {\n return [primaryBackendUrl];\n }\n\n // 调试配置\n static const bool enableApiLogging = true;\n static const bool debugMode = bool.fromEnvironment(\n 'DEBUG',\n defaultValue: false,\n );\n\n static void printConfigInfo() {\n if (kDebugMode) {\n print('=== 应用配置 ===');\n print('环境: ${isProduction ? \"生产\" : \"开发\"}');\n print('平台: ${isWeb ? \"Web\" : \"Native\"}');\n print('API URL: $currentBackendUrl');\n print('================');\n }\n }\n\n static void printCurrentConfig() => printConfigInfo();\n}\n"}, "3c542ccf16bd702be3ca2c944be449a17675282d": {"t": 1785757998.3704991, "n": 6530, "h": "69f66219b5174b97", "c": "import 'package:flutter/foundation.dart';\n\n/// 应用配置 - 统一管理所有配置项\nclass AppConfig {\n AppConfig._();\n\n static final AppConfig instance = AppConfig._();\n\n // 环境配置\n static const String environment = String.fromEnvironment(\n 'ENV',\n defaultValue: 'production',\n );\n\n static bool get isProduction {\n if (kIsWeb) {\n final currentUrl = Uri.base.toString();\n if (currentUrl.contains('fabushi-flutter-web-dev') ||\n currentUrl.contains('localhost')) {\n return false;\n }\n if (currentUrl.contains('fabushi-flutter-web-prod')) {\n return true;\n }\n }\n return environment == 'production';\n }\n\n static bool get isDevelopment => !isProduction;\n static bool get isStaging => environment == 'staging';\n static bool get isWeb => kIsWeb;\n\n // API配置\n static const String primaryBackendUrl = 'https://flutter.ombhrum.com';\n static const String cloudflareWorkerProdUrl = 'https://flutter.ombhrum.com';\n static const String cloudflareWorkerDevUrl = 'https://flutter.ombhrum.com';\n static const String localDevUrl = 'http://localhost:8787';\n\n static String get currentBackendUrl {\n // 统一使用 flutter.ombhrum.com 作为后端地址\n return primaryBackendUrl;\n }\n\n static String get apiUrl => currentBackendUrl;\n\n // 应用信息\n static const String appName = '大乘';\n static const String appVersion = '1.0.0';\n static const int appBuildNumber = 1;\n\n // 功能开关\n static const bool enableFirebaseAuth = true;\n static const bool enableAlipay = true;\n static const bool enableAppleIAP = true;\n static const bool enableVideoFeed = true;\n static bool get enableDebugMode => !isProduction;\n\n // 传输配置\n static const int fileChunkSize = 1024;\n static const int maxRetryCount = 3;\n static const int timeoutDuration = 5000;\n static const int maxConcurrentTransfers = 5;\n\n // 缓存配置\n static const int cacheMaxAge = 7;\n static const int maxCacheSize = 100;\n\n // 分页配置\n static const int defaultPageSize = 20;\n static const int maxPageSize = 100;\n\n // 超时配置\n static final Duration requestTimeout = const Duration(seconds: 30);\n static final Duration connectTimeout = const Duration(seconds: 10);\n\n // 重试配置\n static const int maxRetries = 3;\n static final Duration retryDelay = const Duration(seconds: 1);\n\n // 日志配置\n static const bool enableLogging = true;\n static bool get enableNetworkLogging => !isProduction;\n static bool get enablePerformanceLogging => !isProduction;\n\n // 存储键名\n static const String tokenStorageKey = 'auth_token';\n static const String userInfoStorageKey = 'user_info';\n static const String backendUrlStorageKey = 'backend_url';\n static const String testModeStorageKey = 'test_mode';\n\n // API端点\n static String get loginUrl => '$currentBackendUrl/api/auth/login';\n static String get registerUrl => '$currentBackendUrl/api/auth/register';\n static String get verifyUrl => '$currentBackendUrl/api/auth/verify';\n static String get logoutUrl => '$currentBackendUrl/api/auth/logout';\n static String get deleteAccountUrl => '$currentBackendUrl/api/auth/delete';\n static String get sendVerificationCodeUrl =>\n '$currentBackendUrl/api/auth/send-verification-code';\n static String get verifyCodeUrl => '$currentBackendUrl/api/auth/verify-code';\n static String get forgotPasswordUrl =>\n '$currentBackendUrl/api/auth/forgot-password';\n static String get resetPasswordUrl =>\n '$currentBackendUrl/api/auth/reset-password';\n static String get userInfoUrl => '$currentBackendUrl/api/auth/user-info';\n static String get bindEmailUrl => '$currentBackendUrl/api/auth/bind-email';\n\n static String get alipayCreateOrderUrl =>\n '$currentBackendUrl/api/alipay/create-order';\n static String get alipayQueryOrderUrl =>\n '$currentBackendUrl/api/alipay/query-order';\n static String get alipayMembershipStatusUrl =>\n '$currentBackendUrl/api/alipay/check-membership';\n\n static String get stripeMembershipStatusUrl =>\n '$currentBackendUrl/api/stripe/membership-status';\n static String get stripeCreateSubscriptionUrl =>\n '$currentBackendUrl/api/stripe/create-subscription';\n static String get stripeSessionStatusUrl =>\n '$currentBackendUrl/api/stripe/session-status';\n\n static String get appleVerifyReceiptUrl =>\n '$currentBackendUrl/api/apple/verify-receipt';\n\n static String get adminCheckStatusUrl =>\n '$currentBackendUrl/api/admin/check-status';\n static String get adminCreateRedeemCodeUrl =>\n '$currentBackendUrl/api/admin/create-redeem-code';\n static String get adminRedeemCodesUrl =>\n '$currentBackendUrl/api/admin/redeem-codes';\n static String get adminUseRedeemCodeUrl =>\n '$currentBackendUrl/api/admin/use-redeem-code';\n static String get adminPurchaseHistoryUrl =>\n '$currentBackendUrl/api/admin/purchase-history';\n static String get adminRedeemHistoryUrl =>\n '$currentBackendUrl/api/admin/redeem-history';\n\n static String get leaderboardUrl => '$currentBackendUrl/api/leaderboard';\n static String get updateTransferDataUrl =>\n '$currentBackendUrl/api/leaderboard/update';\n\n static String get healthCheckUrl => '$currentBackendUrl/health';\n\n // iOS 后台保活静音音频\n static String get silenceAudioUrl =>\n '$currentBackendUrl/static/audio/silence.mp3';\n\n // 3D 佛像模型配置\n // 如果 R2 上需要切换到新的对象键,优先改这里,便于强制绕开旧缓存。\n static const String buddhaModelAssetPath = 'models/buddha_model.model';\n // 当前线上正确模型明显大于 48MB,小于该阈值视为误传/降质文件。\n static const int minBuddhaModelSizeBytes = 100 * 1024 * 1024;\n\n // 请求头\n static Map<String, String> get defaultHeaders => {\n 'Content-Type': 'application/json',\n 'Accept': 'application/json',\n 'User-Agent': 'FabushiApp/${isWeb ? \"Web\" : \"Mobile\"}',\n };\n\n // 错误消息\n static const Map<String, String> errorMessages = {\n 'network_error': '网络连接失败,请检查网络设置',\n 'server_error': '服务器错误,请稍后重试',\n 'timeout_error': '请求超时,请检查网络连接',\n 'auth_error': '认证失败,请重新登录',\n 'permission_error': '权限不足',\n 'validation_error': '数据验证失败',\n 'unknown_error': '未知错误,请联系客服',\n };\n\n // 备用地址\n static List<String> get fallbackUrls {\n return [primaryBackendUrl];\n }\n\n // 调试配置\n static const bool enableApiLogging = true;\n static const bool debugMode = bool.fromEnvironment(\n 'DEBUG',\n defaultValue: false,\n );\n\n static void printConfigInfo() {\n if (kDebugMode) {\n print('=== 应用配置 ===');\n print('环境: ${isProduction ? \"生产\" : \"开发\"}');\n print('平台: ${isWeb ? \"Web\" : \"Native\"}');\n print('API URL: $currentBackendUrl');\n print('================');\n }\n }\n\n static void printCurrentConfig() => printConfigInfo();\n}\n"}, "9ddb31fc8f1b1c74281559c164b6fc03f62112e5": {"t": 1785758000.050555, "n": 6392, "h": "8491bc2fdd75a10d", "c": "import 'package:flutter/foundation.dart';\n\n/// 应用配置 - 统一管理所有配置项\nclass AppConfig {\n AppConfig._();\n\n static final AppConfig instance = AppConfig._();\n\n // 环境配置\n static const String environment = String.fromEnvironment('ENV', defaultValue: 'production');\n\n static bool get isProduction {\n if (kIsWeb) {\n final currentUrl = Uri.base.toString();\n if (currentUrl.contains('fabushi-flutter-web-dev') || currentUrl.contains('localhost')) {\n return false;\n }\n if (currentUrl.contains('fabushi-flutter-web-prod')) {\n return true;\n }\n }\n return environment == 'production';\n }\n\n static bool get isDevelopment => !isProduction;\n static bool get isStaging => environment == 'staging';\n static bool get isWeb => kIsWeb;\n\n // API配置\n static const String primaryBackendUrl = 'https://flutter.ombhrum.com';\n static const String cloudflareWorkerProdUrl = 'https://flutter.ombhrum.com';\n static const String cloudflareWorkerDevUrl = 'https://flutter.ombhrum.com';\n static const String localDevUrl = 'http://localhost:8787';\n\n static String get currentBackendUrl {\n // 统一使用 flutter.ombhrum.com 作为后端地址\n return primaryBackendUrl;\n }\n\n static String get apiUrl => currentBackendUrl;\n\n // 应用信息\n static const String appName = '大乘';\n static const String appVersion = '1.0.0';\n static const int appBuildNumber = 1;\n\n // 功能开关\n static const bool enableFirebaseAuth = true;\n static const bool enableAlipay = true;\n static const bool enableAppleIAP = true;\n static const bool enableVideoFeed = true;\n static bool get enableDebugMode => !isProduction;\n\n // 传输配置\n static const int fileChunkSize = 1024;\n static const int maxRetryCount = 3;\n static const int timeoutDuration = 5000;\n static const int maxConcurrentTransfers = 5;\n\n // 缓存配置\n static const int cacheMaxAge = 7;\n static const int maxCacheSize = 100;\n\n // 分页配置\n static const int defaultPageSize = 20;\n static const int maxPageSize = 100;\n\n // 超时配置\n static final Duration requestTimeout = const Duration(seconds: 30);\n static final Duration connectTimeout = const Duration(seconds: 10);\n\n // 重试配置\n static const int maxRetries = 3;\n static final Duration retryDelay = const Duration(seconds: 1);\n\n // 日志配置\n static const bool enableLogging = true;\n static bool get enableNetworkLogging => !isProduction;\n static bool get enablePerformanceLogging => !isProduction;\n\n // 存储键名\n static const String tokenStorageKey = 'auth_token';\n static const String userInfoStorageKey = 'user_info';\n static const String backendUrlStorageKey = 'backend_url';\n static const String testModeStorageKey = 'test_mode';\n\n // API端点\n static String get loginUrl => '$currentBackendUrl/api/auth/login';\n static String get registerUrl => '$currentBackendUrl/api/auth/register';\n static String get verifyUrl => '$currentBackendUrl/api/auth/verify';\n static String get logoutUrl => '$currentBackendUrl/api/auth/logout';\n static String get deleteAccountUrl => '$currentBackendUrl/api/auth/delete';\n static String get sendVerificationCodeUrl => '$currentBackendUrl/api/auth/send-verification-code';\n static String get verifyCodeUrl => '$currentBackendUrl/api/auth/verify-code';\n static String get forgotPasswordUrl => '$currentBackendUrl/api/auth/forgot-password';\n static String get resetPasswordUrl => '$currentBackendUrl/api/auth/reset-password';\n static String get userInfoUrl => '$currentBackendUrl/api/auth/user-info';\n static String get bindEmailUrl => '$currentBackendUrl/api/auth/bind-email';\n\n static String get alipayCreateOrderUrl => '$currentBackendUrl/api/alipay/create-order';\n static String get alipayQueryOrderUrl => '$currentBackendUrl/api/alipay/query-order';\n static String get alipayMembershipStatusUrl => '$currentBackendUrl/api/alipay/check-membership';\n\n static String get stripeMembershipStatusUrl => '$currentBackendUrl/api/stripe/membership-status';\n static String get stripeCreateSubscriptionUrl =>\n '$currentBackendUrl/api/stripe/create-subscription';\n static String get stripeSessionStatusUrl => '$currentBackendUrl/api/stripe/session-status';\n\n static String get appleVerifyReceiptUrl => '$currentBackendUrl/api/apple/verify-receipt';\n\n static String get adminCheckStatusUrl => '$currentBackendUrl/api/admin/check-status';\n static String get adminCreateRedeemCodeUrl => '$currentBackendUrl/api/admin/create-redeem-code';\n static String get adminRedeemCodesUrl => '$currentBackendUrl/api/admin/redeem-codes';\n static String get adminUseRedeemCodeUrl => '$currentBackendUrl/api/admin/use-redeem-code';\n static String get adminPurchaseHistoryUrl => '$currentBackendUrl/api/admin/purchase-history';\n static String get adminRedeemHistoryUrl => '$currentBackendUrl/api/admin/redeem-history';\n\n static String get leaderboardUrl => '$currentBackendUrl/api/leaderboard';\n static String get updateTransferDataUrl => '$currentBackendUrl/api/leaderboard/update';\n\n static String get healthCheckUrl => '$currentBackendUrl/health';\n\n // iOS 后台保活静音音频\n static String get silenceAudioUrl => '$currentBackendUrl/static/audio/silence.mp3';\n\n // 3D 佛像模型配置\n // 如果 R2 上需要切换到新的对象键,优先改这里,便于强制绕开旧缓存。\n static const String buddhaModelAssetPath = 'models/buddha_model.model';\n // 当前线上正确模型明显大于 48MB,小于该阈值视为误传/降质文件。\n static const int minBuddhaModelSizeBytes = 100 * 1024 * 1024;\n\n // 请求头\n static Map<String, String> get defaultHeaders => {\n 'Content-Type': 'application/json',\n 'Accept': 'application/json',\n 'User-Agent': 'FabushiApp/${isWeb ? \"Web\" : \"Mobile\"}',\n };\n\n // 错误消息\n static const Map<String, String> errorMessages = {\n 'network_error': '网络连接失败,请检查网络设置',\n 'server_error': '服务器错误,请稍后重试',\n 'timeout_error': '请求超时,请检查网络连接',\n 'auth_error': '认证失败,请重新登录',\n 'permission_error': '权限不足',\n 'validation_error': '数据验证失败',\n 'unknown_error': '未知错误,请联系客服',\n };\n\n // 备用地址\n static List<String> get fallbackUrls {\n return [primaryBackendUrl];\n }\n\n // 调试配置\n static const bool enableApiLogging = true;\n static const bool debugMode = bool.fromEnvironment('DEBUG', defaultValue: false);\n\n static void printConfigInfo() {\n if (kDebugMode) {\n print('=== 应用配置 ===');\n print('环境: ${isProduction ? \"生产\" : \"开发\"}');\n print('平台: ${isWeb ? \"Web\" : \"Native\"}');\n print('API URL: $currentBackendUrl');\n print('================');\n }\n }\n\n static void printCurrentConfig() => printConfigInfo();\n}\n"}, "33b5aa0d214c8013886ef1773c6ff1304af084f4": {"t": 1786067507.7934217, "n": 11587, "h": "457c3958c206ce9d", "c": "import 'package:flutter/foundation.dart';\n\n/// 应用配置 - 统一管理所有配置项\nclass AppConfig {\n AppConfig._();\n\n static final AppConfig instance = AppConfig._();\n\n // 环境配置\n static const String environment = String.fromEnvironment(\n 'ENV',\n defaultValue: 'production',\n );\n\n static bool get isProduction {\n if (kIsWeb) {\n final currentUrl = Uri.base.toString();\n if (currentUrl.contains('fabushi-flutter-web-dev') ||\n currentUrl.contains('localhost')) {\n return false;\n }\n if (currentUrl.contains('fabushi-flutter-web-prod')) {\n return true;\n }\n }\n return environment == 'production';\n }\n\n static bool get isDevelopment => !isProduction;\n static bool get isStaging => environment == 'staging';\n static bool get isWeb => kIsWeb;\n\n // API配置\n static const String configuredApiBaseUrl = String.fromEnvironment(\n 'API_BASE_URL',\n defaultValue: '',\n );\n static const String primaryBackendUrl = 'https://api.ombhrum.com';\n static const String cloudflareWorkerProdUrl = 'https://api.ombhrum.com';\n static const String cloudflareWorkerDevUrl =\n 'https://fabushi-flutter-web-dev.bhrumom.workers.dev';\n static const String localDevUrl = 'http://localhost:8787';\n static const String publicWebUrl = 'https://flutter.ombhrum.com';\n static const String configuredAiBackendUrl = String.fromEnvironment(\n 'AI_BACKEND_URL',\n defaultValue: '',\n );\n static const String defaultAiBackendUrl = primaryBackendUrl;\n static const String configuredDachengAiWebUrl = String.fromEnvironment(\n 'DACHENG_AI_WEB_URL',\n defaultValue: '',\n );\n static const bool desktopControlEnabled = bool.fromEnvironment(\n 'DACHENG_DESKTOP_CONTROL',\n defaultValue: false,\n );\n static const String configuredOpenClawDeepSeekApiKey = String.fromEnvironment(\n 'DACHENG_OPENCLAW_DEEPSEEK_API_KEY',\n defaultValue: '',\n );\n static const String defaultDachengAiWebUrl =\n 'https://fabushi.ombhrum.com/app/ai';\n\n static String get currentBackendUrl {\n if (configuredApiBaseUrl.isNotEmpty) {\n return configuredApiBaseUrl;\n }\n if (kIsWeb) {\n final currentUrl = Uri.base;\n final host = currentUrl.host;\n if (host.contains('localhost')) {\n return localDevUrl;\n }\n if (host.contains('fabushi-flutter-web-dev')) {\n return cloudflareWorkerDevUrl;\n }\n if (host.contains('fabushi-flutter-web-prod')) {\n return cloudflareWorkerProdUrl;\n }\n }\n return primaryBackendUrl;\n }\n\n static String get apiUrl => currentBackendUrl;\n\n static String get currentAiBackendUrl {\n if (configuredAiBackendUrl.isNotEmpty) {\n return configuredAiBackendUrl;\n }\n return defaultAiBackendUrl;\n }\n\n static String get dachengAiWebUrl {\n if (configuredDachengAiWebUrl.isNotEmpty) {\n return configuredDachengAiWebUrl;\n }\n return defaultDachengAiWebUrl;\n }\n\n static String get openClawDeepSeekProxyBaseUrl {\n final baseUrl = currentAiBackendUrl.replaceFirst(RegExp(r'/+$'), '');\n return '$baseUrl/api/openclaw/deepseek/v1';\n }\n\n /// OpenAI Responses-compatible endpoint consumed by the Mahayana Rust SDK.\n static String get codexDeepSeekResponsesBaseUrl {\n final baseUrl = currentAiBackendUrl.replaceFirst(RegExp(r'/+$'), '');\n return '$baseUrl/codex-deepseek/v1';\n }\n\n static Uri buildDachengAiWebUri({\n String? prompt,\n String? bookTitle,\n String? context,\n }) {\n final base = Uri.parse(dachengAiWebUrl);\n final query = <String, String>{\n ...base.queryParameters,\n if (prompt != null && prompt.trim().isNotEmpty) 'prompt': prompt.trim(),\n if (bookTitle != null && bookTitle.trim().isNotEmpty)\n 'book': bookTitle.trim(),\n if (context != null && context.trim().isNotEmpty)\n 'context': context.trim(),\n };\n return base.replace(queryParameters: query.isEmpty ? null : query);\n }\n\n /// 大厂式 API 网关入口:客户端只允许构造自家后端的相对路径。\n ///\n /// 第三方 API、上游 API、缓存、重试、fallback 都应在后端处理;\n /// App 端不得直接传入 https://... 或 http://... 这类绝对 URL。\n static Uri buildBackendUri(\n String endpoint, {\n Map<String, String>? queryParameters,\n }) {\n final normalizedEndpoint = endpoint.trim();\n if (normalizedEndpoint.isEmpty || !normalizedEndpoint.startsWith('/')) {\n throw ArgumentError(\n 'API endpoint must be a first-party relative path starting with /: $endpoint',\n );\n }\n if (normalizedEndpoint.startsWith('//') ||\n normalizedEndpoint.contains('://')) {\n throw ArgumentError(\n 'API endpoint must not be an absolute or protocol-relative URL: $endpoint',\n );\n }\n\n final baseUrl = currentBackendUrl.replaceFirst(RegExp(r'/+$'), '');\n final uri = Uri.parse('$baseUrl$normalizedEndpoint');\n if (queryParameters == null || queryParameters.isEmpty) {\n return uri;\n }\n return uri.replace(queryParameters: queryParameters);\n }\n\n static String buildBackendUrl(\n String endpoint, {\n Map<String, String>? queryParameters,\n }) {\n return buildBackendUri(\n endpoint,\n queryParameters: queryParameters,\n ).toString();\n }\n\n // 应用信息\n static const String appName = '大乘';\n // 保持与 pubspec.yaml 中的 version 同步,供 Web 回退和服务端比对使用。\n static const String appVersion = '1.0.0';\n static const int appBuildNumber = 400;\n\n // 功能开关\n static const bool enableFirebaseAuth = true;\n static const bool enableAlipay = true;\n static const bool enableAppleIAP = true;\n static const bool enableVideoFeed = true;\n static bool get enableDebugMode => !isProduction;\n\n // 传输配置\n static const int fileChunkSize = 1024;\n static const int maxRetryCount = 3;\n static const int timeoutDuration = 5000;\n static const int maxConcurrentTransfers = 5;\n\n // 缓存配置\n static const int cacheMaxAge = 7;\n static const int maxCacheSize = 100;\n\n // 分页配置\n static const int defaultPageSize = 20;\n static const int maxPageSize = 100;\n\n // 超时配置\n static final Duration requestTimeout = const Duration(seconds: 30);\n static final Duration connectTimeout = const Duration(seconds: 10);\n\n // 重试配置\n static const int maxRetries = 3;\n static final Duration retryDelay = const Duration(seconds: 1);\n\n // 日志配置\n static const bool enableLogging = true;\n static bool get enableNetworkLogging => !isProduction;\n static bool get enablePerformanceLogging => !isProduction;\n\n // 存储键名\n static const String tokenStorageKey = 'auth_token';\n static const String userInfoStorageKey = 'user_info';\n static const String backendUrlStorageKey = 'backend_url';\n static const String testModeStorageKey = 'test_mode';\n\n // API端点\n static String get loginUrl => buildBackendUrl('/api/auth/login');\n static String get registerUrl => buildBackendUrl('/api/auth/register');\n static String get verifyUrl => buildBackendUrl('/api/auth/verify');\n static String get logoutUrl => buildBackendUrl('/api/auth/logout');\n static String get deleteAccountUrl => buildBackendUrl('/api/auth/delete');\n static String get sendVerificationCodeUrl =>\n buildBackendUrl('/api/auth/send-verification-code');\n static String get verifyCodeUrl => buildBackendUrl('/api/auth/verify-code');\n static String get forgotPasswordUrl =>\n buildBackendUrl('/api/auth/forgot-password');\n static String get resetPasswordUrl =>\n buildBackendUrl('/api/auth/reset-password');\n static String get userInfoUrl => buildBackendUrl('/api/auth/user-info');\n static String get bindEmailUrl => buildBackendUrl('/api/auth/bind-email');\n static String get appVersionPolicyUrl =>\n buildBackendUrl('/api/app/version-policy');\n\n static String get agentChatUrl => buildBackendUrl('/api/agent/chat');\n static String get aiQuotaUrl => buildBackendUrl('/api/ai/quota');\n static String agentRunEventsUrl(String runId) =>\n buildBackendUrl('/api/agent/runs/$runId/events');\n static String agentRunCancelUrl(String runId) =>\n buildBackendUrl('/api/agent/runs/$runId/cancel');\n static String agentMessageFeedbackUrl(String messageId) =>\n buildBackendUrl('/api/agent/messages/$messageId/feedback');\n\n static String get alipayCreateOrderUrl =>\n buildBackendUrl('/api/alipay/create-order');\n static String get alipayQueryOrderUrl =>\n buildBackendUrl('/api/alipay/query-order');\n static String get alipayMembershipStatusUrl =>\n buildBackendUrl('/api/alipay/check-membership');\n\n static String get stripeMembershipStatusUrl =>\n buildBackendUrl('/api/stripe/membership-status');\n static String get stripeCreateSubscriptionUrl =>\n buildBackendUrl('/api/stripe/create-subscription');\n static String get stripeSessionStatusUrl =>\n buildBackendUrl('/api/stripe/session-status');\n\n static String get appleVerifyReceiptUrl =>\n buildBackendUrl('/api/apple/verify-receipt');\n static String get purchaseEntitlementUrl =>\n buildBackendUrl('/api/purchases/entitlement');\n static String get walletBalanceUrl => buildBackendUrl('/api/wallet/balance');\n static String get walletSpendUrl => buildBackendUrl('/api/wallet/spend');\n\n static const String zenBuddhaAssetProductId = 'zen_buddha_asset';\n static const String zenBuddhaAssetDisplayName = '3D佛像素材';\n static const String zenBuddhaAssetPriceLabel = '¥33.00';\n static const int zenBuddhaAssetFudeGoldPrice = 33;\n static const String zenBuddhaAssetCardImagePath =\n 'assets/images/zen_buddha_material_card.png';\n\n static String get adminCheckStatusUrl =>\n buildBackendUrl('/api/admin/check-status');\n static String get adminCreateRedeemCodeUrl =>\n buildBackendUrl('/api/admin/create-redeem-code');\n static String get adminRedeemCodesUrl =>\n buildBackendUrl('/api/admin/redeem-codes');\n static String get adminUseRedeemCodeUrl =>\n buildBackendUrl('/api/admin/use-redeem-code');\n static String get adminPurchaseHistoryUrl =>\n buildBackendUrl('/api/admin/purchase-history');\n static String get adminRedeemHistoryUrl =>\n buildBackendUrl('/api/admin/redeem-history');\n\n static String get leaderboardUrl => buildBackendUrl('/api/leaderboard');\n static String get updateTransferDataUrl =>\n buildBackendUrl('/api/leaderboard/update');\n\n static String get healthCheckUrl => buildBackendUrl('/health');\n\n // 3D 佛像模型配置\n // 如果 R2 上需要切换到新的对象键,优先改这里,便于强制绕开旧缓存。\n static const String buddhaModelAssetPath = 'models/buddha_model.model';\n // Android/iOS both use the R2 flutter_scene .model; local cache misses\n // download through AssetLoaderService.\n\n // 当前线上正确模型明显大于 100MB,小于该阈值视为误传/降质文件。\n static const int minBuddhaModelSizeBytes = 100 * 1024 * 1024;\n\n // 请求头\n static Map<String, String> get defaultHeaders => {\n 'Content-Type': 'application/json',\n 'Accept': 'application/json',\n 'User-Agent': 'FabushiApp/${isWeb ? \"Web\" : \"Mobile\"}',\n };\n\n // 错误消息\n static const Map<String, String> errorMessages = {\n 'network_error': '网络连接失败,请检查网络设置',\n 'server_error': '服务器错误,请稍后重试',\n 'timeout_error': '请求超时,请检查网络连接',\n 'auth_error': '认证失败,请重新登录',\n 'permission_error': '权限不足',\n 'validation_error': '数据验证失败',\n 'unknown_error': '未知错误,请联系客服',\n };\n\n // 备用地址\n static List<String> get fallbackUrls {\n return [primaryBackendUrl];\n }\n\n // 调试配置\n static const bool enableApiLogging = true;\n static const bool debugMode = bool.fromEnvironment(\n 'DEBUG',\n defaultValue: false,\n );\n\n static void printConfigInfo() {\n if (kDebugMode) {\n print('=== 应用配置 ===');\n print('环境: ${isProduction ? \"生产\" : \"开发\"}');\n print('平台: ${isWeb ? \"Web\" : \"Native\"}');\n print('API URL: $currentBackendUrl');\n print('================');\n }\n }\n\n static void printCurrentConfig() => printConfigInfo();\n}\n"}, "dc1c9f261e7ed7f15eb1e20fd6dbd40bbd416840": {"t": 1786068343.5509708, "n": 11587, "h": "457c3958c206ce9d", "c": "import 'package:flutter/foundation.dart';\n\n/// 应用配置 - 统一管理所有配置项\nclass AppConfig {\n AppConfig._();\n\n static final AppConfig instance = AppConfig._();\n\n // 环境配置\n static const String environment = String.fromEnvironment(\n 'ENV',\n defaultValue: 'production',\n );\n\n static bool get isProduction {\n if (kIsWeb) {\n final currentUrl = Uri.base.toString();\n if (currentUrl.contains('fabushi-flutter-web-dev') ||\n currentUrl.contains('localhost')) {\n return false;\n }\n if (currentUrl.contains('fabushi-flutter-web-prod')) {\n return true;\n }\n }\n return environment == 'production';\n }\n\n static bool get isDevelopment => !isProduction;\n static bool get isStaging => environment == 'staging';\n static bool get isWeb => kIsWeb;\n\n // API配置\n static const String configuredApiBaseUrl = String.fromEnvironment(\n 'API_BASE_URL',\n defaultValue: '',\n );\n static const String primaryBackendUrl = 'https://api.ombhrum.com';\n static const String cloudflareWorkerProdUrl = 'https://api.ombhrum.com';\n static const String cloudflareWorkerDevUrl =\n 'https://fabushi-flutter-web-dev.bhrumom.workers.dev';\n static const String localDevUrl = 'http://localhost:8787';\n static const String publicWebUrl = 'https://flutter.ombhrum.com';\n static const String configuredAiBackendUrl = String.fromEnvironment(\n 'AI_BACKEND_URL',\n defaultValue: '',\n );\n static const String defaultAiBackendUrl = primaryBackendUrl;\n static const String configuredDachengAiWebUrl = String.fromEnvironment(\n 'DACHENG_AI_WEB_URL',\n defaultValue: '',\n );\n static const bool desktopControlEnabled = bool.fromEnvironment(\n 'DACHENG_DESKTOP_CONTROL',\n defaultValue: false,\n );\n static const String configuredOpenClawDeepSeekApiKey = String.fromEnvironment(\n 'DACHENG_OPENCLAW_DEEPSEEK_API_KEY',\n defaultValue: '',\n );\n static const String defaultDachengAiWebUrl =\n 'https://fabushi.ombhrum.com/app/ai';\n\n static String get currentBackendUrl {\n if (configuredApiBaseUrl.isNotEmpty) {\n return configuredApiBaseUrl;\n }\n if (kIsWeb) {\n final currentUrl = Uri.base;\n final host = currentUrl.host;\n if (host.contains('localhost')) {\n return localDevUrl;\n }\n if (host.contains('fabushi-flutter-web-dev')) {\n return cloudflareWorkerDevUrl;\n }\n if (host.contains('fabushi-flutter-web-prod')) {\n return cloudflareWorkerProdUrl;\n }\n }\n return primaryBackendUrl;\n }\n\n static String get apiUrl => currentBackendUrl;\n\n static String get currentAiBackendUrl {\n if (configuredAiBackendUrl.isNotEmpty) {\n return configuredAiBackendUrl;\n }\n return defaultAiBackendUrl;\n }\n\n static String get dachengAiWebUrl {\n if (configuredDachengAiWebUrl.isNotEmpty) {\n return configuredDachengAiWebUrl;\n }\n return defaultDachengAiWebUrl;\n }\n\n static String get openClawDeepSeekProxyBaseUrl {\n final baseUrl = currentAiBackendUrl.replaceFirst(RegExp(r'/+$'), '');\n return '$baseUrl/api/openclaw/deepseek/v1';\n }\n\n /// OpenAI Responses-compatible endpoint consumed by the Mahayana Rust SDK.\n static String get codexDeepSeekResponsesBaseUrl {\n final baseUrl = currentAiBackendUrl.replaceFirst(RegExp(r'/+$'), '');\n return '$baseUrl/codex-deepseek/v1';\n }\n\n static Uri buildDachengAiWebUri({\n String? prompt,\n String? bookTitle,\n String? context,\n }) {\n final base = Uri.parse(dachengAiWebUrl);\n final query = <String, String>{\n ...base.queryParameters,\n if (prompt != null && prompt.trim().isNotEmpty) 'prompt': prompt.trim(),\n if (bookTitle != null && bookTitle.trim().isNotEmpty)\n 'book': bookTitle.trim(),\n if (context != null && context.trim().isNotEmpty)\n 'context': context.trim(),\n };\n return base.replace(queryParameters: query.isEmpty ? null : query);\n }\n\n /// 大厂式 API 网关入口:客户端只允许构造自家后端的相对路径。\n ///\n /// 第三方 API、上游 API、缓存、重试、fallback 都应在后端处理;\n /// App 端不得直接传入 https://... 或 http://... 这类绝对 URL。\n static Uri buildBackendUri(\n String endpoint, {\n Map<String, String>? queryParameters,\n }) {\n final normalizedEndpoint = endpoint.trim();\n if (normalizedEndpoint.isEmpty || !normalizedEndpoint.startsWith('/')) {\n throw ArgumentError(\n 'API endpoint must be a first-party relative path starting with /: $endpoint',\n );\n }\n if (normalizedEndpoint.startsWith('//') ||\n normalizedEndpoint.contains('://')) {\n throw ArgumentError(\n 'API endpoint must not be an absolute or protocol-relative URL: $endpoint',\n );\n }\n\n final baseUrl = currentBackendUrl.replaceFirst(RegExp(r'/+$'), '');\n final uri = Uri.parse('$baseUrl$normalizedEndpoint');\n if (queryParameters == null || queryParameters.isEmpty) {\n return uri;\n }\n return uri.replace(queryParameters: queryParameters);\n }\n\n static String buildBackendUrl(\n String endpoint, {\n Map<String, String>? queryParameters,\n }) {\n return buildBackendUri(\n endpoint,\n queryParameters: queryParameters,\n ).toString();\n }\n\n // 应用信息\n static const String appName = '大乘';\n // 保持与 pubspec.yaml 中的 version 同步,供 Web 回退和服务端比对使用。\n static const String appVersion = '1.0.0';\n static const int appBuildNumber = 400;\n\n // 功能开关\n static const bool enableFirebaseAuth = true;\n static const bool enableAlipay = true;\n static const bool enableAppleIAP = true;\n static const bool enableVideoFeed = true;\n static bool get enableDebugMode => !isProduction;\n\n // 传输配置\n static const int fileChunkSize = 1024;\n static const int maxRetryCount = 3;\n static const int timeoutDuration = 5000;\n static const int maxConcurrentTransfers = 5;\n\n // 缓存配置\n static const int cacheMaxAge = 7;\n static const int maxCacheSize = 100;\n\n // 分页配置\n static const int defaultPageSize = 20;\n static const int maxPageSize = 100;\n\n // 超时配置\n static final Duration requestTimeout = const Duration(seconds: 30);\n static final Duration connectTimeout = const Duration(seconds: 10);\n\n // 重试配置\n static const int maxRetries = 3;\n static final Duration retryDelay = const Duration(seconds: 1);\n\n // 日志配置\n static const bool enableLogging = true;\n static bool get enableNetworkLogging => !isProduction;\n static bool get enablePerformanceLogging => !isProduction;\n\n // 存储键名\n static const String tokenStorageKey = 'auth_token';\n static const String userInfoStorageKey = 'user_info';\n static const String backendUrlStorageKey = 'backend_url';\n static const String testModeStorageKey = 'test_mode';\n\n // API端点\n static String get loginUrl => buildBackendUrl('/api/auth/login');\n static String get registerUrl => buildBackendUrl('/api/auth/register');\n static String get verifyUrl => buildBackendUrl('/api/auth/verify');\n static String get logoutUrl => buildBackendUrl('/api/auth/logout');\n static String get deleteAccountUrl => buildBackendUrl('/api/auth/delete');\n static String get sendVerificationCodeUrl =>\n buildBackendUrl('/api/auth/send-verification-code');\n static String get verifyCodeUrl => buildBackendUrl('/api/auth/verify-code');\n static String get forgotPasswordUrl =>\n buildBackendUrl('/api/auth/forgot-password');\n static String get resetPasswordUrl =>\n buildBackendUrl('/api/auth/reset-password');\n static String get userInfoUrl => buildBackendUrl('/api/auth/user-info');\n static String get bindEmailUrl => buildBackendUrl('/api/auth/bind-email');\n static String get appVersionPolicyUrl =>\n buildBackendUrl('/api/app/version-policy');\n\n static String get agentChatUrl => buildBackendUrl('/api/agent/chat');\n static String get aiQuotaUrl => buildBackendUrl('/api/ai/quota');\n static String agentRunEventsUrl(String runId) =>\n buildBackendUrl('/api/agent/runs/$runId/events');\n static String agentRunCancelUrl(String runId) =>\n buildBackendUrl('/api/agent/runs/$runId/cancel');\n static String agentMessageFeedbackUrl(String messageId) =>\n buildBackendUrl('/api/agent/messages/$messageId/feedback');\n\n static String get alipayCreateOrderUrl =>\n buildBackendUrl('/api/alipay/create-order');\n static String get alipayQueryOrderUrl =>\n buildBackendUrl('/api/alipay/query-order');\n static String get alipayMembershipStatusUrl =>\n buildBackendUrl('/api/alipay/check-membership');\n\n static String get stripeMembershipStatusUrl =>\n buildBackendUrl('/api/stripe/membership-status');\n static String get stripeCreateSubscriptionUrl =>\n buildBackendUrl('/api/stripe/create-subscription');\n static String get stripeSessionStatusUrl =>\n buildBackendUrl('/api/stripe/session-status');\n\n static String get appleVerifyReceiptUrl =>\n buildBackendUrl('/api/apple/verify-receipt');\n static String get purchaseEntitlementUrl =>\n buildBackendUrl('/api/purchases/entitlement');\n static String get walletBalanceUrl => buildBackendUrl('/api/wallet/balance');\n static String get walletSpendUrl => buildBackendUrl('/api/wallet/spend');\n\n static const String zenBuddhaAssetProductId = 'zen_buddha_asset';\n static const String zenBuddhaAssetDisplayName = '3D佛像素材';\n static const String zenBuddhaAssetPriceLabel = '¥33.00';\n static const int zenBuddhaAssetFudeGoldPrice = 33;\n static const String zenBuddhaAssetCardImagePath =\n 'assets/images/zen_buddha_material_card.png';\n\n static String get adminCheckStatusUrl =>\n buildBackendUrl('/api/admin/check-status');\n static String get adminCreateRedeemCodeUrl =>\n buildBackendUrl('/api/admin/create-redeem-code');\n static String get adminRedeemCodesUrl =>\n buildBackendUrl('/api/admin/redeem-codes');\n static String get adminUseRedeemCodeUrl =>\n buildBackendUrl('/api/admin/use-redeem-code');\n static String get adminPurchaseHistoryUrl =>\n buildBackendUrl('/api/admin/purchase-history');\n static String get adminRedeemHistoryUrl =>\n buildBackendUrl('/api/admin/redeem-history');\n\n static String get leaderboardUrl => buildBackendUrl('/api/leaderboard');\n static String get updateTransferDataUrl =>\n buildBackendUrl('/api/leaderboard/update');\n\n static String get healthCheckUrl => buildBackendUrl('/health');\n\n // 3D 佛像模型配置\n // 如果 R2 上需要切换到新的对象键,优先改这里,便于强制绕开旧缓存。\n static const String buddhaModelAssetPath = 'models/buddha_model.model';\n // Android/iOS both use the R2 flutter_scene .model; local cache misses\n // download through AssetLoaderService.\n\n // 当前线上正确模型明显大于 100MB,小于该阈值视为误传/降质文件。\n static const int minBuddhaModelSizeBytes = 100 * 1024 * 1024;\n\n // 请求头\n static Map<String, String> get defaultHeaders => {\n 'Content-Type': 'application/json',\n 'Accept': 'application/json',\n 'User-Agent': 'FabushiApp/${isWeb ? \"Web\" : \"Mobile\"}',\n };\n\n // 错误消息\n static const Map<String, String> errorMessages = {\n 'network_error': '网络连接失败,请检查网络设置',\n 'server_error': '服务器错误,请稍后重试',\n 'timeout_error': '请求超时,请检查网络连接',\n 'auth_error': '认证失败,请重新登录',\n 'permission_error': '权限不足',\n 'validation_error': '数据验证失败',\n 'unknown_error': '未知错误,请联系客服',\n };\n\n // 备用地址\n static List<String> get fallbackUrls {\n return [primaryBackendUrl];\n }\n\n // 调试配置\n static const bool enableApiLogging = true;\n static const bool debugMode = bool.fromEnvironment(\n 'DEBUG',\n defaultValue: false,\n );\n\n static void printConfigInfo() {\n if (kDebugMode) {\n print('=== 应用配置 ===');\n print('环境: ${isProduction ? \"生产\" : \"开发\"}');\n print('平台: ${isWeb ? \"Web\" : \"Native\"}');\n print('API URL: $currentBackendUrl');\n print('================');\n }\n }\n\n static void printCurrentConfig() => printConfigInfo();\n}\n"}}, "fabushi/lib/services/openclaw/openclaw_runtime_io.dart": {"3323b5c4de0a31d36fe4d8934069ae04a3a16fc0": {"t": 1785745926.4502108, "n": 97198, "h": "2aff8c1751728d25", "c": "import 'dart:async';\nimport 'dart:convert';\nimport 'dart:ffi' show Abi;\nimport 'dart:io';\nimport 'dart:math' as math;\nimport 'package:archive/archive_io.dart';\nimport 'package:convert/convert.dart';\nimport 'package:crypto/crypto.dart';\nimport 'package:flutter/foundation.dart';\nimport 'package:flutter/services.dart';\nimport 'package:http/http.dart' as http;\nimport 'package:path/path.dart' as p;\nimport 'package:path_provider/path_provider.dart';\n\nimport '../../core/config/app_config.dart';\nimport '../app_settings.dart';\nimport '../diagnostic_log_service.dart';\nimport '../desktop_control/desktop_control_bridge.dart';\nimport '../desktop_control/desktop_control_policy.dart';\nimport 'openclaw_port_resolver.dart';\n\nenum OpenClawRuntimeState {\n unsupported,\n notBundled,\n stopped,\n starting,\n running,\n failed,\n}\n\nclass OpenClawRuntimeStatus {\n final OpenClawRuntimeState state;\n final String message;\n final int? port;\n final String? platformKey;\n final String? runtimePath;\n final Map<String, dynamic>? desktopToolsStatus;\n final DateTime checkedAt;\n\n const OpenClawRuntimeStatus({\n required this.state,\n required this.message,\n this.port,\n this.platformKey,\n this.runtimePath,\n this.desktopToolsStatus,\n required this.checkedAt,\n });\n\n bool get isHealthy => state == OpenClawRuntimeState.running;\n\n String get label {\n switch (state) {\n case OpenClawRuntimeState.unsupported:\n return '不支持';\n case OpenClawRuntimeState.notBundled:\n return '未内置';\n case OpenClawRuntimeState.stopped:\n return '未启动';\n case OpenClawRuntimeState.starting:\n return '启动中';\n case OpenClawRuntimeState.running:\n return '运行中';\n case OpenClawRuntimeState.failed:\n return '异常';\n }\n }\n}\n\nclass OpenClawGatewayTarget {\n final Uri baseUri;\n final String token;\n final String model;\n final String? modelOverride;\n final Uri? desktopToolsUri;\n final String? desktopToolsToken;\n final Map<String, dynamic>? desktopToolsStatus;\n\n const OpenClawGatewayTarget({\n required this.baseUri,\n required this.token,\n required this.model,\n this.modelOverride,\n this.desktopToolsUri,\n this.desktopToolsToken,\n this.desktopToolsStatus,\n });\n}\n\nclass OpenClawCliResult {\n final List<String> args;\n final int exitCode;\n final String stdout;\n final String stderr;\n final bool timedOut;\n\n const OpenClawCliResult({\n required this.args,\n required this.exitCode,\n required this.stdout,\n required this.stderr,\n this.timedOut = false,\n });\n\n bool get succeeded => exitCode == 0 && !timedOut;\n\n String get command => 'openclaw ${args.join(' ')}';\n\n String get combinedOutput {\n final parts = <String>[\n if (stdout.trim().isNotEmpty) stdout.trimRight(),\n if (stderr.trim().isNotEmpty) stderr.trimRight(),\n ];\n return parts.join('\\n');\n }\n\n OpenClawCliResult append(OpenClawCliResult next) {\n return OpenClawCliResult(\n args: [...args, '&&', ...next.args],\n exitCode: next.exitCode,\n stdout: [\n if (stdout.trim().isNotEmpty) stdout.trimRight(),\n if (next.stdout.trim().isNotEmpty) next.stdout.trimRight(),\n ].join('\\n'),\n stderr: [\n if (stderr.trim().isNotEmpty) stderr.trimRight(),\n if (next.stderr.trim().isNotEmpty) next.stderr.trimRight(),\n ].join('\\n'),\n timedOut: timedOut || next.timedOut,\n );\n }\n}\n\nclass _OpenClawBundleSpec {\n final String version;\n final int defaultPort;\n final String defaultModel;\n final String? defaultModelOverride;\n final String nodeExecutable;\n final String cliEntrypoint;\n final List<String> gatewayArgs;\n final bool downloaded;\n\n const _OpenClawBundleSpec({\n required this.version,\n required this.defaultPort,\n required this.defaultModel,\n this.defaultModelOverride,\n required this.nodeExecutable,\n required this.cliEntrypoint,\n required this.gatewayArgs,\n this.downloaded = false,\n });\n\n Map<String, dynamic> toJson() => {\n 'schema': 1,\n 'version': version,\n 'defaultPort': defaultPort,\n 'defaultModel': defaultModel,\n if (defaultModelOverride != null)\n 'defaultModelOverride': defaultModelOverride,\n 'nodeExecutable': nodeExecutable,\n 'cliEntrypoint': cliEntrypoint,\n 'gatewayArgs': gatewayArgs,\n 'downloaded': downloaded,\n };\n}\n\nclass _OpenClawRuntimeRelease {\n final String version;\n final Uri archiveUri;\n final String sha256Hex;\n final int size;\n final int defaultPort;\n final String defaultModel;\n final String? defaultModelOverride;\n final String nodeExecutable;\n final String cliEntrypoint;\n final List<String> gatewayArgs;\n\n const _OpenClawRuntimeRelease({\n required this.version,\n required this.archiveUri,\n required this.sha256Hex,\n required this.size,\n required this.defaultPort,\n required this.defaultModel,\n this.defaultModelOverride,\n required this.nodeExecutable,\n required this.cliEntrypoint,\n required this.gatewayArgs,\n });\n\n _OpenClawBundleSpec toBundleSpec() => _OpenClawBundleSpec(\n version: version,\n defaultPort: defaultPort,\n defaultModel: defaultModel,\n defaultModelOverride: defaultModelOverride,\n nodeExecutable: nodeExecutable,\n cliEntrypoint: cliEntrypoint,\n gatewayArgs: gatewayArgs,\n downloaded: true,\n );\n}\n\nclass _DesktopToolsLaunch {\n final Uri? uri;\n final String? token;\n final Map<String, dynamic> statusJson;\n\n const _DesktopToolsLaunch({\n required this.uri,\n required this.token,\n required this.statusJson,\n });\n}\n\nclass _OpenClawCliLaunch {\n final _OpenClawBundleSpec spec;\n final Directory runtimeDir;\n final Directory stateRoot;\n final File configPath;\n final String nodePath;\n final String cliPath;\n final int port;\n final String token;\n\n const _OpenClawCliLaunch({\n required this.spec,\n required this.runtimeDir,\n required this.stateRoot,\n required this.configPath,\n required this.nodePath,\n required this.cliPath,\n required this.port,\n required this.token,\n });\n}\n\nclass OpenClawRuntimeException implements Exception {\n final String message;\n\n const OpenClawRuntimeException(this.message);\n\n @override\n String toString() => message;\n}\n\nclass OpenClawRuntime {\n OpenClawRuntime._();\n\n static final OpenClawRuntime instance = OpenClawRuntime._();\n\n static const String _manifestAsset = 'assets/openclaw/bundle_manifest.json';\n static const String _runtimeDirOverrideDefine = String.fromEnvironment(\n 'DACHENG_OPENCLAW_RUNTIME_DIR',\n );\n static const String _defaultGatewayModel =\n AppSettings.defaultOpenClawGatewayModel;\n static const String _defaultDeepSeekModel =\n AppSettings.defaultOpenClawDeepSeekModel;\n static const String _backendDeepSeekProviderId = 'dacheng-deepseek-proxy';\n static const String _desktopToolsPluginId = 'dacheng-desktop-tools';\n static const String _weChatPluginId = 'openclaw-weixin';\n static const String _weChatPluginPackage = '@tencent-weixin/openclaw-weixin';\n static const Duration _startupTimeout = Duration(seconds: 120);\n static const Duration _probeTimeout = Duration(seconds: 3);\n static const Duration _cliDefaultTimeout = Duration(seconds: 45);\n\n Process? _process;\n Future<OpenClawGatewayTarget>? _starting;\n String _processAuthToken = '';\n OpenClawRuntimeStatus? _lastStatus;\n final List<String> _recentLogs = <String>[];\n final OpenClawPortResolver _portResolver = const OpenClawPortResolver();\n\n Future<OpenClawRuntimeStatus> getStatus({bool probe = true}) async {\n final platformKey = _platformKey;\n final port = await AppSettings.getOpenClawGatewayPort();\n _diag(\n 'status.start',\n data: {'platformKey': platformKey, 'port': port, 'probe': probe},\n );\n\n if (platformKey == null) {\n return _remember(\n OpenClawRuntimeStatus(\n state: OpenClawRuntimeState.unsupported,\n message: '当前平台不是 macOS / Windows / Linux 桌面端',\n port: port,\n checkedAt: DateTime.now(),\n ),\n );\n }\n\n try {\n final spec = await _loadSpec(platformKey, checkUpdates: false);\n final overrideRuntimeDir = await _runtimeDirOverride(spec, platformKey);\n final bundledRuntimeDir = overrideRuntimeDir == null\n ? await _bundledRuntimeDir(spec, platformKey)\n : null;\n final runtimeDir =\n overrideRuntimeDir ??\n bundledRuntimeDir ??\n await _runtimeDir(spec, platformKey);\n final nodePath = p.join(runtimeDir.path, spec.nodeExecutable);\n final cliPath = p.join(runtimeDir.path, spec.cliEntrypoint);\n final nodeExists = await File(nodePath).exists();\n final cliExists = await File(cliPath).exists();\n _diag(\n 'status.paths',\n data: {\n 'platformKey': platformKey,\n 'runtimeDir': runtimeDir.path,\n 'nodePath': nodePath,\n 'nodeExists': nodeExists,\n 'cliPath': cliPath,\n 'cliExists': cliExists,\n 'runtimeSource': overrideRuntimeDir != null\n ? 'override'\n : (bundledRuntimeDir == null ? 'cache' : 'bundle'),\n },\n );\n\n if (!nodeExists || !cliExists) {\n final hasBundleAssets = await _hasRequiredBundleAssets(\n platformKey,\n spec,\n );\n _diag(\n 'status.bundle-assets',\n data: {\n 'platformKey': platformKey,\n 'hasBundleAssets': hasBundleAssets,\n 'nodeAsset': 'assets/openclaw/$platformKey/${spec.nodeExecutable}',\n 'cliAsset': 'assets/openclaw/$platformKey/${spec.cliEntrypoint}',\n },\n );\n return _remember(\n OpenClawRuntimeStatus(\n state: hasBundleAssets\n ? OpenClawRuntimeState.stopped\n : OpenClawRuntimeState.notBundled,\n message: hasBundleAssets\n ? '内置 OpenClaw 已随 App 打包,尚未释放或启动'\n : '当前 App 包未包含 $platformKey 的 OpenClaw runtime',\n port: port,\n platformKey: platformKey,\n runtimePath: runtimeDir.path,\n checkedAt: DateTime.now(),\n ),\n );\n }\n\n if (probe) {\n final healthy = await _probe(\n port,\n await AppSettings.getOpenClawGatewayToken(),\n );\n if (healthy) {\n return _remember(\n OpenClawRuntimeStatus(\n state: OpenClawRuntimeState.running,\n message: '本机 OpenClaw Gateway 正在运行',\n port: port,\n platformKey: platformKey,\n runtimePath: runtimeDir.path,\n checkedAt: DateTime.now(),\n ),\n );\n }\n }\n\n return _remember(\n OpenClawRuntimeStatus(\n state: _process == null\n ? OpenClawRuntimeState.stopped\n : OpenClawRuntimeState.starting,\n message: _process == null ? '本机 OpenClaw 未启动' : '本机 OpenClaw 正在启动',\n port: port,\n platformKey: platformKey,\n runtimePath: runtimeDir.path,\n checkedAt: DateTime.now(),\n ),\n );\n } catch (error) {\n _diag('status.error', error: error);\n return _remember(\n OpenClawRuntimeStatus(\n state: OpenClawRuntimeState.failed,\n message: error.toString(),\n port: port,\n platformKey: platformKey,\n checkedAt: DateTime.now(),\n ),\n );\n }\n }\n\n Future<OpenClawGatewayTarget> ensureStarted({\n String? authToken,\n String? username,\n bool isMember = false,\n }) {\n _starting ??= _ensureStartedInternal(\n authToken: authToken,\n username: username,\n isMember: isMember,\n ).whenComplete(() => _starting = null);\n return _starting!;\n }\n\n Future<OpenClawRuntimeStatus> restart() async {\n await stop();\n try {\n await ensureStarted();\n return getStatus();\n } catch (error) {\n return _remember(\n OpenClawRuntimeStatus(\n state: OpenClawRuntimeState.failed,\n message: error.toString(),\n port: await AppSettings.getOpenClawGatewayPort(),\n platformKey: _platformKey,\n checkedAt: DateTime.now(),\n ),\n );\n }\n }\n\n Future<void> stop() async {\n final process = _process;\n _process = null;\n _processAuthToken = '';\n if (process == null) return;\n process.kill();\n try {\n await process.exitCode.timeout(const Duration(seconds: 5));\n } catch (_) {\n process.kill();\n }\n }\n\n Future<OpenClawCliResult> createMobilePairingCode({bool remote = true}) {\n return runCli([\n 'qr',\n if (remote) '--remote',\n '--json',\n ], timeout: const Duration(seconds: 30));\n }\n\n Future<OpenClawCliResult> loginWeChat() {\n return runCli([\n 'channels',\n 'login',\n '--channel',\n _weChatPluginId,\n ], timeout: const Duration(minutes: 3));\n }\n\n Future<OpenClawCliResult> inspectChannels() {\n return runCli([\n 'channels',\n 'status',\n '--probe',\n ], timeout: const Duration(seconds: 40));\n }\n\n Future<OpenClawCliResult> installWeChatPlugin() async {\n final install = await runCli(\n ['plugins', 'install', _weChatPluginPackage, '--force'],\n timeout: const Duration(minutes: 3),\n ensureGateway: false,\n );\n if (!install.succeeded) return install;\n final enable = await runCli(\n ['config', 'set', 'plugins.entries.$_weChatPluginId.enabled', 'true'],\n timeout: const Duration(seconds: 30),\n ensureGateway: false,\n );\n return install.append(enable);\n }\n\n Future<OpenClawCliResult> runCli(\n List<String> args, {\n Duration timeout = _cliDefaultTimeout,\n bool ensureGateway = true,\n }) async {\n if (args.isEmpty) {\n throw const OpenClawRuntimeException('OpenClaw CLI 参数不能为空');\n }\n final launch = await _prepareCliLaunch(ensureGateway: ensureGateway);\n final processArgs = <String>[launch.cliPath, ...args];\n _diag(\n 'cli.start',\n data: {\n 'args': args,\n 'runtimeDir': launch.runtimeDir.path,\n 'configPath': launch.configPath.path,\n 'timeoutSeconds': timeout.inSeconds,\n },\n );\n\n final process = await Process.start(\n launch.nodePath,\n processArgs,\n workingDirectory: launch.runtimeDir.path,\n environment: _buildOpenClawEnvironment(\n runtimeDir: launch.runtimeDir,\n stateRoot: launch.stateRoot,\n configPath: launch.configPath,\n port: launch.port,\n token: launch.token,\n ),\n mode: ProcessStartMode.normal,\n runInShell: false,\n );\n final stdoutBuffer = StringBuffer();\n final stderrBuffer = StringBuffer();\n final stdoutSub = process.stdout\n .transform(utf8.decoder)\n .listen((chunk) => _appendBounded(stdoutBuffer, chunk));\n final stderrSub = process.stderr\n .transform(utf8.decoder)\n .listen((chunk) => _appendBounded(stderrBuffer, chunk));\n\n var timedOut = false;\n int exitCode;\n try {\n exitCode = await process.exitCode.timeout(timeout);\n } on TimeoutException {\n timedOut = true;\n process.kill();\n exitCode = await process.exitCode.timeout(\n const Duration(seconds: 5),\n onTimeout: () => -1,\n );\n } finally {\n await stdoutSub.cancel();\n await stderrSub.cancel();\n }\n\n final result = OpenClawCliResult(\n args: args,\n exitCode: exitCode,\n stdout: stdoutBuffer.toString(),\n stderr: stderrBuffer.toString(),\n timedOut: timedOut,\n );\n _diag(\n result.succeeded ? 'cli.complete' : 'cli.failed',\n data: {\n 'args': args,\n 'exitCode': exitCode,\n 'timedOut': timedOut,\n 'stdoutPreview': _previewProcessOutput(result.stdout),\n 'stderrPreview': _previewProcessOutput(result.stderr),\n },\n );\n return result;\n }\n\n Future<OpenClawGatewayTarget> _ensureStartedInternal({\n String? authToken,\n String? username,\n bool isMember = false,\n }) async {\n final platformKey = _platformKey;\n _diag('ensure-start.start', data: {'platformKey': platformKey});\n if (platformKey == null) {\n throw const OpenClawRuntimeException('当前平台不支持内置 OpenClaw Gateway');\n }\n\n final spec = await _loadSpec(platformKey, checkUpdates: true);\n final requestedPort = await AppSettings.getOpenClawGatewayPort(\n defaultValue: spec.defaultPort,\n );\n final token = await AppSettings.getOpenClawGatewayToken();\n final savedModel = await AppSettings.getOpenClawModel(\n defaultValue: spec.defaultModel,\n );\n final model = _gatewayChatModel(savedModel);\n final requestedAuthToken = authToken?.trim() ?? '';\n final backendDeepSeekModel = _backendDeepSeekModelRef(\n await AppSettings.getOpenClawDeepSeekModel(\n defaultValue: _defaultDeepSeekModelFor(spec.defaultModel),\n ),\n );\n final deepSeekProxyBaseUrl = _deepSeekProxyBaseUrl();\n final modelOverride = await AppSettings.getOpenClawModelOverride(\n defaultValue: spec.defaultModelOverride ?? '',\n );\n final remoteGatewayUrl = await AppSettings.getOpenClawRemoteGatewayUrl();\n final desktopTools = await _ensureDesktopTools();\n _diag(\n 'ensure-start.config',\n data: {\n 'platformKey': platformKey,\n 'requestedPort': requestedPort,\n 'model': model,\n 'rawModel': savedModel,\n 'backendDeepSeekModel': backendDeepSeekModel,\n 'deepSeekProxyBaseUrl': deepSeekProxyBaseUrl,\n 'hasAuthToken': requestedAuthToken.isNotEmpty,\n 'modelOverrideSet': modelOverride.trim().isNotEmpty,\n 'remoteGatewayUrlSet': remoteGatewayUrl.trim().isNotEmpty,\n 'desktopToolsUri': desktopTools.uri?.toString(),\n 'desktopToolsStatus': desktopTools.statusJson,\n },\n );\n\n final canReuseStartedProcess =\n _process != null && _processAuthToken == requestedAuthToken;\n if (canReuseStartedProcess &&\n await _probe(requestedPort, token, context: 'pre-start')) {\n _diag('ensure-start.reuse-existing', data: {'port': requestedPort});\n return OpenClawGatewayTarget(\n baseUri: Uri.parse('http://127.0.0.1:$requestedPort'),\n token: token,\n model: model,\n modelOverride: modelOverride.trim().isEmpty\n ? null\n : modelOverride.trim(),\n desktopToolsUri: desktopTools.uri,\n desktopToolsToken: desktopTools.token,\n desktopToolsStatus: desktopTools.statusJson,\n );\n } else if (_process != null) {\n _diag(\n 'ensure-start.restart-auth-changed',\n data: {\n 'hasPreviousAuthToken': _processAuthToken.isNotEmpty,\n 'hasRequestedAuthToken': requestedAuthToken.isNotEmpty,\n },\n );\n await stop();\n }\n\n final runtimeDir = await _prepareBundle(spec, platformKey);\n final stateRoot = await _stateRoot();\n final desktopToolsPluginDir = await _ensureDesktopToolsPlugin(\n stateRoot: stateRoot,\n );\n if (_process != null) {\n _diag(\n 'process.stop-stale-before-start',\n data: {'requestedPort': requestedPort},\n );\n await stop();\n }\n final portCandidate = await _portResolver.resolve(requestedPort);\n final port = portCandidate.port;\n if (portCandidate.isFallback) {\n await AppSettings.setOpenClawGatewayPort(port);\n _diag(\n 'ensure-start.fallback-port',\n data: {\n 'requestedPort': requestedPort,\n 'selectedPort': port,\n if (portCandidate.reason != null) 'reason': portCandidate.reason,\n },\n );\n } else {\n _diag(\n 'ensure-start.port-bindable',\n data: {'requestedPort': requestedPort, 'selectedPort': port},\n );\n }\n final configPath = await _ensureConfigFile(\n runtimeDir: runtimeDir,\n stateRoot: stateRoot,\n port: port,\n token: token,\n backendDeepSeekModel: backendDeepSeekModel,\n deepSeekProxyBaseUrl: deepSeekProxyBaseUrl,\n remoteGatewayUrl: remoteGatewayUrl,\n extraPluginLoadPaths: [desktopToolsPluginDir.path],\n );\n await _ensureAgentModelConfig(\n stateRoot: stateRoot,\n deepSeekProxyBaseUrl: deepSeekProxyBaseUrl,\n );\n final desktopToolsManifestPath = await _ensureDesktopToolsManifest(\n stateRoot: stateRoot,\n desktopTools: desktopTools,\n );\n\n final nodePath = p.join(runtimeDir.path, spec.nodeExecutable);\n final cliPath = p.join(runtimeDir.path, spec.cliEntrypoint);\n if (!await File(nodePath).exists()) {\n _diag('ensure-start.missing-node', data: {'nodePath': nodePath});\n throw OpenClawRuntimeException('OpenClaw 内置 Node 不存在: $nodePath');\n }\n if (!await File(cliPath).exists()) {\n _diag('ensure-start.missing-cli', data: {'cliPath': cliPath});\n throw OpenClawRuntimeException('OpenClaw CLI 入口不存在: $cliPath');\n }\n\n _remember(\n OpenClawRuntimeStatus(\n state: OpenClawRuntimeState.starting,\n message: '正在启动内置 OpenClaw Gateway',\n port: port,\n platformKey: platformKey,\n runtimePath: runtimeDir.path,\n checkedAt: DateTime.now(),\n ),\n );\n\n final args = <String>[\n cliPath,\n ...spec.gatewayArgs.map((arg) => arg.replaceAll('{port}', '$port')),\n ];\n _recentLogs.clear();\n _diag(\n 'process.starting',\n data: {\n 'nodePath': nodePath,\n 'args': args,\n 'workingDirectory': runtimeDir.path,\n 'configPath': configPath.path,\n 'desktopToolsPluginPath': desktopToolsPluginDir.path,\n 'desktopToolsManifestPath': desktopToolsManifestPath.path,\n 'requestedPort': requestedPort,\n 'selectedPort': port,\n },\n );\n\n final env =\n _buildOpenClawEnvironment(\n runtimeDir: runtimeDir,\n stateRoot: stateRoot,\n configPath: configPath,\n port: port,\n token: token,\n )..addAll({\n 'DACHENG_DESKTOP_TOOLS_ENABLED': desktopTools.uri == null ? '0' : '1',\n 'DACHENG_DESKTOP_TOOLS_MANIFEST': desktopToolsManifestPath.path,\n if (desktopTools.uri != null)\n 'DACHENG_DESKTOP_TOOLS_URL': desktopTools.uri.toString(),\n if (desktopTools.token != null)\n 'DACHENG_DESKTOP_TOOLS_TOKEN': desktopTools.token!,\n 'DACHENG_APP_RUNTIME': '1',\n // This is a process-local OpenClaw capability, never a Mahayana\n // account credential. Account HTTP stays inside the Rust runtime.\n 'DACHENG_AUTH_TOKEN': token,\n if (username != null && username.isNotEmpty)\n 'DACHENG_USERNAME': username,\n 'DACHENG_IS_MEMBER': isMember ? '1' : '0',\n 'DACHENG_OPENCLAW_PROXY_TOKEN': 'dacheng-openclaw-proxy',\n });\n\n _process = await Process.start(\n nodePath,\n args,\n workingDirectory: runtimeDir.path,\n environment: env,\n mode: ProcessStartMode.normal,\n runInShell: false,\n );\n _processAuthToken = requestedAuthToken;\n _captureLogs(_process!);\n _diag('process.started', data: {'pid': _process!.pid, 'port': port});\n\n final deadline = DateTime.now().add(_startupTimeout);\n while (DateTime.now().isBefore(deadline)) {\n if (await _probe(port, token, context: 'startup')) {\n _diag('process.ready', data: {'pid': _process?.pid, 'port': port});\n _remember(\n OpenClawRuntimeStatus(\n state: OpenClawRuntimeState.running,\n message: '本机 OpenClaw Gateway 已启动',\n port: port,\n platformKey: platformKey,\n runtimePath: runtimeDir.path,\n desktopToolsStatus: desktopTools.statusJson,\n checkedAt: DateTime.now(),\n ),\n );\n return OpenClawGatewayTarget(\n baseUri: Uri.parse('http://127.0.0.1:$port'),\n token: token,\n model: model,\n modelOverride: modelOverride.trim().isEmpty\n ? null\n : modelOverride.trim(),\n desktopToolsUri: desktopTools.uri,\n desktopToolsToken: desktopTools.token,\n desktopToolsStatus: desktopTools.statusJson,\n );\n }\n\n if (_process != null) {\n final exited = await _process!.exitCode.timeout(\n const Duration(milliseconds: 1),\n onTimeout: () => -999999,\n );\n if (exited != -999999) {\n final logs = _recentLogs.take(12).join('\\n');\n _diag(\n 'process.exited-early',\n data: {\n 'exitCode': exited,\n 'recentLogs': _recentLogs.take(12).toList(),\n },\n );\n throw OpenClawRuntimeException(\n 'OpenClaw Gateway 提前退出,exitCode=$exited${logs.isEmpty ? '' : '\\n$logs'}',\n );\n }\n }\n if (_recentLogsContainRuntimeModuleFailure()) {\n final logs = _recentLogs.take(12).join('\\n');\n _diag(\n 'process.runtime-module-failure',\n data: {'recentLogs': _recentLogs.take(12).toList()},\n );\n await stop();\n throw OpenClawRuntimeException(\n 'OpenClaw runtime 模块缓存异常${logs.isEmpty ? '' : '\\n$logs'}',\n );\n }\n await Future<void>.delayed(const Duration(milliseconds: 450));\n }\n\n final logs = _recentLogs.take(12).join('\\n');\n _diag(\n 'process.startup-timeout',\n data: {\n 'timeoutSeconds': _startupTimeout.inSeconds,\n 'recentLogs': _recentLogs.take(12).toList(),\n },\n );\n throw OpenClawRuntimeException(\n 'OpenClaw Gateway 启动超时${logs.isEmpty ? '' : '\\n$logs'}',\n );\n }\n\n Future<_OpenClawBundleSpec> _loadSpec(\n String platformKey, {\n bool checkUpdates = false,\n }) async {\n final raw = await rootBundle.loadString(_manifestAsset);\n final decoded = jsonDecode(raw) as Map<String, dynamic>;\n final platforms = Map<String, dynamic>.from(\n decoded['platforms'] as Map? ?? const {},\n );\n final platform = Map<String, dynamic>.from(\n platforms[platformKey] as Map? ?? const {},\n );\n if (platform.isEmpty) {\n throw OpenClawRuntimeException('bundle_manifest.json 缺少 $platformKey 配置');\n }\n\n final gatewayArgs =\n (platform['gatewayArgs'] as List? ??\n decoded['gatewayArgs'] as List? ??\n const ['gateway', '--port', '{port}'])\n .map((item) => item.toString())\n .toList();\n\n final spec = _OpenClawBundleSpec(\n version: (decoded['version'] ?? 'dev').toString(),\n defaultPort: _readInt(decoded['defaultPort']) ?? 18789,\n defaultModel: (decoded['defaultModel'] ?? 'openclaw/default').toString(),\n defaultModelOverride: decoded['defaultModelOverride']?.toString(),\n nodeExecutable: (platform['nodeExecutable'] ?? 'node/bin/node')\n .toString(),\n cliEntrypoint: (platform['cliEntrypoint'] ?? 'openclaw/bin/openclaw.js')\n .toString(),\n gatewayArgs: gatewayArgs,\n );\n _diag(\n 'manifest.loaded',\n data: {\n 'platformKey': platformKey,\n 'version': spec.version,\n 'defaultPort': spec.defaultPort,\n 'defaultModel': spec.defaultModel,\n 'nodeExecutable': spec.nodeExecutable,\n 'cliEntrypoint': spec.cliEntrypoint,\n 'gatewayArgs': spec.gatewayArgs,\n },\n );\n final allowDownloadedRuntime = await _allowDownloadedRuntime(platformKey);\n var effectiveSpec = spec;\n if (allowDownloadedRuntime) {\n effectiveSpec =\n await _loadActiveDownloadedSpec(platformKey, fallbackSpec: spec) ??\n spec;\n } else {\n await _clearActiveDownloadedRuntimeSpec(\n platformKey: platformKey,\n reason: 'macos-sandbox',\n );\n }\n if (checkUpdates && allowDownloadedRuntime) {\n effectiveSpec =\n await _maybeInstallRuntimeUpdate(\n platformKey: platformKey,\n currentSpec: effectiveSpec,\n ) ??\n effectiveSpec;\n } else if (checkUpdates) {\n _diag(\n 'runtime-update.skip-disabled',\n data: {'platformKey': platformKey, 'reason': 'macos-sandbox'},\n );\n }\n return effectiveSpec;\n }\n\n Future<bool> _allowDownloadedRuntime(String platformKey) async {\n if (!Platform.isMacOS) return true;\n\n final support = await getApplicationSupportDirectory();\n final sandboxContainerId =\n Platform.environment['APP_SANDBOX_CONTAINER_ID']?.trim() ?? '';\n final pathLooksSandboxed = p\n .split(p.normalize(support.path))\n .contains('Containers');\n final sandboxed = sandboxContainerId.isNotEmpty || pathLooksSandboxed;\n if (!sandboxed) return true;\n\n // App Store/TestFlight sandboxed apps cannot reliably execute downloaded\n // Mach-O payloads. Use the bundled runtime that was signed with inherited\n // sandbox entitlements during packaging.\n _diag(\n 'runtime-update.disabled-macos-sandbox',\n data: {\n 'platformKey': platformKey,\n 'supportPath': support.path,\n 'hasSandboxContainerId': sandboxContainerId.isNotEmpty,\n },\n );\n return false;\n }\n\n Future<void> _clearActiveDownloadedRuntimeSpec({\n required String platformKey,\n required String reason,\n }) async {\n final saved = await AppSettings.getOpenClawActiveRuntimeSpec();\n if (saved == null) return;\n await AppSettings.clearOpenClawActiveRuntimeSpec();\n _diag(\n 'runtime-update.active-cleared',\n data: {\n 'platformKey': platformKey,\n 'reason': reason,\n 'version': saved['version']?.toString(),\n 'downloaded': saved['downloaded'] == true,\n },\n );\n }\n\n Future<_OpenClawBundleSpec?> _loadActiveDownloadedSpec(\n String platformKey, {\n required _OpenClawBundleSpec fallbackSpec,\n }) async {\n final saved = await AppSettings.getOpenClawActiveRuntimeSpec();\n if (saved == null) return null;\n final spec = _specFromJson(saved, downloaded: true);\n if (spec == null) {\n await AppSettings.clearOpenClawActiveRuntimeSpec();\n return null;\n }\n final runtimeDir = await _cachedRuntimeDirIfComplete(spec, platformKey);\n if (runtimeDir == null) {\n await AppSettings.clearOpenClawActiveRuntimeSpec();\n _diag(\n 'runtime-update.active-missing',\n data: {'platformKey': platformKey, 'version': spec.version},\n );\n return null;\n }\n if (_isRuntimeVersionNewer(fallbackSpec.version, spec.version)) {\n await AppSettings.clearOpenClawActiveRuntimeSpec();\n _diag(\n 'runtime-update.active-cleared',\n data: {\n 'platformKey': platformKey,\n 'reason': 'bundled-newer',\n 'version': spec.version,\n 'bundledVersion': fallbackSpec.version,\n },\n );\n return null;\n }\n _diag(\n 'runtime-update.active-loaded',\n data: {\n 'platformKey': platformKey,\n 'version': spec.version,\n 'runtimeDir': runtimeDir.path,\n 'bundledVersion': fallbackSpec.version,\n },\n );\n return spec;\n }\n\n bool _isRuntimeVersionNewer(String candidate, String current) {\n final candidateRank = _runtimeVersionRank(candidate);\n final currentRank = _runtimeVersionRank(current);\n final length = math.max(candidateRank.length, currentRank.length);\n for (var i = 0; i < length; i++) {\n final a = i < candidateRank.length ? candidateRank[i] : 0;\n final b = i < currentRank.length ? currentRank[i] : 0;\n if (a != b) return a > b;\n }\n return false;\n }\n\n List<int> _runtimeVersionRank(String version) {\n return RegExp(r'\\d+')\n .allMatches(version)\n .map((match) => int.tryParse(match.group(0) ?? '') ?? 0)\n .toList(growable: false);\n }\n\n _OpenClawBundleSpec? _specFromJson(\n Map<String, dynamic> json, {\n required bool downloaded,\n }) {\n final version = json['version']?.toString().trim() ?? '';\n final nodeExecutable = json['nodeExecutable']?.toString().trim() ?? '';\n final cliEntrypoint = json['cliEntrypoint']?.toString().trim() ?? '';\n if (version.isEmpty || nodeExecutable.isEmpty || cliEntrypoint.isEmpty) {\n return null;\n }\n return _OpenClawBundleSpec(\n version: version,\n defaultPort: _readInt(json['defaultPort']) ?? 18789,\n defaultModel: (json['defaultModel'] ?? _defaultGatewayModel).toString(),\n defaultModelOverride: json['defaultModelOverride']?.toString(),\n nodeExecutable: nodeExecutable,\n cliEntrypoint: cliEntrypoint,\n gatewayArgs:\n (json['gatewayArgs'] as List? ??\n const ['gateway', '--port', '{port}', '--force'])\n .map((item) => item.toString())\n .toList(),\n downloaded: downloaded,\n );\n }\n\n Future<_OpenClawBundleSpec?> _maybeInstallRuntimeUpdate({\n required String platformKey,\n required _OpenClawBundleSpec currentSpec,\n }) async {\n if (_configuredRuntimeDirOverride(platformKey) != null) {\n _diag('runtime-update.skip-override', data: {'platformKey': platformKey});\n return null;\n }\n\n try {\n final release = await _fetchRuntimeRelease(\n platformKey: platformKey,\n currentVersion: currentSpec.version,\n );\n if (release == null || release.version == currentSpec.version) {\n return null;\n }\n\n final releaseSpec = release.toBundleSpec();\n final cached = await _cachedRuntimeDirIfComplete(\n releaseSpec,\n platformKey,\n );\n if (cached != null) {\n await AppSettings.setOpenClawActiveRuntimeSpec(releaseSpec.toJson());\n _diag(\n 'runtime-update.use-cached',\n data: {\n 'platformKey': platformKey,\n 'version': release.version,\n 'runtimeDir': cached.path,\n },\n );\n return releaseSpec;\n }\n\n return await _downloadAndInstallRuntimeUpdate(\n release: release,\n platformKey: platformKey,\n );\n } catch (error, stackTrace) {\n _diag(\n 'runtime-update.failed',\n data: {\n 'platformKey': platformKey,\n 'currentVersion': currentSpec.version,\n 'error': error.toString(),\n },\n error: error,\n stackTrace: stackTrace,\n );\n return null;\n }\n }\n\n Future<_OpenClawRuntimeRelease?> _fetchRuntimeRelease({\n required String platformKey,\n required String currentVersion,\n }) async {\n final baseUrl = AppConfig.currentAiBackendUrl.replaceFirst(\n RegExp(r'/+$'),\n '',\n );\n final uri = Uri.parse('$baseUrl/api/openclaw/runtime/manifest').replace(\n queryParameters: {\n 'platform': platformKey,\n 'currentVersion': currentVersion,\n 'appVersion': AppConfig.appVersion,\n },\n );\n final response = await http\n .get(uri, headers: {'Accept': 'application/json'})\n .timeout(const Duration(seconds: 20));\n if (response.statusCode < 200 || response.statusCode >= 300) {\n _diag(\n 'runtime-update.manifest-http',\n data: {'statusCode': response.statusCode, 'uri': uri.toString()},\n );\n return null;\n }\n final decoded = _decodeJsonOrNull(response.body);\n if (decoded is! Map) return null;\n final payload = Map<String, dynamic>.from(decoded);\n if (payload['updateAvailable'] != true) {\n _diag(\n 'runtime-update.none',\n data: {\n 'platformKey': platformKey,\n 'reason': payload['reason']?.toString(),\n 'currentVersion': currentVersion,\n 'latestVersion': payload['latestVersion']?.toString(),\n },\n );\n return null;\n }\n final latest = payload['latest'];\n if (latest is! Map) return null;\n return _releaseFromJson(Map<String, dynamic>.from(latest));\n }\n\n _OpenClawRuntimeRelease? _releaseFromJson(Map<String, dynamic> json) {\n final version = json['version']?.toString().trim() ?? '';\n final archiveUrl = json['archiveUrl']?.toString().trim() ?? '';\n final sha256Hex = json['sha256']?.toString().trim().toLowerCase() ?? '';\n final archiveUri = Uri.tryParse(archiveUrl);\n final gatewayArgs =\n (json['gatewayArgs'] as List? ??\n const ['gateway', '--port', '{port}', '--force'])\n .map((item) => item.toString())\n .toList();\n if (version.isEmpty ||\n archiveUri == null ||\n !archiveUri.hasScheme ||\n !RegExp(r'^[a-f0-9]{64}$').hasMatch(sha256Hex)) {\n _diag(\n 'runtime-update.invalid-release',\n data: {\n 'version': version,\n 'archiveUrlSet': archiveUrl.isNotEmpty,\n 'sha256Length': sha256Hex.length,\n },\n );\n return null;\n }\n return _OpenClawRuntimeRelease(\n version: version,\n archiveUri: archiveUri,\n sha256Hex: sha256Hex,\n size: _readInt(json['size']) ?? 0,\n defaultPort: _readInt(json['defaultPort']) ?? 18789,\n defaultModel: (json['defaultModel'] ?? _defaultGatewayModel).toString(),\n defaultModelOverride: json['defaultModelOverride']?.toString(),\n nodeExecutable: (json['nodeExecutable'] ?? 'node/bin/node').toString(),\n cliEntrypoint: (json['cliEntrypoint'] ?? 'openclaw/openclaw.mjs')\n .toString(),\n gatewayArgs: gatewayArgs,\n );\n }\n\n Future<_OpenClawBundleSpec> _downloadAndInstallRuntimeUpdate({\n required _OpenClawRuntimeRelease release,\n required String platformKey,\n }) async {\n final spec = release.toBundleSpec();\n final runtimeDir = await _runtimeDir(spec, platformKey);\n final support = await getApplicationSupportDirectory();\n final downloadDir = Directory(p.join(support.path, 'openclaw_downloads'));\n await downloadDir.create(recursive: true);\n final archivePath = p.join(\n downloadDir.path,\n '${_safePathPart(release.version)}-$platformKey${_archiveExtension(release.archiveUri)}',\n );\n final archiveFile = File(archivePath);\n final digest = await _downloadRuntimeArchive(release, archiveFile);\n if (digest != release.sha256Hex) {\n await _deleteFileIfExists(archiveFile);\n throw OpenClawRuntimeException(\n 'OpenClaw runtime 校验失败: expected=${release.sha256Hex}, actual=$digest',\n );\n }\n\n final extractDir = Directory(\n p.join(\n downloadDir.path,\n '${_safePathPart(release.version)}-$platformKey-extract',\n ),\n );\n final stagingDir = Directory(\n p.join(\n downloadDir.path,\n '${_safePathPart(release.version)}-$platformKey-stage',\n ),\n );\n if (await extractDir.exists()) await extractDir.delete(recursive: true);\n if (await stagingDir.exists()) await stagingDir.delete(recursive: true);\n await extractDir.create(recursive: true);\n await _extractRuntimeArchive(archiveFile, extractDir);\n\n final extractedRoot = await _findExtractedRuntimeRoot(\n extractDir: extractDir,\n spec: spec,\n );\n if (extractedRoot == null) {\n throw OpenClawRuntimeException('下载的 OpenClaw runtime 不包含必要的 node/cli 文件');\n }\n\n await runtimeDir.parent.create(recursive: true);\n if (await runtimeDir.exists()) await runtimeDir.delete(recursive: true);\n await _moveOrCopyRuntimeDirectory(extractedRoot, stagingDir);\n await stagingDir.rename(runtimeDir.path);\n final marker = File(p.join(runtimeDir.path, '.bundle_ready'));\n await marker.writeAsString(\n const JsonEncoder.withIndent(' ').convert({\n 'installedAt': DateTime.now().toIso8601String(),\n 'version': release.version,\n 'platform': platformKey,\n 'sha256': digest,\n 'source': release.archiveUri.toString(),\n }),\n );\n await _repairRuntimeLaunchMetadata(\n runtimeDir: runtimeDir,\n nodePath: File(p.join(runtimeDir.path, spec.nodeExecutable)),\n platformKey: platformKey,\n cached: false,\n );\n await AppSettings.setOpenClawActiveRuntimeSpec(spec.toJson());\n await _deleteDirectoryIfExists(extractDir);\n await _deleteFileIfExists(archiveFile);\n _diag(\n 'runtime-update.installed',\n data: {\n 'platformKey': platformKey,\n 'version': release.version,\n 'runtimeDir': runtimeDir.path,\n 'size': release.size,\n },\n );\n return spec;\n }\n\n Future<String> _downloadRuntimeArchive(\n _OpenClawRuntimeRelease release,\n File destination,\n ) async {\n final client = http.Client();\n IOSink? output;\n try {\n final request = http.Request('GET', release.archiveUri)\n ..headers['Accept'] = 'application/octet-stream';\n final response = await client\n .send(request)\n .timeout(const Duration(seconds: 20));\n if (response.statusCode < 200 || response.statusCode >= 300) {\n throw OpenClawRuntimeException(\n 'OpenClaw runtime 下载失败 (${response.statusCode})',\n );\n }\n await destination.parent.create(recursive: true);\n output = destination.openWrite();\n final digestSink = AccumulatorSink<Digest>();\n final hashSink = sha256.startChunkedConversion(digestSink);\n var bytes = 0;\n await for (final chunk in response.stream.timeout(\n const Duration(minutes: 5),\n )) {\n bytes += chunk.length;\n hashSink.add(chunk);\n output.add(chunk);\n }\n await output.flush();\n await output.close();\n output = null;\n hashSink.close();\n final digest = digestSink.events.single.toString();\n _diag(\n 'runtime-update.downloaded',\n data: {\n 'archiveUri': release.archiveUri.toString(),\n 'bytes': bytes,\n 'sha256': digest,\n },\n );\n return digest;\n } finally {\n await output?.close().catchError((_) {});\n client.close();\n }\n }\n\n Future<void> _extractRuntimeArchive(\n File archiveFile,\n Directory destination,\n ) async {\n final lowerPath = archiveFile.path.toLowerCase();\n if (Platform.isMacOS && lowerPath.endsWith('.zip')) {\n try {\n final result = await Process.run('/usr/bin/ditto', [\n '-x',\n '-k',\n archiveFile.path,\n destination.path,\n ]).timeout(const Duration(minutes: 5));\n _diag(\n 'runtime-update.extract-native',\n data: {\n 'archivePath': archiveFile.path,\n 'destination': destination.path,\n 'exitCode': result.exitCode,\n if ((result.stderr as Object).toString().isNotEmpty)\n 'stderr': _previewProcessOutput(result.stderr),\n },\n );\n if (result.exitCode == 0) return;\n } catch (error, stackTrace) {\n _diag(\n 'runtime-update.extract-native-failed',\n data: {\n 'archivePath': archiveFile.path,\n 'destination': destination.path,\n 'error': error.toString(),\n },\n error: error,\n stackTrace: stackTrace,\n );\n }\n }\n\n await extractFileToDisk(archiveFile.path, destination.path);\n _diag(\n 'runtime-update.extract-dart',\n data: {'archivePath': archiveFile.path, 'destination': destination.path},\n );\n }\n\n Future<Directory?> _findExtractedRuntimeRoot({\n required Directory extractDir,\n required _OpenClawBundleSpec spec,\n }) async {\n final direct = await _runtimeRootIfComplete(extractDir, spec);\n if (direct != null) return direct;\n await for (final entity in extractDir.list(followLinks: false)) {\n if (entity is! Directory) continue;\n final nested = await _runtimeRootIfComplete(entity, spec);\n if (nested != null) return nested;\n }\n return null;\n }\n\n Future<Directory?> _runtimeRootIfComplete(\n Directory dir,\n _OpenClawBundleSpec spec,\n ) async {\n final nodePath = File(p.join(dir.path, spec.nodeExecutable));\n final cliPath = File(p.join(dir.path, spec.cliEntrypoint));\n if (await nodePath.exists() && await cliPath.exists()) return dir;\n return null;\n }\n\n Future<Directory?> _cachedRuntimeDirIfComplete(\n _OpenClawBundleSpec spec,\n String platformKey,\n ) async {\n final runtimeDir = await _runtimeDir(spec, platformKey);\n final marker = File(p.join(runtimeDir.path, '.bundle_ready'));\n final nodePath = File(p.join(runtimeDir.path, spec.nodeExecutable));\n final cliPath = File(p.join(runtimeDir.path, spec.cliEntrypoint));\n if (await marker.exists() &&\n await nodePath.exists() &&\n await cliPath.exists()) {\n return runtimeDir;\n }\n return null;\n }\n\n String _safePathPart(String value) {\n final safe = value.replaceAll(RegExp(r'[^A-Za-z0-9_.-]+'), '-');\n return safe.isEmpty ? 'runtime' : safe;\n }\n\n String _archiveExtension(Uri uri) {\n final lower = uri.path.toLowerCase();\n if (lower.endsWith('.tar.gz')) return '.tar.gz';\n if (lower.endsWith('.tgz')) return '.tgz';\n if (lower.endsWith('.tar')) return '.tar';\n return '.zip';\n }\n\n Future<void> _deleteFileIfExists(File file) async {\n try {\n if (await file.exists()) await file.delete();\n } catch (_) {}\n }\n\n Future<void> _deleteDirectoryIfExists(Directory dir) async {\n try {\n if (await dir.exists()) await dir.delete(recursive: true);\n } catch (_) {}\n }\n\n Future<Directory> _prepareBundle(\n _OpenClawBundleSpec spec,\n String platformKey,\n ) async {\n final overrideRuntimeDir = await _runtimeDirOverride(spec, platformKey);\n if (overrideRuntimeDir != null) {\n final nodePath = File(\n p.join(overrideRuntimeDir.path, spec.nodeExecutable),\n );\n await _repairRuntimeLaunchMetadata(\n runtimeDir: overrideRuntimeDir,\n nodePath: nodePath,\n platformKey: platformKey,\n cached: true,\n );\n _diag(\n 'bundle.launch.override-runtime',\n data: {\n 'platformKey': platformKey,\n 'runtimeDir': overrideRuntimeDir.path,\n },\n );\n return overrideRuntimeDir;\n }\n\n if (spec.downloaded) {\n final downloadedRuntimeDir = await _cachedRuntimeDirIfComplete(\n spec,\n platformKey,\n );\n if (downloadedRuntimeDir != null) {\n await _repairRuntimeLaunchMetadata(\n runtimeDir: downloadedRuntimeDir,\n nodePath: File(\n p.join(downloadedRuntimeDir.path, spec.nodeExecutable),\n ),\n platformKey: platformKey,\n cached: true,\n );\n _diag(\n 'bundle.launch.downloaded-runtime',\n data: {\n 'platformKey': platformKey,\n 'version': spec.version,\n 'runtimeDir': downloadedRuntimeDir.path,\n },\n );\n return downloadedRuntimeDir;\n }\n await AppSettings.clearOpenClawActiveRuntimeSpec();\n throw OpenClawRuntimeException(\n '已下载的 OpenClaw runtime 不完整,已回退到内置版本,请重新启动。',\n );\n }\n\n final bundledRuntimeDir = await _bundledRuntimeDir(spec, platformKey);\n if (bundledRuntimeDir != null) {\n _diag(\n 'bundle.launch.bundled-runtime',\n data: {\n 'platformKey': platformKey,\n 'runtimeDir': bundledRuntimeDir.path,\n },\n );\n return bundledRuntimeDir;\n }\n\n if (Platform.isMacOS) {\n throw OpenClawRuntimeException(\n '当前安装包没有可执行的 $platformKey OpenClaw runtime。请先运行 scripts/build_openclaw_desktop_bundle.sh $platformKey,再通过 flutter run 启动。',\n );\n }\n\n final runtimeDir = await _runtimeDir(spec, platformKey);\n final marker = File(p.join(runtimeDir.path, '.bundle_ready'));\n final nodePath = File(p.join(runtimeDir.path, spec.nodeExecutable));\n final cliPath = File(p.join(runtimeDir.path, spec.cliEntrypoint));\n\n if (await marker.exists() &&\n await nodePath.exists() &&\n await cliPath.exists()) {\n await _repairRuntimeLaunchMetadata(\n runtimeDir: runtimeDir,\n nodePath: nodePath,\n platformKey: platformKey,\n cached: true,\n );\n _diag(\n 'bundle.prepare.cached',\n data: {\n 'platformKey': platformKey,\n 'runtimeDir': runtimeDir.path,\n 'marker': marker.path,\n },\n );\n return runtimeDir;\n }\n\n final legacyRuntimeDir = await _legacyRuntimeDir(spec, platformKey);\n if (legacyRuntimeDir.path != runtimeDir.path &&\n await File(\n p.join(legacyRuntimeDir.path, spec.nodeExecutable),\n ).exists() &&\n await File(\n p.join(legacyRuntimeDir.path, spec.cliEntrypoint),\n ).exists()) {\n _diag(\n 'bundle.prepare.migrate-legacy',\n data: {\n 'platformKey': platformKey,\n 'from': legacyRuntimeDir.path,\n 'to': runtimeDir.path,\n },\n );\n if (await runtimeDir.exists()) {\n await runtimeDir.delete(recursive: true);\n }\n await _copyRuntimeDirectory(legacyRuntimeDir, runtimeDir);\n await _repairRuntimeLaunchMetadata(\n runtimeDir: runtimeDir,\n nodePath: nodePath,\n platformKey: platformKey,\n cached: false,\n );\n await marker.writeAsString(DateTime.now().toIso8601String());\n _diag(\n 'bundle.prepare.migrate-complete',\n data: {\n 'platformKey': platformKey,\n 'runtimeDir': runtimeDir.path,\n 'nodeExists': await nodePath.exists(),\n 'cliExists': await cliPath.exists(),\n },\n );\n return runtimeDir;\n }\n\n final prefix = 'assets/openclaw/$platformKey/';\n final assets = await _listAssets(prefix);\n final hasNodeAsset = assets.contains('$prefix${spec.nodeExecutable}');\n final hasCliAsset = assets.contains('$prefix${spec.cliEntrypoint}');\n _diag(\n 'bundle.prepare.assets',\n data: {\n 'platformKey': platformKey,\n 'prefix': prefix,\n 'assetCount': assets.length,\n 'hasNodeAsset': hasNodeAsset,\n 'hasCliAsset': hasCliAsset,\n 'nodeAsset': '$prefix${spec.nodeExecutable}',\n 'cliAsset': '$prefix${spec.cliEntrypoint}',\n 'sample': assets.take(8).toList(),\n },\n );\n if (!hasNodeAsset || !hasCliAsset) {\n throw OpenClawRuntimeException(\n '当前安装包没有内置 $platformKey 的 OpenClaw runtime。请在 release 构建中运行 scripts/build_openclaw_desktop_bundle.sh 后再打包。',\n );\n }\n\n if (await runtimeDir.exists()) {\n _diag('bundle.prepare.remove-old', data: {'runtimeDir': runtimeDir.path});\n await runtimeDir.delete(recursive: true);\n }\n await runtimeDir.create(recursive: true);\n\n for (final asset in assets) {\n final relative = asset.substring(prefix.length);\n if (relative.isEmpty || relative.endsWith('/')) continue;\n final bytes = await rootBundle.load(asset);\n final file = File(p.join(runtimeDir.path, relative));\n await file.parent.create(recursive: true);\n await file.writeAsBytes(\n bytes.buffer.asUint8List(bytes.offsetInBytes, bytes.lengthInBytes),\n flush: false,\n );\n }\n\n await _repairRuntimeLaunchMetadata(\n runtimeDir: runtimeDir,\n nodePath: nodePath,\n platformKey: platformKey,\n cached: false,\n );\n await marker.writeAsString(DateTime.now().toIso8601String());\n _diag(\n 'bundle.prepare.complete',\n data: {\n 'platformKey': platformKey,\n 'runtimeDir': runtimeDir.path,\n 'assetCount': assets.length,\n 'nodeExists': await nodePath.exists(),\n 'nodeSize': await nodePath.exists() ? await nodePath.length() : 0,\n 'cliExists': await cliPath.exists(),\n 'cliSize': await cliPath.exists() ? await cliPath.length() : 0,\n },\n );\n return runtimeDir;\n }\n\n Future<void> _copyRuntimeDirectory(\n Directory source,\n Directory destination,\n ) async {\n await destination.create(recursive: true);\n await for (final entity in source.list(\n recursive: true,\n followLinks: false,\n )) {\n final relative = p.relative(entity.path, from: source.path);\n if (relative == '.') continue;\n final target = p.join(destination.path, relative);\n if (entity is Directory) {\n await Directory(target).create(recursive: true);\n } else if (entity is File) {\n final targetFile = File(target);\n await targetFile.parent.create(recursive: true);\n await entity.openRead().pipe(targetFile.openWrite());\n }\n }\n }\n\n Future<void> _moveOrCopyRuntimeDirectory(\n Directory source,\n Directory destination,\n ) async {\n if (await destination.exists()) {\n await destination.delete(recursive: true);\n }\n await destination.parent.create(recursive: true);\n try {\n await source.rename(destination.path);\n return;\n } on FileSystemException {\n await _copyRuntimeDirectory(source, destination);\n }\n }\n\n Future<void> _repairRuntimeLaunchMetadata({\n required Directory runtimeDir,\n required File nodePath,\n required String platformKey,\n required bool cached,\n }) async {\n if (Platform.isWindows) return;\n\n final chmodResult = await Process.run('/bin/chmod', ['+x', nodePath.path]);\n ProcessResult? xattrResult;\n if (Platform.isMacOS) {\n // Files created by a quarantined app can inherit the quarantine xattr.\n // macOS then rejects Process.start with \"Operation not permitted\".\n xattrResult = await Process.run('/usr/bin/xattr', [\n '-dr',\n 'com.apple.quarantine',\n runtimeDir.path,\n ]);\n await Process.run('/usr/bin/xattr', [\n '-dr',\n 'com.apple.provenance',\n runtimeDir.path,\n ]);\n }\n\n _diag(\n 'bundle.prepare.launch-metadata',\n data: {\n 'platformKey': platformKey,\n 'runtimeDir': runtimeDir.path,\n 'nodePath': nodePath.path,\n 'cached': cached,\n 'chmodExitCode': chmodResult.exitCode,\n if ((chmodResult.stderr as Object).toString().isNotEmpty)\n 'chmodStderr': _previewProcessOutput(chmodResult.stderr),\n if (xattrResult != null) 'xattrExitCode': xattrResult.exitCode,\n if (xattrResult != null &&\n (xattrResult.stderr as Object).toString().isNotEmpty)\n 'xattrStderr': _previewProcessOutput(xattrResult.stderr),\n },\n );\n }\n\n Future<bool> _hasRequiredBundleAssets(\n String platformKey,\n _OpenClawBundleSpec spec,\n ) async {\n final prefix = 'assets/openclaw/$platformKey/';\n final assets = await _listAssets(prefix);\n final hasRequired =\n assets.contains('$prefix${spec.nodeExecutable}') &&\n assets.contains('$prefix${spec.cliEntrypoint}');\n _diag(\n 'bundle.has-required-assets',\n data: {\n 'platformKey': platformKey,\n 'assetCount': assets.length,\n 'hasRequired': hasRequired,\n },\n );\n return hasRequired;\n }\n\n Future<List<String>> _listAssets(String prefix) async {\n final assets = <String>{};\n var jsonCount = 0;\n var binCount = 0;\n var indexCount = 0;\n final failures = <String>[];\n\n try {\n final raw = await rootBundle.loadString('AssetManifest.json');\n final decoded = jsonDecode(raw) as Map<String, dynamic>;\n final items = decoded.keys\n .where((key) => key.startsWith(prefix))\n .toList();\n jsonCount = items.length;\n assets.addAll(items);\n } catch (error) {\n failures.add('AssetManifest.json: $error');\n // Newer Flutter release builds may only include AssetManifest.bin.\n }\n\n try {\n final manifest = await AssetManifest.loadFromAssetBundle(rootBundle);\n final items = manifest\n .listAssets()\n .where((key) => key.startsWith(prefix))\n .toList();\n binCount = items.length;\n assets.addAll(items);\n } catch (error) {\n failures.add('AssetManifest.bin: $error');\n // App Store archives patched after flutter build carry a JSON/index file.\n }\n\n try {\n final raw = await rootBundle.loadString(\n 'assets/openclaw/asset_index.json',\n );\n final decoded = jsonDecode(raw);\n final indexedAssets = decoded is Map\n ? decoded['assets']\n : decoded is List\n ? decoded\n : const [];\n if (indexedAssets is List) {\n final items = indexedAssets\n .map((item) => item.toString())\n .where((key) => key.startsWith(prefix))\n .toList();\n indexCount = items.length;\n assets.addAll(items);\n }\n } catch (error) {\n failures.add('asset_index.json: $error');\n // Older builds do not have an OpenClaw asset index.\n }\n\n final sortedAssets = assets.toList()..sort();\n _diag(\n 'asset.list',\n data: {\n 'prefix': prefix,\n 'total': sortedAssets.length,\n 'jsonCount': jsonCount,\n 'binCount': binCount,\n 'indexCount': indexCount,\n if (failures.isNotEmpty) 'failures': failures,\n 'sample': sortedAssets.take(8).toList(),\n },\n );\n return sortedAssets;\n }\n\n Future<bool> _probe(\n int port,\n String token, {\n String context = 'probe',\n }) async {\n try {\n final healthResponse = await http\n .get(\n Uri.parse('http://127.0.0.1:$port/health'),\n headers: {'Authorization': 'Bearer $token'},\n )\n .timeout(_probeTimeout);\n final healthPayload = _decodeJsonOrNull(healthResponse.body);\n if (healthResponse.statusCode < 200 ||\n healthResponse.statusCode >= 300 ||\n !_looksLikeHealthPayload(healthPayload)) {\n _diag(\n 'probe.health-unhealthy',\n data: {\n 'context': context,\n 'port': port,\n 'statusCode': healthResponse.statusCode,\n 'contentType': healthResponse.headers['content-type'],\n 'bodyPreview': _previewBody(healthResponse.body),\n },\n );\n return false;\n }\n\n final modelsResponse = await http\n .get(\n Uri.parse('http://127.0.0.1:$port/v1/models'),\n headers: {'Authorization': 'Bearer $token'},\n )\n .timeout(_probeTimeout);\n final modelsPayload = _decodeJsonOrNull(modelsResponse.body);\n if (modelsResponse.statusCode < 200 ||\n modelsResponse.statusCode >= 300 ||\n !_looksLikeModelsPayload(modelsPayload)) {\n _diag(\n 'probe.models-unhealthy',\n data: {\n 'context': context,\n 'port': port,\n 'statusCode': modelsResponse.statusCode,\n 'contentType': modelsResponse.headers['content-type'],\n 'bodyPreview': _previewBody(modelsResponse.body),\n },\n );\n return false;\n }\n\n // `/v1/models` can be healthy while `/v1/chat/completions` is disabled\n // by an old config. Use GET so the probe only checks route wiring and\n // never starts a real model request.\n final chatResponse = await http\n .get(\n Uri.parse('http://127.0.0.1:$port/v1/chat/completions'),\n headers: {'Authorization': 'Bearer $token'},\n )\n .timeout(_probeTimeout);\n final healthy =\n chatResponse.statusCode < 500 &&\n chatResponse.statusCode != 401 &&\n chatResponse.statusCode != 403 &&\n chatResponse.statusCode != 404;\n _diag(\n healthy ? 'probe.healthy' : 'probe.chat-unhealthy',\n data: {\n 'context': context,\n 'port': port,\n 'chatStatusCode': chatResponse.statusCode,\n },\n );\n return healthy;\n } catch (error) {\n if (context != 'startup') {\n _diag(\n 'probe.error',\n data: {'context': context, 'port': port},\n error: error,\n );\n }\n return false;\n }\n }\n\n Future<File> _ensureConfigFile({\n required Directory runtimeDir,\n required Directory stateRoot,\n required int port,\n required String token,\n required String backendDeepSeekModel,\n required String deepSeekProxyBaseUrl,\n required String remoteGatewayUrl,\n List<String> extraPluginLoadPaths = const [],\n }) async {\n final configDir = Directory(p.join(stateRoot.path, 'config'));\n await configDir.create(recursive: true);\n final workspace = Directory(p.join(stateRoot.path, 'workspace'));\n await workspace.create(recursive: true);\n final configPath = File(p.join(configDir.path, 'openclaw.json'));\n final bundledPluginLoadPaths = await _bundledPluginLoadPaths(runtimeDir);\n final pluginLoadPaths = _mergeStringLists(\n extraPluginLoadPaths,\n bundledPluginLoadPaths,\n );\n final hasWeChatPlugin = pluginLoadPaths.any(\n (path) => p.basename(path) == _weChatPluginId,\n );\n final remoteUrl = remoteGatewayUrl.trim();\n\n final config = _buildEmbeddedConfig(\n stateRoot: stateRoot,\n port: port,\n token: token,\n backendDeepSeekModel: backendDeepSeekModel,\n deepSeekProxyBaseUrl: deepSeekProxyBaseUrl,\n remoteGatewayUrl: remoteUrl,\n pluginLoadPaths: pluginLoadPaths,\n hasWeChatPlugin: hasWeChatPlugin,\n );\n\n // Repair older embedded configs in-place. OpenClaw 2026.6 requires\n // gateway.mode=local, and the home screen requires the OpenAI-compatible\n // chat endpoint. Preserve unrelated user edits while fixing the fields\n // needed for the app-owned embedded gateway to start reliably.\n try {\n final mcpConfigPath = File(\n p.join(\n Platform.environment['HOME'] ?? '',\n '.gemini',\n 'config',\n 'mcp_config.json',\n ),\n );\n if (await mcpConfigPath.exists()) {\n final mcpConfig = jsonDecode(await mcpConfigPath.readAsString());\n if (mcpConfig is Map && mcpConfig.containsKey('mcpServers')) {\n config['mcpServers'] = mcpConfig['mcpServers'];\n }\n }\n } catch (e) {\n _diag('mcp.config-error', data: {'error': e.toString()});\n }\n\n final merged = await _mergeEmbeddedConfig(configPath, config);\n await configPath.writeAsString(\n const JsonEncoder.withIndent(' ').convert(merged),\n );\n _diag(\n 'config.written',\n data: {\n 'configPath': configPath.path,\n 'port': port,\n 'gatewayMode': _mutableMap(merged['gateway'])['mode'],\n 'backendDeepSeekModel': backendDeepSeekModel,\n 'deepSeekProxyBaseUrl': deepSeekProxyBaseUrl,\n 'remoteGatewayUrlSet': remoteUrl.isNotEmpty,\n 'pluginLoadPaths': pluginLoadPaths,\n 'hasWeChatPlugin': hasWeChatPlugin,\n },\n );\n return configPath;\n }\n\n @visibleForTesting\n Map<String, dynamic> buildEmbeddedConfigForTest({\n required Directory stateRoot,\n required int port,\n required String token,\n required String backendDeepSeekModel,\n required String deepSeekProxyBaseUrl,\n String remoteGatewayUrl = '',\n List<String> pluginLoadPaths = const [],\n bool hasWeChatPlugin = false,\n }) {\n return _buildEmbeddedConfig(\n stateRoot: stateRoot,\n port: port,\n token: token,\n backendDeepSeekModel: backendDeepSeekModel,\n deepSeekProxyBaseUrl: deepSeekProxyBaseUrl,\n remoteGatewayUrl: remoteGatewayUrl,\n pluginLoadPaths: pluginLoadPaths,\n hasWeChatPlugin: hasWeChatPlugin,\n );\n }\n\n Map<String, dynamic> _buildEmbeddedConfig({\n required Directory stateRoot,\n required int port,\n required String token,\n required String backendDeepSeekModel,\n required String deepSeekProxyBaseUrl,\n required String remoteGatewayUrl,\n required List<String> pluginLoadPaths,\n required bool hasWeChatPlugin,\n }) {\n final workspace = p.join(stateRoot.path, 'workspace');\n final remoteUrl = remoteGatewayUrl.trim();\n final desktopToolAllowList = DesktopControlPolicy.supportedTools.toList()\n ..sort();\n final canvasHostConfig = <String, dynamic>{\n 'enabled': true,\n 'root': p.join(stateRoot.path, 'canvas'),\n };\n\n return <String, dynamic>{\n 'gateway': {\n 'mode': 'local',\n 'port': port,\n 'bind': 'loopback',\n 'auth': {'mode': 'token', 'token': token},\n if (remoteUrl.isNotEmpty) 'remote': {'enabled': true, 'url': remoteUrl},\n 'http': {\n 'endpoints': {\n 'chatCompletions': {'enabled': true},\n 'responses': {'enabled': true},\n },\n },\n },\n 'models': {\n 'mode': 'merge',\n 'providers': {\n _backendDeepSeekProviderId: {\n 'baseUrl': deepSeekProxyBaseUrl,\n 'api': 'openai-completions',\n 'apiKey': 'DACHENG_OPENCLAW_PROXY_TOKEN',\n 'authHeader': true,\n 'headers': {'x-dacheng-auth-token': 'DACHENG_AUTH_TOKEN'},\n 'models': [\n {\n 'id': 'deepseek-chat',\n 'name': 'DeepSeek Chat',\n 'contextWindow': 131072,\n 'maxTokens': 8192,\n 'input': ['text'],\n 'compat': {\n 'requiresStringContent': true,\n 'strictMessageKeys': true,\n },\n },\n {\n 'id': 'deepseek-reasoner',\n 'name': 'DeepSeek Reasoner',\n 'contextWindow': 131072,\n 'maxTokens': 8192,\n 'reasoning': true,\n 'input': ['text'],\n 'compat': {\n 'requiresStringContent': true,\n 'strictMessageKeys': true,\n },\n },\n ],\n },\n },\n },\n 'browser': {\n 'enabled': true,\n 'defaultProfile': 'openclaw',\n 'headless': false,\n 'ssrfPolicy': {'dangerouslyAllowPrivateNetwork': true},\n },\n 'tools': {\n 'profile': 'full',\n 'alsoAllow': desktopToolAllowList,\n 'exec': {'host': 'gateway', 'security': 'full', 'ask': 'off'},\n },\n 'plugins': {\n 'enabled': true,\n 'deny': <String>[],\n 'load': {'paths': pluginLoadPaths},\n 'slots': {'memory': 'memory-core'},\n 'entries': <String, dynamic>{\n 'memory-core': {'enabled': true},\n 'bonjour': {'enabled': false},\n 'browser': {'enabled': true},\n 'canvas': {\n 'enabled': true,\n 'config': {'host': canvasHostConfig},\n },\n 'device-pair': {'enabled': true},\n 'file-transfer': {'enabled': true},\n 'phone-control': {'enabled': true},\n 'talk-voice': {'enabled': true},\n _desktopToolsPluginId: {'enabled': true},\n if (hasWeChatPlugin) _weChatPluginId: {'enabled': true},\n },\n },\n 'channels': {\n 'defaults': {'groupPolicy': 'allowlist'},\n if (hasWeChatPlugin)\n _weChatPluginId: {\n 'enabled': true,\n 'dmPolicy': 'pairing',\n 'allowFrom': <String>[],\n 'accounts': <String, dynamic>{},\n },\n },\n 'agents': {\n 'defaults': {\n 'workspace': workspace,\n 'model': {'primary': backendDeepSeekModel},\n },\n 'list': [\n {\n 'id': 'dacheng',\n 'default': true,\n 'workspace': workspace,\n 'model': {'primary': backendDeepSeekModel},\n },\n ],\n },\n };\n }\n\n Future<File> _ensureDesktopToolsManifest({\n required Directory stateRoot,\n required _DesktopToolsLaunch desktopTools,\n }) async {\n final configDir = Directory(p.join(stateRoot.path, 'config'));\n await configDir.create(recursive: true);\n final manifestPath = File(p.join(configDir.path, 'desktop_tools.json'));\n final tools =\n DesktopControlPolicy.supportedTools\n .map(\n (name) => {\n 'name': name,\n 'endpoint': '/v1/tools/execute',\n 'readOnly': DesktopControlPolicy.isReadOnly(name),\n 'requiresConfirmation':\n DesktopControlPolicy.requiresConfirmation(name),\n },\n )\n .toList()\n ..sort(\n (a, b) => a['name'].toString().compareTo(b['name'].toString()),\n );\n\n await manifestPath.writeAsString(\n const JsonEncoder.withIndent(' ').convert({\n 'version': 1,\n 'transport': 'http-loopback',\n 'baseUrl': desktopTools.uri?.toString(),\n 'auth': {'type': 'bearer', 'env': 'DACHENG_DESKTOP_TOOLS_TOKEN'},\n 'status': desktopTools.statusJson,\n 'tools': tools,\n }),\n );\n _diag(\n 'desktop-tools.manifest-written',\n data: {\n 'manifestPath': manifestPath.path,\n 'hasBaseUrl': desktopTools.uri != null,\n },\n );\n return manifestPath;\n }\n\n Future<File> _ensureAgentModelConfig({\n required Directory stateRoot,\n required String deepSeekProxyBaseUrl,\n }) async {\n final agentDir = Directory(\n p.join(stateRoot.path, 'state', 'agents', 'dacheng', 'agent'),\n );\n await agentDir.create(recursive: true);\n final modelsPath = File(p.join(agentDir.path, 'models.json'));\n final providerConfig = <String, dynamic>{\n 'baseUrl': deepSeekProxyBaseUrl,\n 'api': 'openai-completions',\n 'apiKey': 'DACHENG_OPENCLAW_PROXY_TOKEN',\n 'authHeader': true,\n 'headers': {'x-dacheng-auth-token': 'DACHENG_AUTH_TOKEN'},\n 'models': [\n {\n 'id': 'deepseek-chat',\n 'name': 'DeepSeek Chat',\n 'contextWindow': 131072,\n 'maxTokens': 8192,\n 'input': ['text'],\n 'compat': {'requiresStringContent': true, 'strictMessageKeys': true},\n },\n {\n 'id': 'deepseek-reasoner',\n 'name': 'DeepSeek Reasoner',\n 'contextWindow': 131072,\n 'maxTokens': 8192,\n 'reasoning': true,\n 'input': ['text'],\n 'compat': {'requiresStringContent': true, 'strictMessageKeys': true},\n },\n ],\n };\n await modelsPath.writeAsString(\n const JsonEncoder.withIndent(' ').convert({\n 'providers': {_backendDeepSeekProviderId: providerConfig},\n }),\n );\n _diag(\n 'agent-models.written',\n data: {\n 'modelsPath': modelsPath.path,\n 'provider': _backendDeepSeekProviderId,\n 'deepSeekProxyBaseUrl': deepSeekProxyBaseUrl,\n },\n );\n return modelsPath;\n }\n\n Future<_OpenClawCliLaunch> _prepareCliLaunch({\n required bool ensureGateway,\n }) async {\n if (ensureGateway) {\n await ensureStarted();\n }\n final platformKey = _platformKey;\n if (platformKey == null) {\n throw const OpenClawRuntimeException('当前平台不支持内置 OpenClaw Gateway');\n }\n final spec = await _loadSpec(platformKey, checkUpdates: false);\n final runtimeDir = await _prepareBundle(spec, platformKey);\n final stateRoot = await _stateRoot();\n final desktopToolsPluginDir = await _ensureDesktopToolsPlugin(\n stateRoot: stateRoot,\n );\n final port = await AppSettings.getOpenClawGatewayPort(\n defaultValue: spec.defaultPort,\n );\n final token = await AppSettings.getOpenClawGatewayToken();\n final backendDeepSeekModel = _backendDeepSeekModelRef(\n await AppSettings.getOpenClawDeepSeekModel(\n defaultValue: _defaultDeepSeekModelFor(spec.defaultModel),\n ),\n );\n final configPath = await _ensureConfigFile(\n runtimeDir: runtimeDir,\n stateRoot: stateRoot,\n port: port,\n token: token,\n backendDeepSeekModel: backendDeepSeekModel,\n deepSeekProxyBaseUrl: _deepSeekProxyBaseUrl(),\n remoteGatewayUrl: await AppSettings.getOpenClawRemoteGatewayUrl(),\n extraPluginLoadPaths: [desktopToolsPluginDir.path],\n );\n final nodePath = p.join(runtimeDir.path, spec.nodeExecutable);\n final cliPath = p.join(runtimeDir.path, spec.cliEntrypoint);\n if (!await File(nodePath).exists()) {\n throw OpenClawRuntimeException('OpenClaw 内置 Node 不存在: $nodePath');\n }\n if (!await File(cliPath).exists()) {\n throw OpenClawRuntimeException('OpenClaw CLI 入口不存在: $cliPath');\n }\n return _OpenClawCliLaunch(\n spec: spec,\n runtimeDir: runtimeDir,\n stateRoot: stateRoot,\n configPath: configPath,\n nodePath: nodePath,\n cliPath: cliPath,\n port: port,\n token: token,\n );\n }\n\n Map<String, String> _buildOpenClawEnvironment({\n required Directory runtimeDir,\n required Directory stateRoot,\n required File configPath,\n required int port,\n required String token,\n }) {\n final nodeBinDir = Platform.isWindows\n ? p.join(runtimeDir.path, 'node')\n : p.join(runtimeDir.path, 'node', 'bin');\n final pathKey = Platform.isWindows ? 'Path' : 'PATH';\n final currentPath =\n Platform.environment[pathKey] ?? Platform.environment['PATH'] ?? '';\n final separator = Platform.isWindows ? ';' : ':';\n final pathValue = currentPath.isEmpty\n ? nodeBinDir\n : '$nodeBinDir$separator$currentPath';\n final env = Map<String, String>.from(Platform.environment)\n ..remove('NODE_COMPILE_CACHE')\n ..addAll({\n 'OPENCLAW_GATEWAY_PORT': '$port',\n 'OPENCLAW_GATEWAY_BIND': 'loopback',\n 'OPENCLAW_GATEWAY_TOKEN': token,\n 'OPENCLAW_CONFIG_PATH': configPath.path,\n 'OPENCLAW_STATE_DIR': p.join(stateRoot.path, 'state'),\n 'OPENCLAW_AGENT_DIR': p.join(stateRoot.path, 'agents'),\n 'OPENCLAW_WORKSPACE': p.join(stateRoot.path, 'workspace'),\n 'OPENCLAW_DISABLE_BONJOUR': '1',\n 'NODE_DISABLE_COMPILE_CACHE': '1',\n pathKey: pathValue,\n if (pathKey != 'PATH') 'PATH': pathValue,\n });\n return env;\n }\n\n @visibleForTesting\n Map<String, String> buildOpenClawEnvironmentForTest({\n required Directory runtimeDir,\n required Directory stateRoot,\n required File configPath,\n required int port,\n required String token,\n }) {\n return _buildOpenClawEnvironment(\n runtimeDir: runtimeDir,\n stateRoot: stateRoot,\n configPath: configPath,\n port: port,\n token: token,\n );\n }\n\n Future<List<String>> _bundledPluginLoadPaths(Directory runtimeDir) async {\n final paths = <String>[];\n final weChatPluginDir = Directory(\n p.join(runtimeDir.path, 'plugins', _weChatPluginId),\n );\n if (await File(\n p.join(weChatPluginDir.path, 'openclaw.plugin.json'),\n ).exists()) {\n paths.add(weChatPluginDir.path);\n }\n return paths;\n }\n\n Future<Directory> _ensureDesktopToolsPlugin({\n required Directory stateRoot,\n }) async {\n final dir = Directory(\n p.join(stateRoot.path, 'plugins', _desktopToolsPluginId),\n );\n await dir.create(recursive: true);\n final tools = DesktopControlPolicy.supportedTools.toList()..sort();\n await File(p.join(dir.path, 'package.json')).writeAsString(\n const JsonEncoder.withIndent(' ').convert({\n 'name': '@dacheng/openclaw-desktop-tools',\n 'version': '1.0.0',\n 'type': 'module',\n 'openclaw': {\n 'extensions': ['./index.mjs'],\n },\n }),\n );\n await File(p.join(dir.path, 'openclaw.plugin.json')).writeAsString(\n const JsonEncoder.withIndent(' ').convert({\n 'id': _desktopToolsPluginId,\n 'name': 'Dacheng Desktop Tools',\n 'description':\n 'Expose Fabushi desktop and Chrome control bridge tools to embedded OpenClaw.',\n 'activation': {'onStartup': true},\n 'contracts': {'tools': tools},\n 'configSchema': {\n 'type': 'object',\n 'additionalProperties': false,\n 'properties': <String, dynamic>{},\n },\n }),\n );\n await File(\n p.join(dir.path, 'index.mjs'),\n ).writeAsString(_desktopToolsPluginSource(tools));\n _diag(\n 'desktop-tools.plugin-written',\n data: {'pluginDir': dir.path, 'tools': tools},\n );\n return dir;\n }\n\n String _desktopToolsPluginSource(List<String> tools) {\n final toolsJson = jsonEncode(tools);\n return '''\nconst tools = $toolsJson;\n\nconst descriptions = {\n \"desktop.observe\": \"Observe the active desktop application and visible windows.\",\n \"desktop.screenshot\": \"Capture a screenshot of the local desktop.\",\n \"desktop.windows\": \"List visible desktop windows.\",\n \"desktop.click\": \"Click a local desktop coordinate after user confirmation.\",\n \"desktop.type\": \"Type text into the focused local desktop app after user confirmation.\",\n \"desktop.hotkey\": \"Send a keyboard shortcut to the local desktop after user confirmation.\",\n \"desktop.scroll\": \"Scroll the local desktop after user confirmation.\",\n \"chrome.tabs\": \"List tabs from the paired Chrome connector.\",\n \"chrome.navigate\": \"Navigate the active or selected Chrome tab after user confirmation.\",\n \"chrome.dom_snapshot\": \"Read a structured DOM snapshot from the paired Chrome connector.\",\n \"chrome.screenshot\": \"Capture a screenshot from the paired Chrome connector.\",\n \"chrome.click\": \"Click an element or point in the paired Chrome tab after user confirmation.\",\n \"chrome.type\": \"Type into an element in the paired Chrome tab after user confirmation.\"\n};\n\nconst parameterSchemas = {\n \"desktop.click\": {\n type: \"object\",\n additionalProperties: true,\n properties: {\n x: { type: \"number\" },\n y: { type: \"number\" },\n button: { type: \"string\", enum: [\"left\", \"right\"] }\n },\n required: [\"x\", \"y\"]\n },\n \"desktop.type\": {\n type: \"object\",\n additionalProperties: true,\n properties: { text: { type: \"string\" } },\n required: [\"text\"]\n },\n \"desktop.hotkey\": {\n type: \"object\",\n additionalProperties: true,\n properties: {\n key: { type: \"string\" },\n keys: { type: \"array\", items: { type: \"string\" } }\n }\n },\n \"chrome.navigate\": {\n type: \"object\",\n additionalProperties: true,\n properties: { url: { type: \"string\" }, tabId: { type: \"integer\" } },\n required: [\"url\"]\n },\n \"chrome.click\": {\n type: \"object\",\n additionalProperties: true,\n properties: {\n selector: { type: \"string\" },\n x: { type: \"number\" },\n y: { type: \"number\" },\n tabId: { type: \"integer\" }\n }\n },\n \"chrome.type\": {\n type: \"object\",\n additionalProperties: true,\n properties: {\n selector: { type: \"string\" },\n x: { type: \"number\" },\n y: { type: \"number\" },\n text: { type: \"string\" },\n tabId: { type: \"integer\" }\n },\n required: [\"text\"]\n }\n};\n\nconst defaultSchema = {\n type: \"object\",\n additionalProperties: true,\n properties: {}\n};\n\nfunction toolResultText(value) {\n try {\n return JSON.stringify(value, null, 2);\n } catch {\n return String(value);\n }\n}\n\nasync function callBridge(toolName, args) {\n if (process.env.DACHENG_DESKTOP_TOOLS_ENABLED !== \"1\") {\n return {\n isError: true,\n content: [{ type: \"text\", text: \"Fabushi desktop tools bridge is not running.\" }]\n };\n }\n const rawBaseUrl = process.env.DACHENG_DESKTOP_TOOLS_URL || \"\";\n const baseUrl = rawBaseUrl.endsWith(\"/\") ? rawBaseUrl.slice(0, -1) : rawBaseUrl;\n const token = process.env.DACHENG_DESKTOP_TOOLS_TOKEN || \"\";\n if (!baseUrl || !token) {\n return {\n isError: true,\n content: [{ type: \"text\", text: \"Fabushi desktop tools bridge URL/token is missing.\" }]\n };\n }\n const response = await fetch(`\\${baseUrl}/v1/tools/execute`, {\n method: \"POST\",\n headers: {\n \"Authorization\": `Bearer \\${token}`,\n \"Content-Type\": \"application/json\"\n },\n body: JSON.stringify({ tool: toolName, arguments: args || {} })\n });\n const payload = await response.json().catch(() => ({}));\n const ok = response.ok && payload && payload.ok !== false;\n const data = payload && Object.prototype.hasOwnProperty.call(payload, \"data\")\n ? payload.data\n : payload;\n return {\n isError: !ok,\n content: [{ type: \"text\", text: toolResultText(data) }],\n details: data\n };\n}\n\nexport default {\n id: \"dacheng-desktop-tools\",\n name: \"Dacheng Desktop Tools\",\n description: \"Expose Fabushi desktop and Chrome control tools to embedded OpenClaw.\",\n configSchema: { type: \"object\", additionalProperties: false, properties: {} },\n register(api) {\n for (const name of tools) {\n api.registerTool({\n name,\n label: name,\n description: descriptions[name] || `Run \\${name} through the Fabushi desktop bridge.`,\n parameters: parameterSchemas[name] || defaultSchema,\n async execute(_toolCallId, args) {\n return await callBridge(name, args);\n }\n });\n }\n }\n};\n''';\n }\n\n Future<Map<String, dynamic>> _mergeEmbeddedConfig(\n File configPath,\n Map<String, dynamic> defaults,\n ) async {\n Map<String, dynamic> current = <String, dynamic>{};\n if (await configPath.exists()) {\n try {\n final decoded = jsonDecode(await configPath.readAsString());\n if (decoded is Map<String, dynamic>) {\n current = decoded;\n } else if (decoded is Map) {\n current = Map<String, dynamic>.from(decoded);\n }\n } catch (_) {\n current = <String, dynamic>{};\n }\n }\n\n final gateway = _mutableMap(current['gateway']);\n final defaultGateway = _mutableMap(defaults['gateway']);\n gateway['mode'] = 'local';\n gateway['port'] = defaultGateway['port'];\n gateway['bind'] = defaultGateway['bind'];\n gateway['auth'] = Map<String, dynamic>.from(defaultGateway['auth'] as Map);\n if (defaultGateway.containsKey('remote')) {\n gateway['remote'] = {\n ..._mutableMap(gateway['remote']),\n ..._mutableMap(defaultGateway['remote']),\n };\n } else {\n final remote = _mutableMap(gateway['remote'])..remove('url');\n if (remote.isEmpty) {\n gateway.remove('remote');\n } else {\n gateway['remote'] = remote;\n }\n }\n\n final gatewayHttp = _mutableMap(gateway['http']);\n final endpoints = _mutableMap(gatewayHttp['endpoints']);\n endpoints['chatCompletions'] = {'enabled': true};\n endpoints['responses'] = {'enabled': true};\n gatewayHttp['endpoints'] = endpoints;\n gateway['http'] = gatewayHttp;\n current['gateway'] = gateway;\n\n final env = _mutableMap(current['env']);\n env.remove('DEEPSEEK_API_KEY');\n if (env.isEmpty) {\n current.remove('env');\n } else {\n current['env'] = env;\n }\n\n final defaultModels = _mutableMap(defaults['models']);\n final models = _mutableMap(current['models']);\n models['mode'] ??= defaultModels['mode'];\n final providers = _mutableMap(models['providers']);\n final defaultProviders = _mutableMap(defaultModels['providers']);\n providers.remove('deepseek');\n providers.remove('dacheng-deepseek');\n providers.remove(_backendDeepSeekProviderId);\n for (final entry in defaultProviders.entries) {\n providers[entry.key] = entry.value is Map\n ? Map<String, dynamic>.from(entry.value as Map)\n : entry.value;\n }\n models['providers'] = providers;\n current['models'] = models;\n\n final defaultBrowser = _mutableMap(defaults['browser']);\n final browser = _mutableMap(current['browser']);\n browser.addAll(defaultBrowser);\n current['browser'] = browser;\n\n // OpenClaw 2026.6 rejects root-level canvas/canvasHost. Canvas is now\n // configured through plugins.entries.canvas.config.host.\n current.remove('canvas');\n current.remove('canvasHost');\n\n final defaultTools = _mutableMap(defaults['tools']);\n final tools = _mutableMap(current['tools']);\n tools.remove('toolSearch');\n tools.remove('fs');\n final alsoAllow = _mergeStringLists(\n tools['alsoAllow'],\n defaultTools['alsoAllow'],\n );\n final execTools = _mutableMap(tools['exec']);\n execTools.remove('mode');\n if (execTools.isNotEmpty) {\n tools['exec'] = execTools;\n }\n tools.addAll(defaultTools);\n if (alsoAllow.isEmpty) {\n tools.remove('alsoAllow');\n } else {\n tools['alsoAllow'] = alsoAllow;\n }\n current['tools'] = tools;\n\n final defaultPlugins = _mutableMap(defaults['plugins']);\n final plugins = _mutableMap(current['plugins']);\n plugins['enabled'] = true;\n final defaultEntries = _mutableMap(defaultPlugins['entries']);\n final requiredPluginIds = defaultEntries.keys.toSet();\n final allowList = _stringList(plugins['allow']);\n if (allowList.isEmpty) {\n plugins.remove('allow');\n } else {\n plugins['allow'] = _mergeStringLists(allowList, requiredPluginIds);\n }\n final denyList = _stringList(\n plugins['deny'],\n ).where((item) => !requiredPluginIds.contains(item)).toList();\n if (denyList.isEmpty) {\n plugins.remove('deny');\n } else {\n plugins['deny'] = denyList;\n }\n final defaultLoad = _mutableMap(defaultPlugins['load']);\n final load = _mutableMap(plugins['load']);\n load['paths'] = _mergeStringLists(load['paths'], defaultLoad['paths']);\n plugins['load'] = load;\n final slots = _mutableMap(plugins['slots']);\n slots.addAll(_mutableMap(defaultPlugins['slots']));\n plugins['slots'] = slots;\n final entries = _mutableMap(plugins['entries']);\n for (final entry in defaultEntries.entries) {\n final currentEntry = _mutableMap(entries[entry.key]);\n currentEntry.addAll(\n entry.value is Map\n ? Map<String, dynamic>.from(entry.value as Map)\n : {'enabled': true},\n );\n entries[entry.key] = currentEntry;\n }\n plugins['entries'] = entries;\n current['plugins'] = plugins;\n\n final defaultChannels = _mutableMap(defaults['channels']);\n final channels = _mutableMap(current['channels']);\n final defaultChannelDefaults = _mutableMap(defaultChannels['defaults']);\n if (defaultChannelDefaults.isNotEmpty) {\n channels['defaults'] = {\n ..._mutableMap(channels['defaults']),\n ...defaultChannelDefaults,\n };\n }\n for (final entry in defaultChannels.entries) {\n if (entry.key == 'defaults') continue;\n final currentChannel = _mutableMap(channels[entry.key]);\n currentChannel.addAll(\n entry.value is Map\n ? Map<String, dynamic>.from(entry.value as Map)\n : <String, dynamic>{},\n );\n channels[entry.key] = currentChannel;\n }\n current['channels'] = channels;\n\n final agents = _mutableMap(current['agents']);\n final defaultAgents = _mutableMap(defaults['agents']);\n final defaultAgentDefaults = _mutableMap(defaultAgents['defaults']);\n final agentDefaults = _mutableMap(agents['defaults']);\n agentDefaults['workspace'] = defaultAgentDefaults['workspace'];\n agentDefaults['model'] = defaultAgentDefaults['model'];\n agents['defaults'] = agentDefaults;\n\n final defaultList = (defaultAgents['list'] as List? ?? const [])\n .whereType<Map>()\n .map((item) => Map<String, dynamic>.from(item))\n .toList();\n final defaultDacheng = defaultList.isNotEmpty\n ? defaultList.first\n : <String, dynamic>{\n 'id': 'dacheng',\n 'default': true,\n 'workspace': agentDefaults['workspace'],\n 'model': agentDefaults['model'],\n };\n final list = (agents['list'] as List? ?? const [])\n .map((item) => item is Map ? Map<String, dynamic>.from(item) : item)\n .toList();\n var foundDacheng = false;\n for (var i = 0; i < list.length; i++) {\n final item = list[i];\n if (item is! Map<String, dynamic>) continue;\n if (item['id'] != 'dacheng') continue;\n foundDacheng = true;\n item['default'] = true;\n item['workspace'] = defaultDacheng['workspace'];\n item['model'] = defaultDacheng['model'];\n list[i] = item;\n }\n if (!foundDacheng) {\n list.add(defaultDacheng);\n }\n agents['list'] = list.isEmpty ? defaultList : list;\n current['agents'] = agents;\n\n return current;\n }\n\n @visibleForTesting\n Future<Map<String, dynamic>> mergeEmbeddedConfigForTest(\n File configPath,\n Map<String, dynamic> defaults,\n ) {\n return _mergeEmbeddedConfig(configPath, defaults);\n }\n\n String _gatewayChatModel(String model) {\n final trimmed = model.trim();\n if (trimmed == 'openclaw' || trimmed.startsWith('openclaw/')) {\n return trimmed;\n }\n return _defaultGatewayModel;\n }\n\n String _defaultDeepSeekModelFor(String model) {\n final trimmed = model.trim();\n if (trimmed.startsWith('deepseek/')) return trimmed;\n return _defaultDeepSeekModel;\n }\n\n String _backendDeepSeekModelRef(String model) {\n final trimmed = model.trim().isEmpty ? _defaultDeepSeekModel : model.trim();\n final modelId = trimmed.contains('/')\n ? trimmed.split('/').last.trim()\n : trimmed;\n final normalizedModelId = modelId.isEmpty ? 'deepseek-chat' : modelId;\n return '$_backendDeepSeekProviderId/$normalizedModelId';\n }\n\n String _deepSeekProxyBaseUrl() {\n final baseUrl = AppConfig.currentAiBackendUrl.replaceFirst(\n RegExp(r'/+$'),\n '',\n );\n return '$baseUrl/api/openclaw/deepseek/v1';\n }\n\n Map<String, dynamic> _mutableMap(Object? value) {\n if (value is Map<String, dynamic>) return Map<String, dynamic>.from(value);\n if (value is Map) return Map<String, dynamic>.from(value);\n return <String, dynamic>{};\n }\n\n List<String> _stringList(Object? value) {\n if (value is! Iterable) return <String>[];\n return value\n .map((item) => item.toString().trim())\n .where((item) => item.isNotEmpty)\n .toList();\n }\n\n List<String> _mergeStringLists(Object? first, Object? second) {\n final merged = <String>{};\n merged.addAll(_stringList(first));\n merged.addAll(_stringList(second));\n return merged.toList();\n }\n\n void _appendBounded(StringBuffer buffer, String chunk) {\n const maxChars = 120000;\n if (chunk.isEmpty || buffer.length >= maxChars) return;\n final remaining = maxChars - buffer.length;\n buffer.write(\n chunk.length <= remaining ? chunk : chunk.substring(0, remaining),\n );\n }\n\n void _captureLogs(Process process) {\n void addLog(String line) {\n final text = line.trim();\n if (text.isEmpty) return;\n _recentLogs.insert(0, text);\n if (_recentLogs.length > 80) {\n _recentLogs.removeRange(80, _recentLogs.length);\n }\n if (kDebugMode) {\n debugPrint('[OpenClaw] $text');\n }\n _diag('process.output', data: {'line': text});\n }\n\n process.stdout\n .transform(utf8.decoder)\n .transform(const LineSplitter())\n .listen(addLog, onError: (_) {});\n process.stderr\n .transform(utf8.decoder)\n .transform(const LineSplitter())\n .listen(addLog, onError: (_) {});\n }\n\n bool _recentLogsContainRuntimeModuleFailure() {\n return _recentLogs.any((line) {\n final lower = line.toLowerCase();\n return lower.contains('err_vm_module_link_failure') ||\n lower.contains('is not in cache') ||\n lower.contains('request for') && lower.contains('.js');\n });\n }\n\n Future<Directory> _stateRoot() async {\n final support = await getApplicationSupportDirectory();\n final dir = Directory(p.join(support.path, 'openclaw_embedded'));\n await dir.create(recursive: true);\n return dir;\n }\n\n Future<_DesktopToolsLaunch> _ensureDesktopTools() async {\n try {\n final status = await DesktopControlBridge.instance.ensureStarted();\n _diag(\n 'desktop-tools.started',\n data: {\n 'bridgeRunning': status.bridgeRunning,\n 'bridgeUri': status.bridgeUri?.toString(),\n 'message': status.message,\n },\n );\n return _DesktopToolsLaunch(\n uri: status.bridgeUri,\n token: await DesktopControlBridge.instance.bridgeToken,\n statusJson: status.toJson(),\n );\n } catch (error) {\n _diag('desktop-tools.error', error: error);\n return _DesktopToolsLaunch(\n uri: null,\n token: null,\n statusJson: {\n 'enabledByBuild': false,\n 'bridgeRunning': false,\n 'message': error.toString(),\n },\n );\n }\n }\n\n Future<Directory> _runtimeDir(\n _OpenClawBundleSpec spec,\n String platformKey,\n ) async {\n final support = await getApplicationSupportDirectory();\n final runtimeBase = Platform.isMacOS\n ? Directory(\n p.join(\n support.parent.path,\n '${p.basename(support.path)}.openclaw_runtime',\n ),\n )\n : Directory(p.join(support.path, 'openclaw_embedded'));\n return Directory(\n p.join(runtimeBase.path, 'runtime', spec.version, platformKey),\n );\n }\n\n Future<Directory?> _bundledRuntimeDir(\n _OpenClawBundleSpec spec,\n String platformKey,\n ) async {\n if (!Platform.isMacOS) return null;\n\n final contentsDir = Directory(\n p.dirname(p.dirname(Platform.resolvedExecutable)),\n );\n final candidates = <Directory>[\n Directory(\n p.join(\n contentsDir.path,\n 'Frameworks',\n 'App.framework',\n 'Resources',\n 'flutter_assets',\n 'assets',\n 'openclaw',\n platformKey,\n ),\n ),\n Directory(\n p.join(\n contentsDir.path,\n 'Frameworks',\n 'App.framework',\n 'Versions',\n 'A',\n 'Resources',\n 'flutter_assets',\n 'assets',\n 'openclaw',\n platformKey,\n ),\n ),\n Directory(\n p.join(\n contentsDir.path,\n 'Resources',\n 'flutter_assets',\n 'assets',\n 'openclaw',\n platformKey,\n ),\n ),\n ];\n\n for (final dir in candidates) {\n final nodePath = File(p.join(dir.path, spec.nodeExecutable));\n final cliPath = File(p.join(dir.path, spec.cliEntrypoint));\n if (await nodePath.exists() && await cliPath.exists()) {\n return dir;\n }\n }\n _diag(\n 'bundle.macos-bundled-runtime-missing',\n data: {\n 'platformKey': platformKey,\n 'contentsDir': contentsDir.path,\n 'candidates': candidates.map((item) => item.path).toList(),\n },\n );\n return null;\n }\n\n Future<Directory?> _runtimeDirOverride(\n _OpenClawBundleSpec spec,\n String platformKey,\n ) async {\n final raw = _configuredRuntimeDirOverride(platformKey);\n if (raw == null) return null;\n\n final dir = Directory(raw);\n final nodePath = File(p.join(dir.path, spec.nodeExecutable));\n final cliPath = File(p.join(dir.path, spec.cliEntrypoint));\n final nodeExists = await nodePath.exists();\n final cliExists = await cliPath.exists();\n _diag(\n 'bundle.override.paths',\n data: {\n 'platformKey': platformKey,\n 'runtimeDir': dir.path,\n 'nodePath': nodePath.path,\n 'nodeExists': nodeExists,\n 'cliPath': cliPath.path,\n 'cliExists': cliExists,\n },\n );\n\n if (!nodeExists || !cliExists) {\n throw OpenClawRuntimeException(\n 'DACHENG_OPENCLAW_RUNTIME_DIR 指向的 OpenClaw runtime 不完整: ${dir.path}',\n );\n }\n return dir;\n }\n\n String? _configuredRuntimeDirOverride(String platformKey) {\n final raw = _runtimeDirOverrideDefine.trim().isNotEmpty\n ? _runtimeDirOverrideDefine.trim()\n : Platform.environment['DACHENG_OPENCLAW_RUNTIME_DIR']?.trim();\n if (raw == null || raw.isEmpty) return null;\n return p.normalize(raw.replaceAll('{platform}', platformKey));\n }\n\n Future<Directory> _legacyRuntimeDir(\n _OpenClawBundleSpec spec,\n String platformKey,\n ) async {\n final support = await getApplicationSupportDirectory();\n return Directory(\n p.join(\n support.path,\n 'openclaw_embedded',\n 'runtime',\n spec.version,\n platformKey,\n ),\n );\n }\n\n OpenClawRuntimeStatus _remember(OpenClawRuntimeStatus status) {\n _lastStatus = status;\n return status;\n }\n\n String? get _platformKey {\n final abi = Abi.current().toString().toLowerCase();\n if (Platform.isMacOS) {\n return abi.contains('arm64') ? 'macos-arm64' : 'macos-x64';\n }\n if (Platform.isWindows) {\n return abi.contains('arm64') ? 'windows-arm64' : 'windows-x64';\n }\n if (Platform.isLinux) {\n return abi.contains('arm64') ? 'linux-arm64' : 'linux-x64';\n }\n return null;\n }\n\n OpenClawRuntimeStatus? get lastStatus => _lastStatus;\n\n void _diag(\n String message, {\n Map<String, Object?> data = const {},\n Object? error,\n StackTrace? stackTrace,\n }) {\n unawaited(\n DiagnosticLogService.instance.log(\n 'openclaw.runtime',\n message,\n data: data,\n error: error,\n stackTrace: stackTrace,\n ),\n );\n }\n}\n\nint? _readInt(Object? value) {\n if (value is int) return value;\n if (value is num) return value.toInt();\n if (value is String) return int.tryParse(value);\n return null;\n}\n\nObject? _decodeJsonOrNull(String body) {\n try {\n return jsonDecode(body);\n } catch (_) {\n return null;\n }\n}\n\nbool _looksLikeHealthPayload(Object? payload) {\n if (payload is! Map) return false;\n if (payload['ok'] == true) return true;\n final status = payload['status']?.toString().toLowerCase();\n return status == 'live' || status == 'ok' || status == 'healthy';\n}\n\nbool _looksLikeModelsPayload(Object? payload) {\n if (payload is List) return true;\n if (payload is! Map) return false;\n return payload['data'] is List ||\n payload['models'] is List ||\n payload['object'] == 'list';\n}\n\nString _previewBody(String body) {\n final trimmed = body.trim();\n return trimmed.length <= 80 ? trimmed : trimmed.substring(0, 80);\n}\n\nString _previewProcessOutput(Object? output) {\n final text = output?.toString().trim() ?? '';\n return text.length <= 200 ? text : text.substring(0, 200);\n}\n"}, "87fdc3b90f5ef3ec5a885d446bfdd7786e9e9fdb": {"t": 1785757945.9949613, "n": 97198, "h": "2aff8c1751728d25", "c": "import 'dart:async';\nimport 'dart:convert';\nimport 'dart:ffi' show Abi;\nimport 'dart:io';\nimport 'dart:math' as math;\nimport 'package:archive/archive_io.dart';\nimport 'package:convert/convert.dart';\nimport 'package:crypto/crypto.dart';\nimport 'package:flutter/foundation.dart';\nimport 'package:flutter/services.dart';\nimport 'package:http/http.dart' as http;\nimport 'package:path/path.dart' as p;\nimport 'package:path_provider/path_provider.dart';\n\nimport '../../core/config/app_config.dart';\nimport '../app_settings.dart';\nimport '../diagnostic_log_service.dart';\nimport '../desktop_control/desktop_control_bridge.dart';\nimport '../desktop_control/desktop_control_policy.dart';\nimport 'openclaw_port_resolver.dart';\n\nenum OpenClawRuntimeState {\n unsupported,\n notBundled,\n stopped,\n starting,\n running,\n failed,\n}\n\nclass OpenClawRuntimeStatus {\n final OpenClawRuntimeState state;\n final String message;\n final int? port;\n final String? platformKey;\n final String? runtimePath;\n final Map<String, dynamic>? desktopToolsStatus;\n final DateTime checkedAt;\n\n const OpenClawRuntimeStatus({\n required this.state,\n required this.message,\n this.port,\n this.platformKey,\n this.runtimePath,\n this.desktopToolsStatus,\n required this.checkedAt,\n });\n\n bool get isHealthy => state == OpenClawRuntimeState.running;\n\n String get label {\n switch (state) {\n case OpenClawRuntimeState.unsupported:\n return '不支持';\n case OpenClawRuntimeState.notBundled:\n return '未内置';\n case OpenClawRuntimeState.stopped:\n return '未启动';\n case OpenClawRuntimeState.starting:\n return '启动中';\n case OpenClawRuntimeState.running:\n return '运行中';\n case OpenClawRuntimeState.failed:\n return '异常';\n }\n }\n}\n\nclass OpenClawGatewayTarget {\n final Uri baseUri;\n final String token;\n final String model;\n final String? modelOverride;\n final Uri? desktopToolsUri;\n final String? desktopToolsToken;\n final Map<String, dynamic>? desktopToolsStatus;\n\n const OpenClawGatewayTarget({\n required this.baseUri,\n required this.token,\n required this.model,\n this.modelOverride,\n this.desktopToolsUri,\n this.desktopToolsToken,\n this.desktopToolsStatus,\n });\n}\n\nclass OpenClawCliResult {\n final List<String> args;\n final int exitCode;\n final String stdout;\n final String stderr;\n final bool timedOut;\n\n const OpenClawCliResult({\n required this.args,\n required this.exitCode,\n required this.stdout,\n required this.stderr,\n this.timedOut = false,\n });\n\n bool get succeeded => exitCode == 0 && !timedOut;\n\n String get command => 'openclaw ${args.join(' ')}';\n\n String get combinedOutput {\n final parts = <String>[\n if (stdout.trim().isNotEmpty) stdout.trimRight(),\n if (stderr.trim().isNotEmpty) stderr.trimRight(),\n ];\n return parts.join('\\n');\n }\n\n OpenClawCliResult append(OpenClawCliResult next) {\n return OpenClawCliResult(\n args: [...args, '&&', ...next.args],\n exitCode: next.exitCode,\n stdout: [\n if (stdout.trim().isNotEmpty) stdout.trimRight(),\n if (next.stdout.trim().isNotEmpty) next.stdout.trimRight(),\n ].join('\\n'),\n stderr: [\n if (stderr.trim().isNotEmpty) stderr.trimRight(),\n if (next.stderr.trim().isNotEmpty) next.stderr.trimRight(),\n ].join('\\n'),\n timedOut: timedOut || next.timedOut,\n );\n }\n}\n\nclass _OpenClawBundleSpec {\n final String version;\n final int defaultPort;\n final String defaultModel;\n final String? defaultModelOverride;\n final String nodeExecutable;\n final String cliEntrypoint;\n final List<String> gatewayArgs;\n final bool downloaded;\n\n const _OpenClawBundleSpec({\n required this.version,\n required this.defaultPort,\n required this.defaultModel,\n this.defaultModelOverride,\n required this.nodeExecutable,\n required this.cliEntrypoint,\n required this.gatewayArgs,\n this.downloaded = false,\n });\n\n Map<String, dynamic> toJson() => {\n 'schema': 1,\n 'version': version,\n 'defaultPort': defaultPort,\n 'defaultModel': defaultModel,\n if (defaultModelOverride != null)\n 'defaultModelOverride': defaultModelOverride,\n 'nodeExecutable': nodeExecutable,\n 'cliEntrypoint': cliEntrypoint,\n 'gatewayArgs': gatewayArgs,\n 'downloaded': downloaded,\n };\n}\n\nclass _OpenClawRuntimeRelease {\n final String version;\n final Uri archiveUri;\n final String sha256Hex;\n final int size;\n final int defaultPort;\n final String defaultModel;\n final String? defaultModelOverride;\n final String nodeExecutable;\n final String cliEntrypoint;\n final List<String> gatewayArgs;\n\n const _OpenClawRuntimeRelease({\n required this.version,\n required this.archiveUri,\n required this.sha256Hex,\n required this.size,\n required this.defaultPort,\n required this.defaultModel,\n this.defaultModelOverride,\n required this.nodeExecutable,\n required this.cliEntrypoint,\n required this.gatewayArgs,\n });\n\n _OpenClawBundleSpec toBundleSpec() => _OpenClawBundleSpec(\n version: version,\n defaultPort: defaultPort,\n defaultModel: defaultModel,\n defaultModelOverride: defaultModelOverride,\n nodeExecutable: nodeExecutable,\n cliEntrypoint: cliEntrypoint,\n gatewayArgs: gatewayArgs,\n downloaded: true,\n );\n}\n\nclass _DesktopToolsLaunch {\n final Uri? uri;\n final String? token;\n final Map<String, dynamic> statusJson;\n\n const _DesktopToolsLaunch({\n required this.uri,\n required this.token,\n required this.statusJson,\n });\n}\n\nclass _OpenClawCliLaunch {\n final _OpenClawBundleSpec spec;\n final Directory runtimeDir;\n final Directory stateRoot;\n final File configPath;\n final String nodePath;\n final String cliPath;\n final int port;\n final String token;\n\n const _OpenClawCliLaunch({\n required this.spec,\n required this.runtimeDir,\n required this.stateRoot,\n required this.configPath,\n required this.nodePath,\n required this.cliPath,\n required this.port,\n required this.token,\n });\n}\n\nclass OpenClawRuntimeException implements Exception {\n final String message;\n\n const OpenClawRuntimeException(this.message);\n\n @override\n String toString() => message;\n}\n\nclass OpenClawRuntime {\n OpenClawRuntime._();\n\n static final OpenClawRuntime instance = OpenClawRuntime._();\n\n static const String _manifestAsset = 'assets/openclaw/bundle_manifest.json';\n static const String _runtimeDirOverrideDefine = String.fromEnvironment(\n 'DACHENG_OPENCLAW_RUNTIME_DIR',\n );\n static const String _defaultGatewayModel =\n AppSettings.defaultOpenClawGatewayModel;\n static const String _defaultDeepSeekModel =\n AppSettings.defaultOpenClawDeepSeekModel;\n static const String _backendDeepSeekProviderId = 'dacheng-deepseek-proxy';\n static const String _desktopToolsPluginId = 'dacheng-desktop-tools';\n static const String _weChatPluginId = 'openclaw-weixin';\n static const String _weChatPluginPackage = '@tencent-weixin/openclaw-weixin';\n static const Duration _startupTimeout = Duration(seconds: 120);\n static const Duration _probeTimeout = Duration(seconds: 3);\n static const Duration _cliDefaultTimeout = Duration(seconds: 45);\n\n Process? _process;\n Future<OpenClawGatewayTarget>? _starting;\n String _processAuthToken = '';\n OpenClawRuntimeStatus? _lastStatus;\n final List<String> _recentLogs = <String>[];\n final OpenClawPortResolver _portResolver = const OpenClawPortResolver();\n\n Future<OpenClawRuntimeStatus> getStatus({bool probe = true}) async {\n final platformKey = _platformKey;\n final port = await AppSettings.getOpenClawGatewayPort();\n _diag(\n 'status.start',\n data: {'platformKey': platformKey, 'port': port, 'probe': probe},\n );\n\n if (platformKey == null) {\n return _remember(\n OpenClawRuntimeStatus(\n state: OpenClawRuntimeState.unsupported,\n message: '当前平台不是 macOS / Windows / Linux 桌面端',\n port: port,\n checkedAt: DateTime.now(),\n ),\n );\n }\n\n try {\n final spec = await _loadSpec(platformKey, checkUpdates: false);\n final overrideRuntimeDir = await _runtimeDirOverride(spec, platformKey);\n final bundledRuntimeDir = overrideRuntimeDir == null\n ? await _bundledRuntimeDir(spec, platformKey)\n : null;\n final runtimeDir =\n overrideRuntimeDir ??\n bundledRuntimeDir ??\n await _runtimeDir(spec, platformKey);\n final nodePath = p.join(runtimeDir.path, spec.nodeExecutable);\n final cliPath = p.join(runtimeDir.path, spec.cliEntrypoint);\n final nodeExists = await File(nodePath).exists();\n final cliExists = await File(cliPath).exists();\n _diag(\n 'status.paths',\n data: {\n 'platformKey': platformKey,\n 'runtimeDir': runtimeDir.path,\n 'nodePath': nodePath,\n 'nodeExists': nodeExists,\n 'cliPath': cliPath,\n 'cliExists': cliExists,\n 'runtimeSource': overrideRuntimeDir != null\n ? 'override'\n : (bundledRuntimeDir == null ? 'cache' : 'bundle'),\n },\n );\n\n if (!nodeExists || !cliExists) {\n final hasBundleAssets = await _hasRequiredBundleAssets(\n platformKey,\n spec,\n );\n _diag(\n 'status.bundle-assets',\n data: {\n 'platformKey': platformKey,\n 'hasBundleAssets': hasBundleAssets,\n 'nodeAsset': 'assets/openclaw/$platformKey/${spec.nodeExecutable}',\n 'cliAsset': 'assets/openclaw/$platformKey/${spec.cliEntrypoint}',\n },\n );\n return _remember(\n OpenClawRuntimeStatus(\n state: hasBundleAssets\n ? OpenClawRuntimeState.stopped\n : OpenClawRuntimeState.notBundled,\n message: hasBundleAssets\n ? '内置 OpenClaw 已随 App 打包,尚未释放或启动'\n : '当前 App 包未包含 $platformKey 的 OpenClaw runtime',\n port: port,\n platformKey: platformKey,\n runtimePath: runtimeDir.path,\n checkedAt: DateTime.now(),\n ),\n );\n }\n\n if (probe) {\n final healthy = await _probe(\n port,\n await AppSettings.getOpenClawGatewayToken(),\n );\n if (healthy) {\n return _remember(\n OpenClawRuntimeStatus(\n state: OpenClawRuntimeState.running,\n message: '本机 OpenClaw Gateway 正在运行',\n port: port,\n platformKey: platformKey,\n runtimePath: runtimeDir.path,\n checkedAt: DateTime.now(),\n ),\n );\n }\n }\n\n return _remember(\n OpenClawRuntimeStatus(\n state: _process == null\n ? OpenClawRuntimeState.stopped\n : OpenClawRuntimeState.starting,\n message: _process == null ? '本机 OpenClaw 未启动' : '本机 OpenClaw 正在启动',\n port: port,\n platformKey: platformKey,\n runtimePath: runtimeDir.path,\n checkedAt: DateTime.now(),\n ),\n );\n } catch (error) {\n _diag('status.error', error: error);\n return _remember(\n OpenClawRuntimeStatus(\n state: OpenClawRuntimeState.failed,\n message: error.toString(),\n port: port,\n platformKey: platformKey,\n checkedAt: DateTime.now(),\n ),\n );\n }\n }\n\n Future<OpenClawGatewayTarget> ensureStarted({\n String? authToken,\n String? username,\n bool isMember = false,\n }) {\n _starting ??= _ensureStartedInternal(\n authToken: authToken,\n username: username,\n isMember: isMember,\n ).whenComplete(() => _starting = null);\n return _starting!;\n }\n\n Future<OpenClawRuntimeStatus> restart() async {\n await stop();\n try {\n await ensureStarted();\n return getStatus();\n } catch (error) {\n return _remember(\n OpenClawRuntimeStatus(\n state: OpenClawRuntimeState.failed,\n message: error.toString(),\n port: await AppSettings.getOpenClawGatewayPort(),\n platformKey: _platformKey,\n checkedAt: DateTime.now(),\n ),\n );\n }\n }\n\n Future<void> stop() async {\n final process = _process;\n _process = null;\n _processAuthToken = '';\n if (process == null) return;\n process.kill();\n try {\n await process.exitCode.timeout(const Duration(seconds: 5));\n } catch (_) {\n process.kill();\n }\n }\n\n Future<OpenClawCliResult> createMobilePairingCode({bool remote = true}) {\n return runCli([\n 'qr',\n if (remote) '--remote',\n '--json',\n ], timeout: const Duration(seconds: 30));\n }\n\n Future<OpenClawCliResult> loginWeChat() {\n return runCli([\n 'channels',\n 'login',\n '--channel',\n _weChatPluginId,\n ], timeout: const Duration(minutes: 3));\n }\n\n Future<OpenClawCliResult> inspectChannels() {\n return runCli([\n 'channels',\n 'status',\n '--probe',\n ], timeout: const Duration(seconds: 40));\n }\n\n Future<OpenClawCliResult> installWeChatPlugin() async {\n final install = await runCli(\n ['plugins', 'install', _weChatPluginPackage, '--force'],\n timeout: const Duration(minutes: 3),\n ensureGateway: false,\n );\n if (!install.succeeded) return install;\n final enable = await runCli(\n ['config', 'set', 'plugins.entries.$_weChatPluginId.enabled', 'true'],\n timeout: const Duration(seconds: 30),\n ensureGateway: false,\n );\n return install.append(enable);\n }\n\n Future<OpenClawCliResult> runCli(\n List<String> args, {\n Duration timeout = _cliDefaultTimeout,\n bool ensureGateway = true,\n }) async {\n if (args.isEmpty) {\n throw const OpenClawRuntimeException('OpenClaw CLI 参数不能为空');\n }\n final launch = await _prepareCliLaunch(ensureGateway: ensureGateway);\n final processArgs = <String>[launch.cliPath, ...args];\n _diag(\n 'cli.start',\n data: {\n 'args': args,\n 'runtimeDir': launch.runtimeDir.path,\n 'configPath': launch.configPath.path,\n 'timeoutSeconds': timeout.inSeconds,\n },\n );\n\n final process = await Process.start(\n launch.nodePath,\n processArgs,\n workingDirectory: launch.runtimeDir.path,\n environment: _buildOpenClawEnvironment(\n runtimeDir: launch.runtimeDir,\n stateRoot: launch.stateRoot,\n configPath: launch.configPath,\n port: launch.port,\n token: launch.token,\n ),\n mode: ProcessStartMode.normal,\n runInShell: false,\n );\n final stdoutBuffer = StringBuffer();\n final stderrBuffer = StringBuffer();\n final stdoutSub = process.stdout\n .transform(utf8.decoder)\n .listen((chunk) => _appendBounded(stdoutBuffer, chunk));\n final stderrSub = process.stderr\n .transform(utf8.decoder)\n .listen((chunk) => _appendBounded(stderrBuffer, chunk));\n\n var timedOut = false;\n int exitCode;\n try {\n exitCode = await process.exitCode.timeout(timeout);\n } on TimeoutException {\n timedOut = true;\n process.kill();\n exitCode = await process.exitCode.timeout(\n const Duration(seconds: 5),\n onTimeout: () => -1,\n );\n } finally {\n await stdoutSub.cancel();\n await stderrSub.cancel();\n }\n\n final result = OpenClawCliResult(\n args: args,\n exitCode: exitCode,\n stdout: stdoutBuffer.toString(),\n stderr: stderrBuffer.toString(),\n timedOut: timedOut,\n );\n _diag(\n result.succeeded ? 'cli.complete' : 'cli.failed',\n data: {\n 'args': args,\n 'exitCode': exitCode,\n 'timedOut': timedOut,\n 'stdoutPreview': _previewProcessOutput(result.stdout),\n 'stderrPreview': _previewProcessOutput(result.stderr),\n },\n );\n return result;\n }\n\n Future<OpenClawGatewayTarget> _ensureStartedInternal({\n String? authToken,\n String? username,\n bool isMember = false,\n }) async {\n final platformKey = _platformKey;\n _diag('ensure-start.start', data: {'platformKey': platformKey});\n if (platformKey == null) {\n throw const OpenClawRuntimeException('当前平台不支持内置 OpenClaw Gateway');\n }\n\n final spec = await _loadSpec(platformKey, checkUpdates: true);\n final requestedPort = await AppSettings.getOpenClawGatewayPort(\n defaultValue: spec.defaultPort,\n );\n final token = await AppSettings.getOpenClawGatewayToken();\n final savedModel = await AppSettings.getOpenClawModel(\n defaultValue: spec.defaultModel,\n );\n final model = _gatewayChatModel(savedModel);\n final requestedAuthToken = authToken?.trim() ?? '';\n final backendDeepSeekModel = _backendDeepSeekModelRef(\n await AppSettings.getOpenClawDeepSeekModel(\n defaultValue: _defaultDeepSeekModelFor(spec.defaultModel),\n ),\n );\n final deepSeekProxyBaseUrl = _deepSeekProxyBaseUrl();\n final modelOverride = await AppSettings.getOpenClawModelOverride(\n defaultValue: spec.defaultModelOverride ?? '',\n );\n final remoteGatewayUrl = await AppSettings.getOpenClawRemoteGatewayUrl();\n final desktopTools = await _ensureDesktopTools();\n _diag(\n 'ensure-start.config',\n data: {\n 'platformKey': platformKey,\n 'requestedPort': requestedPort,\n 'model': model,\n 'rawModel': savedModel,\n 'backendDeepSeekModel': backendDeepSeekModel,\n 'deepSeekProxyBaseUrl': deepSeekProxyBaseUrl,\n 'hasAuthToken': requestedAuthToken.isNotEmpty,\n 'modelOverrideSet': modelOverride.trim().isNotEmpty,\n 'remoteGatewayUrlSet': remoteGatewayUrl.trim().isNotEmpty,\n 'desktopToolsUri': desktopTools.uri?.toString(),\n 'desktopToolsStatus': desktopTools.statusJson,\n },\n );\n\n final canReuseStartedProcess =\n _process != null && _processAuthToken == requestedAuthToken;\n if (canReuseStartedProcess &&\n await _probe(requestedPort, token, context: 'pre-start')) {\n _diag('ensure-start.reuse-existing', data: {'port': requestedPort});\n return OpenClawGatewayTarget(\n baseUri: Uri.parse('http://127.0.0.1:$requestedPort'),\n token: token,\n model: model,\n modelOverride: modelOverride.trim().isEmpty\n ? null\n : modelOverride.trim(),\n desktopToolsUri: desktopTools.uri,\n desktopToolsToken: desktopTools.token,\n desktopToolsStatus: desktopTools.statusJson,\n );\n } else if (_process != null) {\n _diag(\n 'ensure-start.restart-auth-changed',\n data: {\n 'hasPreviousAuthToken': _processAuthToken.isNotEmpty,\n 'hasRequestedAuthToken': requestedAuthToken.isNotEmpty,\n },\n );\n await stop();\n }\n\n final runtimeDir = await _prepareBundle(spec, platformKey);\n final stateRoot = await _stateRoot();\n final desktopToolsPluginDir = await _ensureDesktopToolsPlugin(\n stateRoot: stateRoot,\n );\n if (_process != null) {\n _diag(\n 'process.stop-stale-before-start',\n data: {'requestedPort': requestedPort},\n );\n await stop();\n }\n final portCandidate = await _portResolver.resolve(requestedPort);\n final port = portCandidate.port;\n if (portCandidate.isFallback) {\n await AppSettings.setOpenClawGatewayPort(port);\n _diag(\n 'ensure-start.fallback-port',\n data: {\n 'requestedPort': requestedPort,\n 'selectedPort': port,\n if (portCandidate.reason != null) 'reason': portCandidate.reason,\n },\n );\n } else {\n _diag(\n 'ensure-start.port-bindable',\n data: {'requestedPort': requestedPort, 'selectedPort': port},\n );\n }\n final configPath = await _ensureConfigFile(\n runtimeDir: runtimeDir,\n stateRoot: stateRoot,\n port: port,\n token: token,\n backendDeepSeekModel: backendDeepSeekModel,\n deepSeekProxyBaseUrl: deepSeekProxyBaseUrl,\n remoteGatewayUrl: remoteGatewayUrl,\n extraPluginLoadPaths: [desktopToolsPluginDir.path],\n );\n await _ensureAgentModelConfig(\n stateRoot: stateRoot,\n deepSeekProxyBaseUrl: deepSeekProxyBaseUrl,\n );\n final desktopToolsManifestPath = await _ensureDesktopToolsManifest(\n stateRoot: stateRoot,\n desktopTools: desktopTools,\n );\n\n final nodePath = p.join(runtimeDir.path, spec.nodeExecutable);\n final cliPath = p.join(runtimeDir.path, spec.cliEntrypoint);\n if (!await File(nodePath).exists()) {\n _diag('ensure-start.missing-node', data: {'nodePath': nodePath});\n throw OpenClawRuntimeException('OpenClaw 内置 Node 不存在: $nodePath');\n }\n if (!await File(cliPath).exists()) {\n _diag('ensure-start.missing-cli', data: {'cliPath': cliPath});\n throw OpenClawRuntimeException('OpenClaw CLI 入口不存在: $cliPath');\n }\n\n _remember(\n OpenClawRuntimeStatus(\n state: OpenClawRuntimeState.starting,\n message: '正在启动内置 OpenClaw Gateway',\n port: port,\n platformKey: platformKey,\n runtimePath: runtimeDir.path,\n checkedAt: DateTime.now(),\n ),\n );\n\n final args = <String>[\n cliPath,\n ...spec.gatewayArgs.map((arg) => arg.replaceAll('{port}', '$port')),\n ];\n _recentLogs.clear();\n _diag(\n 'process.starting',\n data: {\n 'nodePath': nodePath,\n 'args': args,\n 'workingDirectory': runtimeDir.path,\n 'configPath': configPath.path,\n 'desktopToolsPluginPath': desktopToolsPluginDir.path,\n 'desktopToolsManifestPath': desktopToolsManifestPath.path,\n 'requestedPort': requestedPort,\n 'selectedPort': port,\n },\n );\n\n final env =\n _buildOpenClawEnvironment(\n runtimeDir: runtimeDir,\n stateRoot: stateRoot,\n configPath: configPath,\n port: port,\n token: token,\n )..addAll({\n 'DACHENG_DESKTOP_TOOLS_ENABLED': desktopTools.uri == null ? '0' : '1',\n 'DACHENG_DESKTOP_TOOLS_MANIFEST': desktopToolsManifestPath.path,\n if (desktopTools.uri != null)\n 'DACHENG_DESKTOP_TOOLS_URL': desktopTools.uri.toString(),\n if (desktopTools.token != null)\n 'DACHENG_DESKTOP_TOOLS_TOKEN': desktopTools.token!,\n 'DACHENG_APP_RUNTIME': '1',\n // This is a process-local OpenClaw capability, never a Mahayana\n // account credential. Account HTTP stays inside the Rust runtime.\n 'DACHENG_AUTH_TOKEN': token,\n if (username != null && username.isNotEmpty)\n 'DACHENG_USERNAME': username,\n 'DACHENG_IS_MEMBER': isMember ? '1' : '0',\n 'DACHENG_OPENCLAW_PROXY_TOKEN': 'dacheng-openclaw-proxy',\n });\n\n _process = await Process.start(\n nodePath,\n args,\n workingDirectory: runtimeDir.path,\n environment: env,\n mode: ProcessStartMode.normal,\n runInShell: false,\n );\n _processAuthToken = requestedAuthToken;\n _captureLogs(_process!);\n _diag('process.started', data: {'pid': _process!.pid, 'port': port});\n\n final deadline = DateTime.now().add(_startupTimeout);\n while (DateTime.now().isBefore(deadline)) {\n if (await _probe(port, token, context: 'startup')) {\n _diag('process.ready', data: {'pid': _process?.pid, 'port': port});\n _remember(\n OpenClawRuntimeStatus(\n state: OpenClawRuntimeState.running,\n message: '本机 OpenClaw Gateway 已启动',\n port: port,\n platformKey: platformKey,\n runtimePath: runtimeDir.path,\n desktopToolsStatus: desktopTools.statusJson,\n checkedAt: DateTime.now(),\n ),\n );\n return OpenClawGatewayTarget(\n baseUri: Uri.parse('http://127.0.0.1:$port'),\n token: token,\n model: model,\n modelOverride: modelOverride.trim().isEmpty\n ? null\n : modelOverride.trim(),\n desktopToolsUri: desktopTools.uri,\n desktopToolsToken: desktopTools.token,\n desktopToolsStatus: desktopTools.statusJson,\n );\n }\n\n if (_process != null) {\n final exited = await _process!.exitCode.timeout(\n const Duration(milliseconds: 1),\n onTimeout: () => -999999,\n );\n if (exited != -999999) {\n final logs = _recentLogs.take(12).join('\\n');\n _diag(\n 'process.exited-early',\n data: {\n 'exitCode': exited,\n 'recentLogs': _recentLogs.take(12).toList(),\n },\n );\n throw OpenClawRuntimeException(\n 'OpenClaw Gateway 提前退出,exitCode=$exited${logs.isEmpty ? '' : '\\n$logs'}',\n );\n }\n }\n if (_recentLogsContainRuntimeModuleFailure()) {\n final logs = _recentLogs.take(12).join('\\n');\n _diag(\n 'process.runtime-module-failure',\n data: {'recentLogs': _recentLogs.take(12).toList()},\n );\n await stop();\n throw OpenClawRuntimeException(\n 'OpenClaw runtime 模块缓存异常${logs.isEmpty ? '' : '\\n$logs'}',\n );\n }\n await Future<void>.delayed(const Duration(milliseconds: 450));\n }\n\n final logs = _recentLogs.take(12).join('\\n');\n _diag(\n 'process.startup-timeout',\n data: {\n 'timeoutSeconds': _startupTimeout.inSeconds,\n 'recentLogs': _recentLogs.take(12).toList(),\n },\n );\n throw OpenClawRuntimeException(\n 'OpenClaw Gateway 启动超时${logs.isEmpty ? '' : '\\n$logs'}',\n );\n }\n\n Future<_OpenClawBundleSpec> _loadSpec(\n String platformKey, {\n bool checkUpdates = false,\n }) async {\n final raw = await rootBundle.loadString(_manifestAsset);\n final decoded = jsonDecode(raw) as Map<String, dynamic>;\n final platforms = Map<String, dynamic>.from(\n decoded['platforms'] as Map? ?? const {},\n );\n final platform = Map<String, dynamic>.from(\n platforms[platformKey] as Map? ?? const {},\n );\n if (platform.isEmpty) {\n throw OpenClawRuntimeException('bundle_manifest.json 缺少 $platformKey 配置');\n }\n\n final gatewayArgs =\n (platform['gatewayArgs'] as List? ??\n decoded['gatewayArgs'] as List? ??\n const ['gateway', '--port', '{port}'])\n .map((item) => item.toString())\n .toList();\n\n final spec = _OpenClawBundleSpec(\n version: (decoded['version'] ?? 'dev').toString(),\n defaultPort: _readInt(decoded['defaultPort']) ?? 18789,\n defaultModel: (decoded['defaultModel'] ?? 'openclaw/default').toString(),\n defaultModelOverride: decoded['defaultModelOverride']?.toString(),\n nodeExecutable: (platform['nodeExecutable'] ?? 'node/bin/node')\n .toString(),\n cliEntrypoint: (platform['cliEntrypoint'] ?? 'openclaw/bin/openclaw.js')\n .toString(),\n gatewayArgs: gatewayArgs,\n );\n _diag(\n 'manifest.loaded',\n data: {\n 'platformKey': platformKey,\n 'version': spec.version,\n 'defaultPort': spec.defaultPort,\n 'defaultModel': spec.defaultModel,\n 'nodeExecutable': spec.nodeExecutable,\n 'cliEntrypoint': spec.cliEntrypoint,\n 'gatewayArgs': spec.gatewayArgs,\n },\n );\n final allowDownloadedRuntime = await _allowDownloadedRuntime(platformKey);\n var effectiveSpec = spec;\n if (allowDownloadedRuntime) {\n effectiveSpec =\n await _loadActiveDownloadedSpec(platformKey, fallbackSpec: spec) ??\n spec;\n } else {\n await _clearActiveDownloadedRuntimeSpec(\n platformKey: platformKey,\n reason: 'macos-sandbox',\n );\n }\n if (checkUpdates && allowDownloadedRuntime) {\n effectiveSpec =\n await _maybeInstallRuntimeUpdate(\n platformKey: platformKey,\n currentSpec: effectiveSpec,\n ) ??\n effectiveSpec;\n } else if (checkUpdates) {\n _diag(\n 'runtime-update.skip-disabled',\n data: {'platformKey': platformKey, 'reason': 'macos-sandbox'},\n );\n }\n return effectiveSpec;\n }\n\n Future<bool> _allowDownloadedRuntime(String platformKey) async {\n if (!Platform.isMacOS) return true;\n\n final support = await getApplicationSupportDirectory();\n final sandboxContainerId =\n Platform.environment['APP_SANDBOX_CONTAINER_ID']?.trim() ?? '';\n final pathLooksSandboxed = p\n .split(p.normalize(support.path))\n .contains('Containers');\n final sandboxed = sandboxContainerId.isNotEmpty || pathLooksSandboxed;\n if (!sandboxed) return true;\n\n // App Store/TestFlight sandboxed apps cannot reliably execute downloaded\n // Mach-O payloads. Use the bundled runtime that was signed with inherited\n // sandbox entitlements during packaging.\n _diag(\n 'runtime-update.disabled-macos-sandbox',\n data: {\n 'platformKey': platformKey,\n 'supportPath': support.path,\n 'hasSandboxContainerId': sandboxContainerId.isNotEmpty,\n },\n );\n return false;\n }\n\n Future<void> _clearActiveDownloadedRuntimeSpec({\n required String platformKey,\n required String reason,\n }) async {\n final saved = await AppSettings.getOpenClawActiveRuntimeSpec();\n if (saved == null) return;\n await AppSettings.clearOpenClawActiveRuntimeSpec();\n _diag(\n 'runtime-update.active-cleared',\n data: {\n 'platformKey': platformKey,\n 'reason': reason,\n 'version': saved['version']?.toString(),\n 'downloaded': saved['downloaded'] == true,\n },\n );\n }\n\n Future<_OpenClawBundleSpec?> _loadActiveDownloadedSpec(\n String platformKey, {\n required _OpenClawBundleSpec fallbackSpec,\n }) async {\n final saved = await AppSettings.getOpenClawActiveRuntimeSpec();\n if (saved == null) return null;\n final spec = _specFromJson(saved, downloaded: true);\n if (spec == null) {\n await AppSettings.clearOpenClawActiveRuntimeSpec();\n return null;\n }\n final runtimeDir = await _cachedRuntimeDirIfComplete(spec, platformKey);\n if (runtimeDir == null) {\n await AppSettings.clearOpenClawActiveRuntimeSpec();\n _diag(\n 'runtime-update.active-missing',\n data: {'platformKey': platformKey, 'version': spec.version},\n );\n return null;\n }\n if (_isRuntimeVersionNewer(fallbackSpec.version, spec.version)) {\n await AppSettings.clearOpenClawActiveRuntimeSpec();\n _diag(\n 'runtime-update.active-cleared',\n data: {\n 'platformKey': platformKey,\n 'reason': 'bundled-newer',\n 'version': spec.version,\n 'bundledVersion': fallbackSpec.version,\n },\n );\n return null;\n }\n _diag(\n 'runtime-update.active-loaded',\n data: {\n 'platformKey': platformKey,\n 'version': spec.version,\n 'runtimeDir': runtimeDir.path,\n 'bundledVersion': fallbackSpec.version,\n },\n );\n return spec;\n }\n\n bool _isRuntimeVersionNewer(String candidate, String current) {\n final candidateRank = _runtimeVersionRank(candidate);\n final currentRank = _runtimeVersionRank(current);\n final length = math.max(candidateRank.length, currentRank.length);\n for (var i = 0; i < length; i++) {\n final a = i < candidateRank.length ? candidateRank[i] : 0;\n final b = i < currentRank.length ? currentRank[i] : 0;\n if (a != b) return a > b;\n }\n return false;\n }\n\n List<int> _runtimeVersionRank(String version) {\n return RegExp(r'\\d+')\n .allMatches(version)\n .map((match) => int.tryParse(match.group(0) ?? '') ?? 0)\n .toList(growable: false);\n }\n\n _OpenClawBundleSpec? _specFromJson(\n Map<String, dynamic> json, {\n required bool downloaded,\n }) {\n final version = json['version']?.toString().trim() ?? '';\n final nodeExecutable = json['nodeExecutable']?.toString().trim() ?? '';\n final cliEntrypoint = json['cliEntrypoint']?.toString().trim() ?? '';\n if (version.isEmpty || nodeExecutable.isEmpty || cliEntrypoint.isEmpty) {\n return null;\n }\n return _OpenClawBundleSpec(\n version: version,\n defaultPort: _readInt(json['defaultPort']) ?? 18789,\n defaultModel: (json['defaultModel'] ?? _defaultGatewayModel).toString(),\n defaultModelOverride: json['defaultModelOverride']?.toString(),\n nodeExecutable: nodeExecutable,\n cliEntrypoint: cliEntrypoint,\n gatewayArgs:\n (json['gatewayArgs'] as List? ??\n const ['gateway', '--port', '{port}', '--force'])\n .map((item) => item.toString())\n .toList(),\n downloaded: downloaded,\n );\n }\n\n Future<_OpenClawBundleSpec?> _maybeInstallRuntimeUpdate({\n required String platformKey,\n required _OpenClawBundleSpec currentSpec,\n }) async {\n if (_configuredRuntimeDirOverride(platformKey) != null) {\n _diag('runtime-update.skip-override', data: {'platformKey': platformKey});\n return null;\n }\n\n try {\n final release = await _fetchRuntimeRelease(\n platformKey: platformKey,\n currentVersion: currentSpec.version,\n );\n if (release == null || release.version == currentSpec.version) {\n return null;\n }\n\n final releaseSpec = release.toBundleSpec();\n final cached = await _cachedRuntimeDirIfComplete(\n releaseSpec,\n platformKey,\n );\n if (cached != null) {\n await AppSettings.setOpenClawActiveRuntimeSpec(releaseSpec.toJson());\n _diag(\n 'runtime-update.use-cached',\n data: {\n 'platformKey': platformKey,\n 'version': release.version,\n 'runtimeDir': cached.path,\n },\n );\n return releaseSpec;\n }\n\n return await _downloadAndInstallRuntimeUpdate(\n release: release,\n platformKey: platformKey,\n );\n } catch (error, stackTrace) {\n _diag(\n 'runtime-update.failed',\n data: {\n 'platformKey': platformKey,\n 'currentVersion': currentSpec.version,\n 'error': error.toString(),\n },\n error: error,\n stackTrace: stackTrace,\n );\n return null;\n }\n }\n\n Future<_OpenClawRuntimeRelease?> _fetchRuntimeRelease({\n required String platformKey,\n required String currentVersion,\n }) async {\n final baseUrl = AppConfig.currentAiBackendUrl.replaceFirst(\n RegExp(r'/+$'),\n '',\n );\n final uri = Uri.parse('$baseUrl/api/openclaw/runtime/manifest').replace(\n queryParameters: {\n 'platform': platformKey,\n 'currentVersion': currentVersion,\n 'appVersion': AppConfig.appVersion,\n },\n );\n final response = await http\n .get(uri, headers: {'Accept': 'application/json'})\n .timeout(const Duration(seconds: 20));\n if (response.statusCode < 200 || response.statusCode >= 300) {\n _diag(\n 'runtime-update.manifest-http',\n data: {'statusCode': response.statusCode, 'uri': uri.toString()},\n );\n return null;\n }\n final decoded = _decodeJsonOrNull(response.body);\n if (decoded is! Map) return null;\n final payload = Map<String, dynamic>.from(decoded);\n if (payload['updateAvailable'] != true) {\n _diag(\n 'runtime-update.none',\n data: {\n 'platformKey': platformKey,\n 'reason': payload['reason']?.toString(),\n 'currentVersion': currentVersion,\n 'latestVersion': payload['latestVersion']?.toString(),\n },\n );\n return null;\n }\n final latest = payload['latest'];\n if (latest is! Map) return null;\n return _releaseFromJson(Map<String, dynamic>.from(latest));\n }\n\n _OpenClawRuntimeRelease? _releaseFromJson(Map<String, dynamic> json) {\n final version = json['version']?.toString().trim() ?? '';\n final archiveUrl = json['archiveUrl']?.toString().trim() ?? '';\n final sha256Hex = json['sha256']?.toString().trim().toLowerCase() ?? '';\n final archiveUri = Uri.tryParse(archiveUrl);\n final gatewayArgs =\n (json['gatewayArgs'] as List? ??\n const ['gateway', '--port', '{port}', '--force'])\n .map((item) => item.toString())\n .toList();\n if (version.isEmpty ||\n archiveUri == null ||\n !archiveUri.hasScheme ||\n !RegExp(r'^[a-f0-9]{64}$').hasMatch(sha256Hex)) {\n _diag(\n 'runtime-update.invalid-release',\n data: {\n 'version': version,\n 'archiveUrlSet': archiveUrl.isNotEmpty,\n 'sha256Length': sha256Hex.length,\n },\n );\n return null;\n }\n return _OpenClawRuntimeRelease(\n version: version,\n archiveUri: archiveUri,\n sha256Hex: sha256Hex,\n size: _readInt(json['size']) ?? 0,\n defaultPort: _readInt(json['defaultPort']) ?? 18789,\n defaultModel: (json['defaultModel'] ?? _defaultGatewayModel).toString(),\n defaultModelOverride: json['defaultModelOverride']?.toString(),\n nodeExecutable: (json['nodeExecutable'] ?? 'node/bin/node').toString(),\n cliEntrypoint: (json['cliEntrypoint'] ?? 'openclaw/openclaw.mjs')\n .toString(),\n gatewayArgs: gatewayArgs,\n );\n }\n\n Future<_OpenClawBundleSpec> _downloadAndInstallRuntimeUpdate({\n required _OpenClawRuntimeRelease release,\n required String platformKey,\n }) async {\n final spec = release.toBundleSpec();\n final runtimeDir = await _runtimeDir(spec, platformKey);\n final support = await getApplicationSupportDirectory();\n final downloadDir = Directory(p.join(support.path, 'openclaw_downloads'));\n await downloadDir.create(recursive: true);\n final archivePath = p.join(\n downloadDir.path,\n '${_safePathPart(release.version)}-$platformKey${_archiveExtension(release.archiveUri)}',\n );\n final archiveFile = File(archivePath);\n final digest = await _downloadRuntimeArchive(release, archiveFile);\n if (digest != release.sha256Hex) {\n await _deleteFileIfExists(archiveFile);\n throw OpenClawRuntimeException(\n 'OpenClaw runtime 校验失败: expected=${release.sha256Hex}, actual=$digest',\n );\n }\n\n final extractDir = Directory(\n p.join(\n downloadDir.path,\n '${_safePathPart(release.version)}-$platformKey-extract',\n ),\n );\n final stagingDir = Directory(\n p.join(\n downloadDir.path,\n '${_safePathPart(release.version)}-$platformKey-stage',\n ),\n );\n if (await extractDir.exists()) await extractDir.delete(recursive: true);\n if (await stagingDir.exists()) await stagingDir.delete(recursive: true);\n await extractDir.create(recursive: true);\n await _extractRuntimeArchive(archiveFile, extractDir);\n\n final extractedRoot = await _findExtractedRuntimeRoot(\n extractDir: extractDir,\n spec: spec,\n );\n if (extractedRoot == null) {\n throw OpenClawRuntimeException('下载的 OpenClaw runtime 不包含必要的 node/cli 文件');\n }\n\n await runtimeDir.parent.create(recursive: true);\n if (await runtimeDir.exists()) await runtimeDir.delete(recursive: true);\n await _moveOrCopyRuntimeDirectory(extractedRoot, stagingDir);\n await stagingDir.rename(runtimeDir.path);\n final marker = File(p.join(runtimeDir.path, '.bundle_ready'));\n await marker.writeAsString(\n const JsonEncoder.withIndent(' ').convert({\n 'installedAt': DateTime.now().toIso8601String(),\n 'version': release.version,\n 'platform': platformKey,\n 'sha256': digest,\n 'source': release.archiveUri.toString(),\n }),\n );\n await _repairRuntimeLaunchMetadata(\n runtimeDir: runtimeDir,\n nodePath: File(p.join(runtimeDir.path, spec.nodeExecutable)),\n platformKey: platformKey,\n cached: false,\n );\n await AppSettings.setOpenClawActiveRuntimeSpec(spec.toJson());\n await _deleteDirectoryIfExists(extractDir);\n await _deleteFileIfExists(archiveFile);\n _diag(\n 'runtime-update.installed',\n data: {\n 'platformKey': platformKey,\n 'version': release.version,\n 'runtimeDir': runtimeDir.path,\n 'size': release.size,\n },\n );\n return spec;\n }\n\n Future<String> _downloadRuntimeArchive(\n _OpenClawRuntimeRelease release,\n File destination,\n ) async {\n final client = http.Client();\n IOSink? output;\n try {\n final request = http.Request('GET', release.archiveUri)\n ..headers['Accept'] = 'application/octet-stream';\n final response = await client\n .send(request)\n .timeout(const Duration(seconds: 20));\n if (response.statusCode < 200 || response.statusCode >= 300) {\n throw OpenClawRuntimeException(\n 'OpenClaw runtime 下载失败 (${response.statusCode})',\n );\n }\n await destination.parent.create(recursive: true);\n output = destination.openWrite();\n final digestSink = AccumulatorSink<Digest>();\n final hashSink = sha256.startChunkedConversion(digestSink);\n var bytes = 0;\n await for (final chunk in response.stream.timeout(\n const Duration(minutes: 5),\n )) {\n bytes += chunk.length;\n hashSink.add(chunk);\n output.add(chunk);\n }\n await output.flush();\n await output.close();\n output = null;\n hashSink.close();\n final digest = digestSink.events.single.toString();\n _diag(\n 'runtime-update.downloaded',\n data: {\n 'archiveUri': release.archiveUri.toString(),\n 'bytes': bytes,\n 'sha256': digest,\n },\n );\n return digest;\n } finally {\n await output?.close().catchError((_) {});\n client.close();\n }\n }\n\n Future<void> _extractRuntimeArchive(\n File archiveFile,\n Directory destination,\n ) async {\n final lowerPath = archiveFile.path.toLowerCase();\n if (Platform.isMacOS && lowerPath.endsWith('.zip')) {\n try {\n final result = await Process.run('/usr/bin/ditto', [\n '-x',\n '-k',\n archiveFile.path,\n destination.path,\n ]).timeout(const Duration(minutes: 5));\n _diag(\n 'runtime-update.extract-native',\n data: {\n 'archivePath': archiveFile.path,\n 'destination': destination.path,\n 'exitCode': result.exitCode,\n if ((result.stderr as Object).toString().isNotEmpty)\n 'stderr': _previewProcessOutput(result.stderr),\n },\n );\n if (result.exitCode == 0) return;\n } catch (error, stackTrace) {\n _diag(\n 'runtime-update.extract-native-failed',\n data: {\n 'archivePath': archiveFile.path,\n 'destination': destination.path,\n 'error': error.toString(),\n },\n error: error,\n stackTrace: stackTrace,\n );\n }\n }\n\n await extractFileToDisk(archiveFile.path, destination.path);\n _diag(\n 'runtime-update.extract-dart',\n data: {'archivePath': archiveFile.path, 'destination': destination.path},\n );\n }\n\n Future<Directory?> _findExtractedRuntimeRoot({\n required Directory extractDir,\n required _OpenClawBundleSpec spec,\n }) async {\n final direct = await _runtimeRootIfComplete(extractDir, spec);\n if (direct != null) return direct;\n await for (final entity in extractDir.list(followLinks: false)) {\n if (entity is! Directory) continue;\n final nested = await _runtimeRootIfComplete(entity, spec);\n if (nested != null) return nested;\n }\n return null;\n }\n\n Future<Directory?> _runtimeRootIfComplete(\n Directory dir,\n _OpenClawBundleSpec spec,\n ) async {\n final nodePath = File(p.join(dir.path, spec.nodeExecutable));\n final cliPath = File(p.join(dir.path, spec.cliEntrypoint));\n if (await nodePath.exists() && await cliPath.exists()) return dir;\n return null;\n }\n\n Future<Directory?> _cachedRuntimeDirIfComplete(\n _OpenClawBundleSpec spec,\n String platformKey,\n ) async {\n final runtimeDir = await _runtimeDir(spec, platformKey);\n final marker = File(p.join(runtimeDir.path, '.bundle_ready'));\n final nodePath = File(p.join(runtimeDir.path, spec.nodeExecutable));\n final cliPath = File(p.join(runtimeDir.path, spec.cliEntrypoint));\n if (await marker.exists() &&\n await nodePath.exists() &&\n await cliPath.exists()) {\n return runtimeDir;\n }\n return null;\n }\n\n String _safePathPart(String value) {\n final safe = value.replaceAll(RegExp(r'[^A-Za-z0-9_.-]+'), '-');\n return safe.isEmpty ? 'runtime' : safe;\n }\n\n String _archiveExtension(Uri uri) {\n final lower = uri.path.toLowerCase();\n if (lower.endsWith('.tar.gz')) return '.tar.gz';\n if (lower.endsWith('.tgz')) return '.tgz';\n if (lower.endsWith('.tar')) return '.tar';\n return '.zip';\n }\n\n Future<void> _deleteFileIfExists(File file) async {\n try {\n if (await file.exists()) await file.delete();\n } catch (_) {}\n }\n\n Future<void> _deleteDirectoryIfExists(Directory dir) async {\n try {\n if (await dir.exists()) await dir.delete(recursive: true);\n } catch (_) {}\n }\n\n Future<Directory> _prepareBundle(\n _OpenClawBundleSpec spec,\n String platformKey,\n ) async {\n final overrideRuntimeDir = await _runtimeDirOverride(spec, platformKey);\n if (overrideRuntimeDir != null) {\n final nodePath = File(\n p.join(overrideRuntimeDir.path, spec.nodeExecutable),\n );\n await _repairRuntimeLaunchMetadata(\n runtimeDir: overrideRuntimeDir,\n nodePath: nodePath,\n platformKey: platformKey,\n cached: true,\n );\n _diag(\n 'bundle.launch.override-runtime',\n data: {\n 'platformKey': platformKey,\n 'runtimeDir': overrideRuntimeDir.path,\n },\n );\n return overrideRuntimeDir;\n }\n\n if (spec.downloaded) {\n final downloadedRuntimeDir = await _cachedRuntimeDirIfComplete(\n spec,\n platformKey,\n );\n if (downloadedRuntimeDir != null) {\n await _repairRuntimeLaunchMetadata(\n runtimeDir: downloadedRuntimeDir,\n nodePath: File(\n p.join(downloadedRuntimeDir.path, spec.nodeExecutable),\n ),\n platformKey: platformKey,\n cached: true,\n );\n _diag(\n 'bundle.launch.downloaded-runtime',\n data: {\n 'platformKey': platformKey,\n 'version': spec.version,\n 'runtimeDir': downloadedRuntimeDir.path,\n },\n );\n return downloadedRuntimeDir;\n }\n await AppSettings.clearOpenClawActiveRuntimeSpec();\n throw OpenClawRuntimeException(\n '已下载的 OpenClaw runtime 不完整,已回退到内置版本,请重新启动。',\n );\n }\n\n final bundledRuntimeDir = await _bundledRuntimeDir(spec, platformKey);\n if (bundledRuntimeDir != null) {\n _diag(\n 'bundle.launch.bundled-runtime',\n data: {\n 'platformKey': platformKey,\n 'runtimeDir': bundledRuntimeDir.path,\n },\n );\n return bundledRuntimeDir;\n }\n\n if (Platform.isMacOS) {\n throw OpenClawRuntimeException(\n '当前安装包没有可执行的 $platformKey OpenClaw runtime。请先运行 scripts/build_openclaw_desktop_bundle.sh $platformKey,再通过 flutter run 启动。',\n );\n }\n\n final runtimeDir = await _runtimeDir(spec, platformKey);\n final marker = File(p.join(runtimeDir.path, '.bundle_ready'));\n final nodePath = File(p.join(runtimeDir.path, spec.nodeExecutable));\n final cliPath = File(p.join(runtimeDir.path, spec.cliEntrypoint));\n\n if (await marker.exists() &&\n await nodePath.exists() &&\n await cliPath.exists()) {\n await _repairRuntimeLaunchMetadata(\n runtimeDir: runtimeDir,\n nodePath: nodePath,\n platformKey: platformKey,\n cached: true,\n );\n _diag(\n 'bundle.prepare.cached',\n data: {\n 'platformKey': platformKey,\n 'runtimeDir': runtimeDir.path,\n 'marker': marker.path,\n },\n );\n return runtimeDir;\n }\n\n final legacyRuntimeDir = await _legacyRuntimeDir(spec, platformKey);\n if (legacyRuntimeDir.path != runtimeDir.path &&\n await File(\n p.join(legacyRuntimeDir.path, spec.nodeExecutable),\n ).exists() &&\n await File(\n p.join(legacyRuntimeDir.path, spec.cliEntrypoint),\n ).exists()) {\n _diag(\n 'bundle.prepare.migrate-legacy',\n data: {\n 'platformKey': platformKey,\n 'from': legacyRuntimeDir.path,\n 'to': runtimeDir.path,\n },\n );\n if (await runtimeDir.exists()) {\n await runtimeDir.delete(recursive: true);\n }\n await _copyRuntimeDirectory(legacyRuntimeDir, runtimeDir);\n await _repairRuntimeLaunchMetadata(\n runtimeDir: runtimeDir,\n nodePath: nodePath,\n platformKey: platformKey,\n cached: false,\n );\n await marker.writeAsString(DateTime.now().toIso8601String());\n _diag(\n 'bundle.prepare.migrate-complete',\n data: {\n 'platformKey': platformKey,\n 'runtimeDir': runtimeDir.path,\n 'nodeExists': await nodePath.exists(),\n 'cliExists': await cliPath.exists(),\n },\n );\n return runtimeDir;\n }\n\n final prefix = 'assets/openclaw/$platformKey/';\n final assets = await _listAssets(prefix);\n final hasNodeAsset = assets.contains('$prefix${spec.nodeExecutable}');\n final hasCliAsset = assets.contains('$prefix${spec.cliEntrypoint}');\n _diag(\n 'bundle.prepare.assets',\n data: {\n 'platformKey': platformKey,\n 'prefix': prefix,\n 'assetCount': assets.length,\n 'hasNodeAsset': hasNodeAsset,\n 'hasCliAsset': hasCliAsset,\n 'nodeAsset': '$prefix${spec.nodeExecutable}',\n 'cliAsset': '$prefix${spec.cliEntrypoint}',\n 'sample': assets.take(8).toList(),\n },\n );\n if (!hasNodeAsset || !hasCliAsset) {\n throw OpenClawRuntimeException(\n '当前安装包没有内置 $platformKey 的 OpenClaw runtime。请在 release 构建中运行 scripts/build_openclaw_desktop_bundle.sh 后再打包。',\n );\n }\n\n if (await runtimeDir.exists()) {\n _diag('bundle.prepare.remove-old', data: {'runtimeDir': runtimeDir.path});\n await runtimeDir.delete(recursive: true);\n }\n await runtimeDir.create(recursive: true);\n\n for (final asset in assets) {\n final relative = asset.substring(prefix.length);\n if (relative.isEmpty || relative.endsWith('/')) continue;\n final bytes = await rootBundle.load(asset);\n final file = File(p.join(runtimeDir.path, relative));\n await file.parent.create(recursive: true);\n await file.writeAsBytes(\n bytes.buffer.asUint8List(bytes.offsetInBytes, bytes.lengthInBytes),\n flush: false,\n );\n }\n\n await _repairRuntimeLaunchMetadata(\n runtimeDir: runtimeDir,\n nodePath: nodePath,\n platformKey: platformKey,\n cached: false,\n );\n await marker.writeAsString(DateTime.now().toIso8601String());\n _diag(\n 'bundle.prepare.complete',\n data: {\n 'platformKey': platformKey,\n 'runtimeDir': runtimeDir.path,\n 'assetCount': assets.length,\n 'nodeExists': await nodePath.exists(),\n 'nodeSize': await nodePath.exists() ? await nodePath.length() : 0,\n 'cliExists': await cliPath.exists(),\n 'cliSize': await cliPath.exists() ? await cliPath.length() : 0,\n },\n );\n return runtimeDir;\n }\n\n Future<void> _copyRuntimeDirectory(\n Directory source,\n Directory destination,\n ) async {\n await destination.create(recursive: true);\n await for (final entity in source.list(\n recursive: true,\n followLinks: false,\n )) {\n final relative = p.relative(entity.path, from: source.path);\n if (relative == '.') continue;\n final target = p.join(destination.path, relative);\n if (entity is Directory) {\n await Directory(target).create(recursive: true);\n } else if (entity is File) {\n final targetFile = File(target);\n await targetFile.parent.create(recursive: true);\n await entity.openRead().pipe(targetFile.openWrite());\n }\n }\n }\n\n Future<void> _moveOrCopyRuntimeDirectory(\n Directory source,\n Directory destination,\n ) async {\n if (await destination.exists()) {\n await destination.delete(recursive: true);\n }\n await destination.parent.create(recursive: true);\n try {\n await source.rename(destination.path);\n return;\n } on FileSystemException {\n await _copyRuntimeDirectory(source, destination);\n }\n }\n\n Future<void> _repairRuntimeLaunchMetadata({\n required Directory runtimeDir,\n required File nodePath,\n required String platformKey,\n required bool cached,\n }) async {\n if (Platform.isWindows) return;\n\n final chmodResult = await Process.run('/bin/chmod', ['+x', nodePath.path]);\n ProcessResult? xattrResult;\n if (Platform.isMacOS) {\n // Files created by a quarantined app can inherit the quarantine xattr.\n // macOS then rejects Process.start with \"Operation not permitted\".\n xattrResult = await Process.run('/usr/bin/xattr', [\n '-dr',\n 'com.apple.quarantine',\n runtimeDir.path,\n ]);\n await Process.run('/usr/bin/xattr', [\n '-dr',\n 'com.apple.provenance',\n runtimeDir.path,\n ]);\n }\n\n _diag(\n 'bundle.prepare.launch-metadata',\n data: {\n 'platformKey': platformKey,\n 'runtimeDir': runtimeDir.path,\n 'nodePath': nodePath.path,\n 'cached': cached,\n 'chmodExitCode': chmodResult.exitCode,\n if ((chmodResult.stderr as Object).toString().isNotEmpty)\n 'chmodStderr': _previewProcessOutput(chmodResult.stderr),\n if (xattrResult != null) 'xattrExitCode': xattrResult.exitCode,\n if (xattrResult != null &&\n (xattrResult.stderr as Object).toString().isNotEmpty)\n 'xattrStderr': _previewProcessOutput(xattrResult.stderr),\n },\n );\n }\n\n Future<bool> _hasRequiredBundleAssets(\n String platformKey,\n _OpenClawBundleSpec spec,\n ) async {\n final prefix = 'assets/openclaw/$platformKey/';\n final assets = await _listAssets(prefix);\n final hasRequired =\n assets.contains('$prefix${spec.nodeExecutable}') &&\n assets.contains('$prefix${spec.cliEntrypoint}');\n _diag(\n 'bundle.has-required-assets',\n data: {\n 'platformKey': platformKey,\n 'assetCount': assets.length,\n 'hasRequired': hasRequired,\n },\n );\n return hasRequired;\n }\n\n Future<List<String>> _listAssets(String prefix) async {\n final assets = <String>{};\n var jsonCount = 0;\n var binCount = 0;\n var indexCount = 0;\n final failures = <String>[];\n\n try {\n final raw = await rootBundle.loadString('AssetManifest.json');\n final decoded = jsonDecode(raw) as Map<String, dynamic>;\n final items = decoded.keys\n .where((key) => key.startsWith(prefix))\n .toList();\n jsonCount = items.length;\n assets.addAll(items);\n } catch (error) {\n failures.add('AssetManifest.json: $error');\n // Newer Flutter release builds may only include AssetManifest.bin.\n }\n\n try {\n final manifest = await AssetManifest.loadFromAssetBundle(rootBundle);\n final items = manifest\n .listAssets()\n .where((key) => key.startsWith(prefix))\n .toList();\n binCount = items.length;\n assets.addAll(items);\n } catch (error) {\n failures.add('AssetManifest.bin: $error');\n // App Store archives patched after flutter build carry a JSON/index file.\n }\n\n try {\n final raw = await rootBundle.loadString(\n 'assets/openclaw/asset_index.json',\n );\n final decoded = jsonDecode(raw);\n final indexedAssets = decoded is Map\n ? decoded['assets']\n : decoded is List\n ? decoded\n : const [];\n if (indexedAssets is List) {\n final items = indexedAssets\n .map((item) => item.toString())\n .where((key) => key.startsWith(prefix))\n .toList();\n indexCount = items.length;\n assets.addAll(items);\n }\n } catch (error) {\n failures.add('asset_index.json: $error');\n // Older builds do not have an OpenClaw asset index.\n }\n\n final sortedAssets = assets.toList()..sort();\n _diag(\n 'asset.list',\n data: {\n 'prefix': prefix,\n 'total': sortedAssets.length,\n 'jsonCount': jsonCount,\n 'binCount': binCount,\n 'indexCount': indexCount,\n if (failures.isNotEmpty) 'failures': failures,\n 'sample': sortedAssets.take(8).toList(),\n },\n );\n return sortedAssets;\n }\n\n Future<bool> _probe(\n int port,\n String token, {\n String context = 'probe',\n }) async {\n try {\n final healthResponse = await http\n .get(\n Uri.parse('http://127.0.0.1:$port/health'),\n headers: {'Authorization': 'Bearer $token'},\n )\n .timeout(_probeTimeout);\n final healthPayload = _decodeJsonOrNull(healthResponse.body);\n if (healthResponse.statusCode < 200 ||\n healthResponse.statusCode >= 300 ||\n !_looksLikeHealthPayload(healthPayload)) {\n _diag(\n 'probe.health-unhealthy',\n data: {\n 'context': context,\n 'port': port,\n 'statusCode': healthResponse.statusCode,\n 'contentType': healthResponse.headers['content-type'],\n 'bodyPreview': _previewBody(healthResponse.body),\n },\n );\n return false;\n }\n\n final modelsResponse = await http\n .get(\n Uri.parse('http://127.0.0.1:$port/v1/models'),\n headers: {'Authorization': 'Bearer $token'},\n )\n .timeout(_probeTimeout);\n final modelsPayload = _decodeJsonOrNull(modelsResponse.body);\n if (modelsResponse.statusCode < 200 ||\n modelsResponse.statusCode >= 300 ||\n !_looksLikeModelsPayload(modelsPayload)) {\n _diag(\n 'probe.models-unhealthy',\n data: {\n 'context': context,\n 'port': port,\n 'statusCode': modelsResponse.statusCode,\n 'contentType': modelsResponse.headers['content-type'],\n 'bodyPreview': _previewBody(modelsResponse.body),\n },\n );\n return false;\n }\n\n // `/v1/models` can be healthy while `/v1/chat/completions` is disabled\n // by an old config. Use GET so the probe only checks route wiring and\n // never starts a real model request.\n final chatResponse = await http\n .get(\n Uri.parse('http://127.0.0.1:$port/v1/chat/completions'),\n headers: {'Authorization': 'Bearer $token'},\n )\n .timeout(_probeTimeout);\n final healthy =\n chatResponse.statusCode < 500 &&\n chatResponse.statusCode != 401 &&\n chatResponse.statusCode != 403 &&\n chatResponse.statusCode != 404;\n _diag(\n healthy ? 'probe.healthy' : 'probe.chat-unhealthy',\n data: {\n 'context': context,\n 'port': port,\n 'chatStatusCode': chatResponse.statusCode,\n },\n );\n return healthy;\n } catch (error) {\n if (context != 'startup') {\n _diag(\n 'probe.error',\n data: {'context': context, 'port': port},\n error: error,\n );\n }\n return false;\n }\n }\n\n Future<File> _ensureConfigFile({\n required Directory runtimeDir,\n required Directory stateRoot,\n required int port,\n required String token,\n required String backendDeepSeekModel,\n required String deepSeekProxyBaseUrl,\n required String remoteGatewayUrl,\n List<String> extraPluginLoadPaths = const [],\n }) async {\n final configDir = Directory(p.join(stateRoot.path, 'config'));\n await configDir.create(recursive: true);\n final workspace = Directory(p.join(stateRoot.path, 'workspace'));\n await workspace.create(recursive: true);\n final configPath = File(p.join(configDir.path, 'openclaw.json'));\n final bundledPluginLoadPaths = await _bundledPluginLoadPaths(runtimeDir);\n final pluginLoadPaths = _mergeStringLists(\n extraPluginLoadPaths,\n bundledPluginLoadPaths,\n );\n final hasWeChatPlugin = pluginLoadPaths.any(\n (path) => p.basename(path) == _weChatPluginId,\n );\n final remoteUrl = remoteGatewayUrl.trim();\n\n final config = _buildEmbeddedConfig(\n stateRoot: stateRoot,\n port: port,\n token: token,\n backendDeepSeekModel: backendDeepSeekModel,\n deepSeekProxyBaseUrl: deepSeekProxyBaseUrl,\n remoteGatewayUrl: remoteUrl,\n pluginLoadPaths: pluginLoadPaths,\n hasWeChatPlugin: hasWeChatPlugin,\n );\n\n // Repair older embedded configs in-place. OpenClaw 2026.6 requires\n // gateway.mode=local, and the home screen requires the OpenAI-compatible\n // chat endpoint. Preserve unrelated user edits while fixing the fields\n // needed for the app-owned embedded gateway to start reliably.\n try {\n final mcpConfigPath = File(\n p.join(\n Platform.environment['HOME'] ?? '',\n '.gemini',\n 'config',\n 'mcp_config.json',\n ),\n );\n if (await mcpConfigPath.exists()) {\n final mcpConfig = jsonDecode(await mcpConfigPath.readAsString());\n if (mcpConfig is Map && mcpConfig.containsKey('mcpServers')) {\n config['mcpServers'] = mcpConfig['mcpServers'];\n }\n }\n } catch (e) {\n _diag('mcp.config-error', data: {'error': e.toString()});\n }\n\n final merged = await _mergeEmbeddedConfig(configPath, config);\n await configPath.writeAsString(\n const JsonEncoder.withIndent(' ').convert(merged),\n );\n _diag(\n 'config.written',\n data: {\n 'configPath': configPath.path,\n 'port': port,\n 'gatewayMode': _mutableMap(merged['gateway'])['mode'],\n 'backendDeepSeekModel': backendDeepSeekModel,\n 'deepSeekProxyBaseUrl': deepSeekProxyBaseUrl,\n 'remoteGatewayUrlSet': remoteUrl.isNotEmpty,\n 'pluginLoadPaths': pluginLoadPaths,\n 'hasWeChatPlugin': hasWeChatPlugin,\n },\n );\n return configPath;\n }\n\n @visibleForTesting\n Map<String, dynamic> buildEmbeddedConfigForTest({\n required Directory stateRoot,\n required int port,\n required String token,\n required String backendDeepSeekModel,\n required String deepSeekProxyBaseUrl,\n String remoteGatewayUrl = '',\n List<String> pluginLoadPaths = const [],\n bool hasWeChatPlugin = false,\n }) {\n return _buildEmbeddedConfig(\n stateRoot: stateRoot,\n port: port,\n token: token,\n backendDeepSeekModel: backendDeepSeekModel,\n deepSeekProxyBaseUrl: deepSeekProxyBaseUrl,\n remoteGatewayUrl: remoteGatewayUrl,\n pluginLoadPaths: pluginLoadPaths,\n hasWeChatPlugin: hasWeChatPlugin,\n );\n }\n\n Map<String, dynamic> _buildEmbeddedConfig({\n required Directory stateRoot,\n required int port,\n required String token,\n required String backendDeepSeekModel,\n required String deepSeekProxyBaseUrl,\n required String remoteGatewayUrl,\n required List<String> pluginLoadPaths,\n required bool hasWeChatPlugin,\n }) {\n final workspace = p.join(stateRoot.path, 'workspace');\n final remoteUrl = remoteGatewayUrl.trim();\n final desktopToolAllowList = DesktopControlPolicy.supportedTools.toList()\n ..sort();\n final canvasHostConfig = <String, dynamic>{\n 'enabled': true,\n 'root': p.join(stateRoot.path, 'canvas'),\n };\n\n return <String, dynamic>{\n 'gateway': {\n 'mode': 'local',\n 'port': port,\n 'bind': 'loopback',\n 'auth': {'mode': 'token', 'token': token},\n if (remoteUrl.isNotEmpty) 'remote': {'enabled': true, 'url': remoteUrl},\n 'http': {\n 'endpoints': {\n 'chatCompletions': {'enabled': true},\n 'responses': {'enabled': true},\n },\n },\n },\n 'models': {\n 'mode': 'merge',\n 'providers': {\n _backendDeepSeekProviderId: {\n 'baseUrl': deepSeekProxyBaseUrl,\n 'api': 'openai-completions',\n 'apiKey': 'DACHENG_OPENCLAW_PROXY_TOKEN',\n 'authHeader': true,\n 'headers': {'x-dacheng-auth-token': 'DACHENG_AUTH_TOKEN'},\n 'models': [\n {\n 'id': 'deepseek-chat',\n 'name': 'DeepSeek Chat',\n 'contextWindow': 131072,\n 'maxTokens': 8192,\n 'input': ['text'],\n 'compat': {\n 'requiresStringContent': true,\n 'strictMessageKeys': true,\n },\n },\n {\n 'id': 'deepseek-reasoner',\n 'name': 'DeepSeek Reasoner',\n 'contextWindow': 131072,\n 'maxTokens': 8192,\n 'reasoning': true,\n 'input': ['text'],\n 'compat': {\n 'requiresStringContent': true,\n 'strictMessageKeys': true,\n },\n },\n ],\n },\n },\n },\n 'browser': {\n 'enabled': true,\n 'defaultProfile': 'openclaw',\n 'headless': false,\n 'ssrfPolicy': {'dangerouslyAllowPrivateNetwork': true},\n },\n 'tools': {\n 'profile': 'full',\n 'alsoAllow': desktopToolAllowList,\n 'exec': {'host': 'gateway', 'security': 'full', 'ask': 'off'},\n },\n 'plugins': {\n 'enabled': true,\n 'deny': <String>[],\n 'load': {'paths': pluginLoadPaths},\n 'slots': {'memory': 'memory-core'},\n 'entries': <String, dynamic>{\n 'memory-core': {'enabled': true},\n 'bonjour': {'enabled': false},\n 'browser': {'enabled': true},\n 'canvas': {\n 'enabled': true,\n 'config': {'host': canvasHostConfig},\n },\n 'device-pair': {'enabled': true},\n 'file-transfer': {'enabled': true},\n 'phone-control': {'enabled': true},\n 'talk-voice': {'enabled': true},\n _desktopToolsPluginId: {'enabled': true},\n if (hasWeChatPlugin) _weChatPluginId: {'enabled': true},\n },\n },\n 'channels': {\n 'defaults': {'groupPolicy': 'allowlist'},\n if (hasWeChatPlugin)\n _weChatPluginId: {\n 'enabled': true,\n 'dmPolicy': 'pairing',\n 'allowFrom': <String>[],\n 'accounts': <String, dynamic>{},\n },\n },\n 'agents': {\n 'defaults': {\n 'workspace': workspace,\n 'model': {'primary': backendDeepSeekModel},\n },\n 'list': [\n {\n 'id': 'dacheng',\n 'default': true,\n 'workspace': workspace,\n 'model': {'primary': backendDeepSeekModel},\n },\n ],\n },\n };\n }\n\n Future<File> _ensureDesktopToolsManifest({\n required Directory stateRoot,\n required _DesktopToolsLaunch desktopTools,\n }) async {\n final configDir = Directory(p.join(stateRoot.path, 'config'));\n await configDir.create(recursive: true);\n final manifestPath = File(p.join(configDir.path, 'desktop_tools.json'));\n final tools =\n DesktopControlPolicy.supportedTools\n .map(\n (name) => {\n 'name': name,\n 'endpoint': '/v1/tools/execute',\n 'readOnly': DesktopControlPolicy.isReadOnly(name),\n 'requiresConfirmation':\n DesktopControlPolicy.requiresConfirmation(name),\n },\n )\n .toList()\n ..sort(\n (a, b) => a['name'].toString().compareTo(b['name'].toString()),\n );\n\n await manifestPath.writeAsString(\n const JsonEncoder.withIndent(' ').convert({\n 'version': 1,\n 'transport': 'http-loopback',\n 'baseUrl': desktopTools.uri?.toString(),\n 'auth': {'type': 'bearer', 'env': 'DACHENG_DESKTOP_TOOLS_TOKEN'},\n 'status': desktopTools.statusJson,\n 'tools': tools,\n }),\n );\n _diag(\n 'desktop-tools.manifest-written',\n data: {\n 'manifestPath': manifestPath.path,\n 'hasBaseUrl': desktopTools.uri != null,\n },\n );\n return manifestPath;\n }\n\n Future<File> _ensureAgentModelConfig({\n required Directory stateRoot,\n required String deepSeekProxyBaseUrl,\n }) async {\n final agentDir = Directory(\n p.join(stateRoot.path, 'state', 'agents', 'dacheng', 'agent'),\n );\n await agentDir.create(recursive: true);\n final modelsPath = File(p.join(agentDir.path, 'models.json'));\n final providerConfig = <String, dynamic>{\n 'baseUrl': deepSeekProxyBaseUrl,\n 'api': 'openai-completions',\n 'apiKey': 'DACHENG_OPENCLAW_PROXY_TOKEN',\n 'authHeader': true,\n 'headers': {'x-dacheng-auth-token': 'DACHENG_AUTH_TOKEN'},\n 'models': [\n {\n 'id': 'deepseek-chat',\n 'name': 'DeepSeek Chat',\n 'contextWindow': 131072,\n 'maxTokens': 8192,\n 'input': ['text'],\n 'compat': {'requiresStringContent': true, 'strictMessageKeys': true},\n },\n {\n 'id': 'deepseek-reasoner',\n 'name': 'DeepSeek Reasoner',\n 'contextWindow': 131072,\n 'maxTokens': 8192,\n 'reasoning': true,\n 'input': ['text'],\n 'compat': {'requiresStringContent': true, 'strictMessageKeys': true},\n },\n ],\n };\n await modelsPath.writeAsString(\n const JsonEncoder.withIndent(' ').convert({\n 'providers': {_backendDeepSeekProviderId: providerConfig},\n }),\n );\n _diag(\n 'agent-models.written',\n data: {\n 'modelsPath': modelsPath.path,\n 'provider': _backendDeepSeekProviderId,\n 'deepSeekProxyBaseUrl': deepSeekProxyBaseUrl,\n },\n );\n return modelsPath;\n }\n\n Future<_OpenClawCliLaunch> _prepareCliLaunch({\n required bool ensureGateway,\n }) async {\n if (ensureGateway) {\n await ensureStarted();\n }\n final platformKey = _platformKey;\n if (platformKey == null) {\n throw const OpenClawRuntimeException('当前平台不支持内置 OpenClaw Gateway');\n }\n final spec = await _loadSpec(platformKey, checkUpdates: false);\n final runtimeDir = await _prepareBundle(spec, platformKey);\n final stateRoot = await _stateRoot();\n final desktopToolsPluginDir = await _ensureDesktopToolsPlugin(\n stateRoot: stateRoot,\n );\n final port = await AppSettings.getOpenClawGatewayPort(\n defaultValue: spec.defaultPort,\n );\n final token = await AppSettings.getOpenClawGatewayToken();\n final backendDeepSeekModel = _backendDeepSeekModelRef(\n await AppSettings.getOpenClawDeepSeekModel(\n defaultValue: _defaultDeepSeekModelFor(spec.defaultModel),\n ),\n );\n final configPath = await _ensureConfigFile(\n runtimeDir: runtimeDir,\n stateRoot: stateRoot,\n port: port,\n token: token,\n backendDeepSeekModel: backendDeepSeekModel,\n deepSeekProxyBaseUrl: _deepSeekProxyBaseUrl(),\n remoteGatewayUrl: await AppSettings.getOpenClawRemoteGatewayUrl(),\n extraPluginLoadPaths: [desktopToolsPluginDir.path],\n );\n final nodePath = p.join(runtimeDir.path, spec.nodeExecutable);\n final cliPath = p.join(runtimeDir.path, spec.cliEntrypoint);\n if (!await File(nodePath).exists()) {\n throw OpenClawRuntimeException('OpenClaw 内置 Node 不存在: $nodePath');\n }\n if (!await File(cliPath).exists()) {\n throw OpenClawRuntimeException('OpenClaw CLI 入口不存在: $cliPath');\n }\n return _OpenClawCliLaunch(\n spec: spec,\n runtimeDir: runtimeDir,\n stateRoot: stateRoot,\n configPath: configPath,\n nodePath: nodePath,\n cliPath: cliPath,\n port: port,\n token: token,\n );\n }\n\n Map<String, String> _buildOpenClawEnvironment({\n required Directory runtimeDir,\n required Directory stateRoot,\n required File configPath,\n required int port,\n required String token,\n }) {\n final nodeBinDir = Platform.isWindows\n ? p.join(runtimeDir.path, 'node')\n : p.join(runtimeDir.path, 'node', 'bin');\n final pathKey = Platform.isWindows ? 'Path' : 'PATH';\n final currentPath =\n Platform.environment[pathKey] ?? Platform.environment['PATH'] ?? '';\n final separator = Platform.isWindows ? ';' : ':';\n final pathValue = currentPath.isEmpty\n ? nodeBinDir\n : '$nodeBinDir$separator$currentPath';\n final env = Map<String, String>.from(Platform.environment)\n ..remove('NODE_COMPILE_CACHE')\n ..addAll({\n 'OPENCLAW_GATEWAY_PORT': '$port',\n 'OPENCLAW_GATEWAY_BIND': 'loopback',\n 'OPENCLAW_GATEWAY_TOKEN': token,\n 'OPENCLAW_CONFIG_PATH': configPath.path,\n 'OPENCLAW_STATE_DIR': p.join(stateRoot.path, 'state'),\n 'OPENCLAW_AGENT_DIR': p.join(stateRoot.path, 'agents'),\n 'OPENCLAW_WORKSPACE': p.join(stateRoot.path, 'workspace'),\n 'OPENCLAW_DISABLE_BONJOUR': '1',\n 'NODE_DISABLE_COMPILE_CACHE': '1',\n pathKey: pathValue,\n if (pathKey != 'PATH') 'PATH': pathValue,\n });\n return env;\n }\n\n @visibleForTesting\n Map<String, String> buildOpenClawEnvironmentForTest({\n required Directory runtimeDir,\n required Directory stateRoot,\n required File configPath,\n required int port,\n required String token,\n }) {\n return _buildOpenClawEnvironment(\n runtimeDir: runtimeDir,\n stateRoot: stateRoot,\n configPath: configPath,\n port: port,\n token: token,\n );\n }\n\n Future<List<String>> _bundledPluginLoadPaths(Directory runtimeDir) async {\n final paths = <String>[];\n final weChatPluginDir = Directory(\n p.join(runtimeDir.path, 'plugins', _weChatPluginId),\n );\n if (await File(\n p.join(weChatPluginDir.path, 'openclaw.plugin.json'),\n ).exists()) {\n paths.add(weChatPluginDir.path);\n }\n return paths;\n }\n\n Future<Directory> _ensureDesktopToolsPlugin({\n required Directory stateRoot,\n }) async {\n final dir = Directory(\n p.join(stateRoot.path, 'plugins', _desktopToolsPluginId),\n );\n await dir.create(recursive: true);\n final tools = DesktopControlPolicy.supportedTools.toList()..sort();\n await File(p.join(dir.path, 'package.json')).writeAsString(\n const JsonEncoder.withIndent(' ').convert({\n 'name': '@dacheng/openclaw-desktop-tools',\n 'version': '1.0.0',\n 'type': 'module',\n 'openclaw': {\n 'extensions': ['./index.mjs'],\n },\n }),\n );\n await File(p.join(dir.path, 'openclaw.plugin.json')).writeAsString(\n const JsonEncoder.withIndent(' ').convert({\n 'id': _desktopToolsPluginId,\n 'name': 'Dacheng Desktop Tools',\n 'description':\n 'Expose Fabushi desktop and Chrome control bridge tools to embedded OpenClaw.',\n 'activation': {'onStartup': true},\n 'contracts': {'tools': tools},\n 'configSchema': {\n 'type': 'object',\n 'additionalProperties': false,\n 'properties': <String, dynamic>{},\n },\n }),\n );\n await File(\n p.join(dir.path, 'index.mjs'),\n ).writeAsString(_desktopToolsPluginSource(tools));\n _diag(\n 'desktop-tools.plugin-written',\n data: {'pluginDir': dir.path, 'tools': tools},\n );\n return dir;\n }\n\n String _desktopToolsPluginSource(List<String> tools) {\n final toolsJson = jsonEncode(tools);\n return '''\nconst tools = $toolsJson;\n\nconst descriptions = {\n \"desktop.observe\": \"Observe the active desktop application and visible windows.\",\n \"desktop.screenshot\": \"Capture a screenshot of the local desktop.\",\n \"desktop.windows\": \"List visible desktop windows.\",\n \"desktop.click\": \"Click a local desktop coordinate after user confirmation.\",\n \"desktop.type\": \"Type text into the focused local desktop app after user confirmation.\",\n \"desktop.hotkey\": \"Send a keyboard shortcut to the local desktop after user confirmation.\",\n \"desktop.scroll\": \"Scroll the local desktop after user confirmation.\",\n \"chrome.tabs\": \"List tabs from the paired Chrome connector.\",\n \"chrome.navigate\": \"Navigate the active or selected Chrome tab after user confirmation.\",\n \"chrome.dom_snapshot\": \"Read a structured DOM snapshot from the paired Chrome connector.\",\n \"chrome.screenshot\": \"Capture a screenshot from the paired Chrome connector.\",\n \"chrome.click\": \"Click an element or point in the paired Chrome tab after user confirmation.\",\n \"chrome.type\": \"Type into an element in the paired Chrome tab after user confirmation.\"\n};\n\nconst parameterSchemas = {\n \"desktop.click\": {\n type: \"object\",\n additionalProperties: true,\n properties: {\n x: { type: \"number\" },\n y: { type: \"number\" },\n button: { type: \"string\", enum: [\"left\", \"right\"] }\n },\n required: [\"x\", \"y\"]\n },\n \"desktop.type\": {\n type: \"object\",\n additionalProperties: true,\n properties: { text: { type: \"string\" } },\n required: [\"text\"]\n },\n \"desktop.hotkey\": {\n type: \"object\",\n additionalProperties: true,\n properties: {\n key: { type: \"string\" },\n keys: { type: \"array\", items: { type: \"string\" } }\n }\n },\n \"chrome.navigate\": {\n type: \"object\",\n additionalProperties: true,\n properties: { url: { type: \"string\" }, tabId: { type: \"integer\" } },\n required: [\"url\"]\n },\n \"chrome.click\": {\n type: \"object\",\n additionalProperties: true,\n properties: {\n selector: { type: \"string\" },\n x: { type: \"number\" },\n y: { type: \"number\" },\n tabId: { type: \"integer\" }\n }\n },\n \"chrome.type\": {\n type: \"object\",\n additionalProperties: true,\n properties: {\n selector: { type: \"string\" },\n x: { type: \"number\" },\n y: { type: \"number\" },\n text: { type: \"string\" },\n tabId: { type: \"integer\" }\n },\n required: [\"text\"]\n }\n};\n\nconst defaultSchema = {\n type: \"object\",\n additionalProperties: true,\n properties: {}\n};\n\nfunction toolResultText(value) {\n try {\n return JSON.stringify(value, null, 2);\n } catch {\n return String(value);\n }\n}\n\nasync function callBridge(toolName, args) {\n if (process.env.DACHENG_DESKTOP_TOOLS_ENABLED !== \"1\") {\n return {\n isError: true,\n content: [{ type: \"text\", text: \"Fabushi desktop tools bridge is not running.\" }]\n };\n }\n const rawBaseUrl = process.env.DACHENG_DESKTOP_TOOLS_URL || \"\";\n const baseUrl = rawBaseUrl.endsWith(\"/\") ? rawBaseUrl.slice(0, -1) : rawBaseUrl;\n const token = process.env.DACHENG_DESKTOP_TOOLS_TOKEN || \"\";\n if (!baseUrl || !token) {\n return {\n isError: true,\n content: [{ type: \"text\", text: \"Fabushi desktop tools bridge URL/token is missing.\" }]\n };\n }\n const response = await fetch(`\\${baseUrl}/v1/tools/execute`, {\n method: \"POST\",\n headers: {\n \"Authorization\": `Bearer \\${token}`,\n \"Content-Type\": \"application/json\"\n },\n body: JSON.stringify({ tool: toolName, arguments: args || {} })\n });\n const payload = await response.json().catch(() => ({}));\n const ok = response.ok && payload && payload.ok !== false;\n const data = payload && Object.prototype.hasOwnProperty.call(payload, \"data\")\n ? payload.data\n : payload;\n return {\n isError: !ok,\n content: [{ type: \"text\", text: toolResultText(data) }],\n details: data\n };\n}\n\nexport default {\n id: \"dacheng-desktop-tools\",\n name: \"Dacheng Desktop Tools\",\n description: \"Expose Fabushi desktop and Chrome control tools to embedded OpenClaw.\",\n configSchema: { type: \"object\", additionalProperties: false, properties: {} },\n register(api) {\n for (const name of tools) {\n api.registerTool({\n name,\n label: name,\n description: descriptions[name] || `Run \\${name} through the Fabushi desktop bridge.`,\n parameters: parameterSchemas[name] || defaultSchema,\n async execute(_toolCallId, args) {\n return await callBridge(name, args);\n }\n });\n }\n }\n};\n''';\n }\n\n Future<Map<String, dynamic>> _mergeEmbeddedConfig(\n File configPath,\n Map<String, dynamic> defaults,\n ) async {\n Map<String, dynamic> current = <String, dynamic>{};\n if (await configPath.exists()) {\n try {\n final decoded = jsonDecode(await configPath.readAsString());\n if (decoded is Map<String, dynamic>) {\n current = decoded;\n } else if (decoded is Map) {\n current = Map<String, dynamic>.from(decoded);\n }\n } catch (_) {\n current = <String, dynamic>{};\n }\n }\n\n final gateway = _mutableMap(current['gateway']);\n final defaultGateway = _mutableMap(defaults['gateway']);\n gateway['mode'] = 'local';\n gateway['port'] = defaultGateway['port'];\n gateway['bind'] = defaultGateway['bind'];\n gateway['auth'] = Map<String, dynamic>.from(defaultGateway['auth'] as Map);\n if (defaultGateway.containsKey('remote')) {\n gateway['remote'] = {\n ..._mutableMap(gateway['remote']),\n ..._mutableMap(defaultGateway['remote']),\n };\n } else {\n final remote = _mutableMap(gateway['remote'])..remove('url');\n if (remote.isEmpty) {\n gateway.remove('remote');\n } else {\n gateway['remote'] = remote;\n }\n }\n\n final gatewayHttp = _mutableMap(gateway['http']);\n final endpoints = _mutableMap(gatewayHttp['endpoints']);\n endpoints['chatCompletions'] = {'enabled': true};\n endpoints['responses'] = {'enabled': true};\n gatewayHttp['endpoints'] = endpoints;\n gateway['http'] = gatewayHttp;\n current['gateway'] = gateway;\n\n final env = _mutableMap(current['env']);\n env.remove('DEEPSEEK_API_KEY');\n if (env.isEmpty) {\n current.remove('env');\n } else {\n current['env'] = env;\n }\n\n final defaultModels = _mutableMap(defaults['models']);\n final models = _mutableMap(current['models']);\n models['mode'] ??= defaultModels['mode'];\n final providers = _mutableMap(models['providers']);\n final defaultProviders = _mutableMap(defaultModels['providers']);\n providers.remove('deepseek');\n providers.remove('dacheng-deepseek');\n providers.remove(_backendDeepSeekProviderId);\n for (final entry in defaultProviders.entries) {\n providers[entry.key] = entry.value is Map\n ? Map<String, dynamic>.from(entry.value as Map)\n : entry.value;\n }\n models['providers'] = providers;\n current['models'] = models;\n\n final defaultBrowser = _mutableMap(defaults['browser']);\n final browser = _mutableMap(current['browser']);\n browser.addAll(defaultBrowser);\n current['browser'] = browser;\n\n // OpenClaw 2026.6 rejects root-level canvas/canvasHost. Canvas is now\n // configured through plugins.entries.canvas.config.host.\n current.remove('canvas');\n current.remove('canvasHost');\n\n final defaultTools = _mutableMap(defaults['tools']);\n final tools = _mutableMap(current['tools']);\n tools.remove('toolSearch');\n tools.remove('fs');\n final alsoAllow = _mergeStringLists(\n tools['alsoAllow'],\n defaultTools['alsoAllow'],\n );\n final execTools = _mutableMap(tools['exec']);\n execTools.remove('mode');\n if (execTools.isNotEmpty) {\n tools['exec'] = execTools;\n }\n tools.addAll(defaultTools);\n if (alsoAllow.isEmpty) {\n tools.remove('alsoAllow');\n } else {\n tools['alsoAllow'] = alsoAllow;\n }\n current['tools'] = tools;\n\n final defaultPlugins = _mutableMap(defaults['plugins']);\n final plugins = _mutableMap(current['plugins']);\n plugins['enabled'] = true;\n final defaultEntries = _mutableMap(defaultPlugins['entries']);\n final requiredPluginIds = defaultEntries.keys.toSet();\n final allowList = _stringList(plugins['allow']);\n if (allowList.isEmpty) {\n plugins.remove('allow');\n } else {\n plugins['allow'] = _mergeStringLists(allowList, requiredPluginIds);\n }\n final denyList = _stringList(\n plugins['deny'],\n ).where((item) => !requiredPluginIds.contains(item)).toList();\n if (denyList.isEmpty) {\n plugins.remove('deny');\n } else {\n plugins['deny'] = denyList;\n }\n final defaultLoad = _mutableMap(defaultPlugins['load']);\n final load = _mutableMap(plugins['load']);\n load['paths'] = _mergeStringLists(load['paths'], defaultLoad['paths']);\n plugins['load'] = load;\n final slots = _mutableMap(plugins['slots']);\n slots.addAll(_mutableMap(defaultPlugins['slots']));\n plugins['slots'] = slots;\n final entries = _mutableMap(plugins['entries']);\n for (final entry in defaultEntries.entries) {\n final currentEntry = _mutableMap(entries[entry.key]);\n currentEntry.addAll(\n entry.value is Map\n ? Map<String, dynamic>.from(entry.value as Map)\n : {'enabled': true},\n );\n entries[entry.key] = currentEntry;\n }\n plugins['entries'] = entries;\n current['plugins'] = plugins;\n\n final defaultChannels = _mutableMap(defaults['channels']);\n final channels = _mutableMap(current['channels']);\n final defaultChannelDefaults = _mutableMap(defaultChannels['defaults']);\n if (defaultChannelDefaults.isNotEmpty) {\n channels['defaults'] = {\n ..._mutableMap(channels['defaults']),\n ...defaultChannelDefaults,\n };\n }\n for (final entry in defaultChannels.entries) {\n if (entry.key == 'defaults') continue;\n final currentChannel = _mutableMap(channels[entry.key]);\n currentChannel.addAll(\n entry.value is Map\n ? Map<String, dynamic>.from(entry.value as Map)\n : <String, dynamic>{},\n );\n channels[entry.key] = currentChannel;\n }\n current['channels'] = channels;\n\n final agents = _mutableMap(current['agents']);\n final defaultAgents = _mutableMap(defaults['agents']);\n final defaultAgentDefaults = _mutableMap(defaultAgents['defaults']);\n final agentDefaults = _mutableMap(agents['defaults']);\n agentDefaults['workspace'] = defaultAgentDefaults['workspace'];\n agentDefaults['model'] = defaultAgentDefaults['model'];\n agents['defaults'] = agentDefaults;\n\n final defaultList = (defaultAgents['list'] as List? ?? const [])\n .whereType<Map>()\n .map((item) => Map<String, dynamic>.from(item))\n .toList();\n final defaultDacheng = defaultList.isNotEmpty\n ? defaultList.first\n : <String, dynamic>{\n 'id': 'dacheng',\n 'default': true,\n 'workspace': agentDefaults['workspace'],\n 'model': agentDefaults['model'],\n };\n final list = (agents['list'] as List? ?? const [])\n .map((item) => item is Map ? Map<String, dynamic>.from(item) : item)\n .toList();\n var foundDacheng = false;\n for (var i = 0; i < list.length; i++) {\n final item = list[i];\n if (item is! Map<String, dynamic>) continue;\n if (item['id'] != 'dacheng') continue;\n foundDacheng = true;\n item['default'] = true;\n item['workspace'] = defaultDacheng['workspace'];\n item['model'] = defaultDacheng['model'];\n list[i] = item;\n }\n if (!foundDacheng) {\n list.add(defaultDacheng);\n }\n agents['list'] = list.isEmpty ? defaultList : list;\n current['agents'] = agents;\n\n return current;\n }\n\n @visibleForTesting\n Future<Map<String, dynamic>> mergeEmbeddedConfigForTest(\n File configPath,\n Map<String, dynamic> defaults,\n ) {\n return _mergeEmbeddedConfig(configPath, defaults);\n }\n\n String _gatewayChatModel(String model) {\n final trimmed = model.trim();\n if (trimmed == 'openclaw' || trimmed.startsWith('openclaw/')) {\n return trimmed;\n }\n return _defaultGatewayModel;\n }\n\n String _defaultDeepSeekModelFor(String model) {\n final trimmed = model.trim();\n if (trimmed.startsWith('deepseek/')) return trimmed;\n return _defaultDeepSeekModel;\n }\n\n String _backendDeepSeekModelRef(String model) {\n final trimmed = model.trim().isEmpty ? _defaultDeepSeekModel : model.trim();\n final modelId = trimmed.contains('/')\n ? trimmed.split('/').last.trim()\n : trimmed;\n final normalizedModelId = modelId.isEmpty ? 'deepseek-chat' : modelId;\n return '$_backendDeepSeekProviderId/$normalizedModelId';\n }\n\n String _deepSeekProxyBaseUrl() {\n final baseUrl = AppConfig.currentAiBackendUrl.replaceFirst(\n RegExp(r'/+$'),\n '',\n );\n return '$baseUrl/api/openclaw/deepseek/v1';\n }\n\n Map<String, dynamic> _mutableMap(Object? value) {\n if (value is Map<String, dynamic>) return Map<String, dynamic>.from(value);\n if (value is Map) return Map<String, dynamic>.from(value);\n return <String, dynamic>{};\n }\n\n List<String> _stringList(Object? value) {\n if (value is! Iterable) return <String>[];\n return value\n .map((item) => item.toString().trim())\n .where((item) => item.isNotEmpty)\n .toList();\n }\n\n List<String> _mergeStringLists(Object? first, Object? second) {\n final merged = <String>{};\n merged.addAll(_stringList(first));\n merged.addAll(_stringList(second));\n return merged.toList();\n }\n\n void _appendBounded(StringBuffer buffer, String chunk) {\n const maxChars = 120000;\n if (chunk.isEmpty || buffer.length >= maxChars) return;\n final remaining = maxChars - buffer.length;\n buffer.write(\n chunk.length <= remaining ? chunk : chunk.substring(0, remaining),\n );\n }\n\n void _captureLogs(Process process) {\n void addLog(String line) {\n final text = line.trim();\n if (text.isEmpty) return;\n _recentLogs.insert(0, text);\n if (_recentLogs.length > 80) {\n _recentLogs.removeRange(80, _recentLogs.length);\n }\n if (kDebugMode) {\n debugPrint('[OpenClaw] $text');\n }\n _diag('process.output', data: {'line': text});\n }\n\n process.stdout\n .transform(utf8.decoder)\n .transform(const LineSplitter())\n .listen(addLog, onError: (_) {});\n process.stderr\n .transform(utf8.decoder)\n .transform(const LineSplitter())\n .listen(addLog, onError: (_) {});\n }\n\n bool _recentLogsContainRuntimeModuleFailure() {\n return _recentLogs.any((line) {\n final lower = line.toLowerCase();\n return lower.contains('err_vm_module_link_failure') ||\n lower.contains('is not in cache') ||\n lower.contains('request for') && lower.contains('.js');\n });\n }\n\n Future<Directory> _stateRoot() async {\n final support = await getApplicationSupportDirectory();\n final dir = Directory(p.join(support.path, 'openclaw_embedded'));\n await dir.create(recursive: true);\n return dir;\n }\n\n Future<_DesktopToolsLaunch> _ensureDesktopTools() async {\n try {\n final status = await DesktopControlBridge.instance.ensureStarted();\n _diag(\n 'desktop-tools.started',\n data: {\n 'bridgeRunning': status.bridgeRunning,\n 'bridgeUri': status.bridgeUri?.toString(),\n 'message': status.message,\n },\n );\n return _DesktopToolsLaunch(\n uri: status.bridgeUri,\n token: await DesktopControlBridge.instance.bridgeToken,\n statusJson: status.toJson(),\n );\n } catch (error) {\n _diag('desktop-tools.error', error: error);\n return _DesktopToolsLaunch(\n uri: null,\n token: null,\n statusJson: {\n 'enabledByBuild': false,\n 'bridgeRunning': false,\n 'message': error.toString(),\n },\n );\n }\n }\n\n Future<Directory> _runtimeDir(\n _OpenClawBundleSpec spec,\n String platformKey,\n ) async {\n final support = await getApplicationSupportDirectory();\n final runtimeBase = Platform.isMacOS\n ? Directory(\n p.join(\n support.parent.path,\n '${p.basename(support.path)}.openclaw_runtime',\n ),\n )\n : Directory(p.join(support.path, 'openclaw_embedded'));\n return Directory(\n p.join(runtimeBase.path, 'runtime', spec.version, platformKey),\n );\n }\n\n Future<Directory?> _bundledRuntimeDir(\n _OpenClawBundleSpec spec,\n String platformKey,\n ) async {\n if (!Platform.isMacOS) return null;\n\n final contentsDir = Directory(\n p.dirname(p.dirname(Platform.resolvedExecutable)),\n );\n final candidates = <Directory>[\n Directory(\n p.join(\n contentsDir.path,\n 'Frameworks',\n 'App.framework',\n 'Resources',\n 'flutter_assets',\n 'assets',\n 'openclaw',\n platformKey,\n ),\n ),\n Directory(\n p.join(\n contentsDir.path,\n 'Frameworks',\n 'App.framework',\n 'Versions',\n 'A',\n 'Resources',\n 'flutter_assets',\n 'assets',\n 'openclaw',\n platformKey,\n ),\n ),\n Directory(\n p.join(\n contentsDir.path,\n 'Resources',\n 'flutter_assets',\n 'assets',\n 'openclaw',\n platformKey,\n ),\n ),\n ];\n\n for (final dir in candidates) {\n final nodePath = File(p.join(dir.path, spec.nodeExecutable));\n final cliPath = File(p.join(dir.path, spec.cliEntrypoint));\n if (await nodePath.exists() && await cliPath.exists()) {\n return dir;\n }\n }\n _diag(\n 'bundle.macos-bundled-runtime-missing',\n data: {\n 'platformKey': platformKey,\n 'contentsDir': contentsDir.path,\n 'candidates': candidates.map((item) => item.path).toList(),\n },\n );\n return null;\n }\n\n Future<Directory?> _runtimeDirOverride(\n _OpenClawBundleSpec spec,\n String platformKey,\n ) async {\n final raw = _configuredRuntimeDirOverride(platformKey);\n if (raw == null) return null;\n\n final dir = Directory(raw);\n final nodePath = File(p.join(dir.path, spec.nodeExecutable));\n final cliPath = File(p.join(dir.path, spec.cliEntrypoint));\n final nodeExists = await nodePath.exists();\n final cliExists = await cliPath.exists();\n _diag(\n 'bundle.override.paths',\n data: {\n 'platformKey': platformKey,\n 'runtimeDir': dir.path,\n 'nodePath': nodePath.path,\n 'nodeExists': nodeExists,\n 'cliPath': cliPath.path,\n 'cliExists': cliExists,\n },\n );\n\n if (!nodeExists || !cliExists) {\n throw OpenClawRuntimeException(\n 'DACHENG_OPENCLAW_RUNTIME_DIR 指向的 OpenClaw runtime 不完整: ${dir.path}',\n );\n }\n return dir;\n }\n\n String? _configuredRuntimeDirOverride(String platformKey) {\n final raw = _runtimeDirOverrideDefine.trim().isNotEmpty\n ? _runtimeDirOverrideDefine.trim()\n : Platform.environment['DACHENG_OPENCLAW_RUNTIME_DIR']?.trim();\n if (raw == null || raw.isEmpty) return null;\n return p.normalize(raw.replaceAll('{platform}', platformKey));\n }\n\n Future<Directory> _legacyRuntimeDir(\n _OpenClawBundleSpec spec,\n String platformKey,\n ) async {\n final support = await getApplicationSupportDirectory();\n return Directory(\n p.join(\n support.path,\n 'openclaw_embedded',\n 'runtime',\n spec.version,\n platformKey,\n ),\n );\n }\n\n OpenClawRuntimeStatus _remember(OpenClawRuntimeStatus status) {\n _lastStatus = status;\n return status;\n }\n\n String? get _platformKey {\n final abi = Abi.current().toString().toLowerCase();\n if (Platform.isMacOS) {\n return abi.contains('arm64') ? 'macos-arm64' : 'macos-x64';\n }\n if (Platform.isWindows) {\n return abi.contains('arm64') ? 'windows-arm64' : 'windows-x64';\n }\n if (Platform.isLinux) {\n return abi.contains('arm64') ? 'linux-arm64' : 'linux-x64';\n }\n return null;\n }\n\n OpenClawRuntimeStatus? get lastStatus => _lastStatus;\n\n void _diag(\n String message, {\n Map<String, Object?> data = const {},\n Object? error,\n StackTrace? stackTrace,\n }) {\n unawaited(\n DiagnosticLogService.instance.log(\n 'openclaw.runtime',\n message,\n data: data,\n error: error,\n stackTrace: stackTrace,\n ),\n );\n }\n}\n\nint? _readInt(Object? value) {\n if (value is int) return value;\n if (value is num) return value.toInt();\n if (value is String) return int.tryParse(value);\n return null;\n}\n\nObject? _decodeJsonOrNull(String body) {\n try {\n return jsonDecode(body);\n } catch (_) {\n return null;\n }\n}\n\nbool _looksLikeHealthPayload(Object? payload) {\n if (payload is! Map) return false;\n if (payload['ok'] == true) return true;\n final status = payload['status']?.toString().toLowerCase();\n return status == 'live' || status == 'ok' || status == 'healthy';\n}\n\nbool _looksLikeModelsPayload(Object? payload) {\n if (payload is List) return true;\n if (payload is! Map) return false;\n return payload['data'] is List ||\n payload['models'] is List ||\n payload['object'] == 'list';\n}\n\nString _previewBody(String body) {\n final trimmed = body.trim();\n return trimmed.length <= 80 ? trimmed : trimmed.substring(0, 80);\n}\n\nString _previewProcessOutput(Object? output) {\n final text = output?.toString().trim() ?? '';\n return text.length <= 200 ? text : text.substring(0, 200);\n}\n"}, "2662ff6b6da15829552fd211f3d2aba35ca41fc0": {"t": 1785757948.0005429, "n": 97131, "h": "c97f5c4c2a5a2702", "c": "import 'dart:async';\nimport 'dart:convert';\nimport 'dart:ffi' show Abi;\nimport 'dart:io';\nimport 'dart:math' as math;\nimport 'package:archive/archive_io.dart';\nimport 'package:convert/convert.dart';\nimport 'package:crypto/crypto.dart';\nimport 'package:flutter/foundation.dart';\nimport 'package:flutter/services.dart';\nimport 'package:http/http.dart' as http;\nimport 'package:path/path.dart' as p;\nimport 'package:path_provider/path_provider.dart';\n\nimport '../../core/config/app_config.dart';\nimport '../app_settings.dart';\nimport '../diagnostic_log_service.dart';\nimport '../desktop_control/desktop_control_bridge.dart';\nimport '../desktop_control/desktop_control_policy.dart';\nimport 'openclaw_port_resolver.dart';\n\nenum OpenClawRuntimeState {\n unsupported,\n notBundled,\n stopped,\n starting,\n running,\n failed,\n}\n\nclass OpenClawRuntimeStatus {\n final OpenClawRuntimeState state;\n final String message;\n final int? port;\n final String? platformKey;\n final String? runtimePath;\n final Map<String, dynamic>? desktopToolsStatus;\n final DateTime checkedAt;\n\n const OpenClawRuntimeStatus({\n required this.state,\n required this.message,\n this.port,\n this.platformKey,\n this.runtimePath,\n this.desktopToolsStatus,\n required this.checkedAt,\n });\n\n bool get isHealthy => state == OpenClawRuntimeState.running;\n\n String get label {\n switch (state) {\n case OpenClawRuntimeState.unsupported:\n return '不支持';\n case OpenClawRuntimeState.notBundled:\n return '未内置';\n case OpenClawRuntimeState.stopped:\n return '未启动';\n case OpenClawRuntimeState.starting:\n return '启动中';\n case OpenClawRuntimeState.running:\n return '运行中';\n case OpenClawRuntimeState.failed:\n return '异常';\n }\n }\n}\n\nclass OpenClawGatewayTarget {\n final Uri baseUri;\n final String token;\n final String model;\n final String? modelOverride;\n final Uri? desktopToolsUri;\n final String? desktopToolsToken;\n final Map<String, dynamic>? desktopToolsStatus;\n\n const OpenClawGatewayTarget({\n required this.baseUri,\n required this.token,\n required this.model,\n this.modelOverride,\n this.desktopToolsUri,\n this.desktopToolsToken,\n this.desktopToolsStatus,\n });\n}\n\nclass OpenClawCliResult {\n final List<String> args;\n final int exitCode;\n final String stdout;\n final String stderr;\n final bool timedOut;\n\n const OpenClawCliResult({\n required this.args,\n required this.exitCode,\n required this.stdout,\n required this.stderr,\n this.timedOut = false,\n });\n\n bool get succeeded => exitCode == 0 && !timedOut;\n\n String get command => 'openclaw ${args.join(' ')}';\n\n String get combinedOutput {\n final parts = <String>[\n if (stdout.trim().isNotEmpty) stdout.trimRight(),\n if (stderr.trim().isNotEmpty) stderr.trimRight(),\n ];\n return parts.join('\\n');\n }\n\n OpenClawCliResult append(OpenClawCliResult next) {\n return OpenClawCliResult(\n args: [...args, '&&', ...next.args],\n exitCode: next.exitCode,\n stdout: [\n if (stdout.trim().isNotEmpty) stdout.trimRight(),\n if (next.stdout.trim().isNotEmpty) next.stdout.trimRight(),\n ].join('\\n'),\n stderr: [\n if (stderr.trim().isNotEmpty) stderr.trimRight(),\n if (next.stderr.trim().isNotEmpty) next.stderr.trimRight(),\n ].join('\\n'),\n timedOut: timedOut || next.timedOut,\n );\n }\n}\n\nclass _OpenClawBundleSpec {\n final String version;\n final int defaultPort;\n final String defaultModel;\n final String? defaultModelOverride;\n final String nodeExecutable;\n final String cliEntrypoint;\n final List<String> gatewayArgs;\n final bool downloaded;\n\n const _OpenClawBundleSpec({\n required this.version,\n required this.defaultPort,\n required this.defaultModel,\n this.defaultModelOverride,\n required this.nodeExecutable,\n required this.cliEntrypoint,\n required this.gatewayArgs,\n this.downloaded = false,\n });\n\n Map<String, dynamic> toJson() => {\n 'schema': 1,\n 'version': version,\n 'defaultPort': defaultPort,\n 'defaultModel': defaultModel,\n if (defaultModelOverride != null)\n 'defaultModelOverride': defaultModelOverride,\n 'nodeExecutable': nodeExecutable,\n 'cliEntrypoint': cliEntrypoint,\n 'gatewayArgs': gatewayArgs,\n 'downloaded': downloaded,\n };\n}\n\nclass _OpenClawRuntimeRelease {\n final String version;\n final Uri archiveUri;\n final String sha256Hex;\n final int size;\n final int defaultPort;\n final String defaultModel;\n final String? defaultModelOverride;\n final String nodeExecutable;\n final String cliEntrypoint;\n final List<String> gatewayArgs;\n\n const _OpenClawRuntimeRelease({\n required this.version,\n required this.archiveUri,\n required this.sha256Hex,\n required this.size,\n required this.defaultPort,\n required this.defaultModel,\n this.defaultModelOverride,\n required this.nodeExecutable,\n required this.cliEntrypoint,\n required this.gatewayArgs,\n });\n\n _OpenClawBundleSpec toBundleSpec() => _OpenClawBundleSpec(\n version: version,\n defaultPort: defaultPort,\n defaultModel: defaultModel,\n defaultModelOverride: defaultModelOverride,\n nodeExecutable: nodeExecutable,\n cliEntrypoint: cliEntrypoint,\n gatewayArgs: gatewayArgs,\n downloaded: true,\n );\n}\n\nclass _DesktopToolsLaunch {\n final Uri? uri;\n final String? token;\n final Map<String, dynamic> statusJson;\n\n const _DesktopToolsLaunch({\n required this.uri,\n required this.token,\n required this.statusJson,\n });\n}\n\nclass _OpenClawCliLaunch {\n final _OpenClawBundleSpec spec;\n final Directory runtimeDir;\n final Directory stateRoot;\n final File configPath;\n final String nodePath;\n final String cliPath;\n final int port;\n final String token;\n\n const _OpenClawCliLaunch({\n required this.spec,\n required this.runtimeDir,\n required this.stateRoot,\n required this.configPath,\n required this.nodePath,\n required this.cliPath,\n required this.port,\n required this.token,\n });\n}\n\nclass OpenClawRuntimeException implements Exception {\n final String message;\n\n const OpenClawRuntimeException(this.message);\n\n @override\n String toString() => message;\n}\n\nclass OpenClawRuntime {\n OpenClawRuntime._();\n\n static final OpenClawRuntime instance = OpenClawRuntime._();\n\n static const String _manifestAsset = 'assets/openclaw/bundle_manifest.json';\n static const String _runtimeDirOverrideDefine = String.fromEnvironment(\n 'DACHENG_OPENCLAW_RUNTIME_DIR',\n );\n static const String _defaultGatewayModel =\n AppSettings.defaultOpenClawGatewayModel;\n static const String _defaultDeepSeekModel =\n AppSettings.defaultOpenClawDeepSeekModel;\n static const String _backendDeepSeekProviderId = 'dacheng-deepseek-proxy';\n static const String _desktopToolsPluginId = 'dacheng-desktop-tools';\n static const String _weChatPluginId = 'openclaw-weixin';\n static const String _weChatPluginPackage = '@tencent-weixin/openclaw-weixin';\n static const Duration _startupTimeout = Duration(seconds: 120);\n static const Duration _probeTimeout = Duration(seconds: 3);\n static const Duration _cliDefaultTimeout = Duration(seconds: 45);\n\n Process? _process;\n Future<OpenClawGatewayTarget>? _starting;\n String _processAuthToken = '';\n OpenClawRuntimeStatus? _lastStatus;\n final List<String> _recentLogs = <String>[];\n final OpenClawPortResolver _portResolver = const OpenClawPortResolver();\n\n Future<OpenClawRuntimeStatus> getStatus({bool probe = true}) async {\n final platformKey = _platformKey;\n final port = await AppSettings.getOpenClawGatewayPort();\n _diag(\n 'status.start',\n data: {'platformKey': platformKey, 'port': port, 'probe': probe},\n );\n\n if (platformKey == null) {\n return _remember(\n OpenClawRuntimeStatus(\n state: OpenClawRuntimeState.unsupported,\n message: '当前平台不是 macOS / Windows / Linux 桌面端',\n port: port,\n checkedAt: DateTime.now(),\n ),\n );\n }\n\n try {\n final spec = await _loadSpec(platformKey, checkUpdates: false);\n final overrideRuntimeDir = await _runtimeDirOverride(spec, platformKey);\n final bundledRuntimeDir = overrideRuntimeDir == null\n ? await _bundledRuntimeDir(spec, platformKey)\n : null;\n final runtimeDir =\n overrideRuntimeDir ??\n bundledRuntimeDir ??\n await _runtimeDir(spec, platformKey);\n final nodePath = p.join(runtimeDir.path, spec.nodeExecutable);\n final cliPath = p.join(runtimeDir.path, spec.cliEntrypoint);\n final nodeExists = await File(nodePath).exists();\n final cliExists = await File(cliPath).exists();\n _diag(\n 'status.paths',\n data: {\n 'platformKey': platformKey,\n 'runtimeDir': runtimeDir.path,\n 'nodePath': nodePath,\n 'nodeExists': nodeExists,\n 'cliPath': cliPath,\n 'cliExists': cliExists,\n 'runtimeSource': overrideRuntimeDir != null\n ? 'override'\n : (bundledRuntimeDir == null ? 'cache' : 'bundle'),\n },\n );\n\n if (!nodeExists || !cliExists) {\n final hasBundleAssets = await _hasRequiredBundleAssets(\n platformKey,\n spec,\n );\n _diag(\n 'status.bundle-assets',\n data: {\n 'platformKey': platformKey,\n 'hasBundleAssets': hasBundleAssets,\n 'nodeAsset': 'assets/openclaw/$platformKey/${spec.nodeExecutable}',\n 'cliAsset': 'assets/openclaw/$platformKey/${spec.cliEntrypoint}',\n },\n );\n return _remember(\n OpenClawRuntimeStatus(\n state: hasBundleAssets\n ? OpenClawRuntimeState.stopped\n : OpenClawRuntimeState.notBundled,\n message: hasBundleAssets\n ? '内置 OpenClaw 已随 App 打包,尚未释放或启动'\n : '当前 App 包未包含 $platformKey 的 OpenClaw runtime',\n port: port,\n platformKey: platformKey,\n runtimePath: runtimeDir.path,\n checkedAt: DateTime.now(),\n ),\n );\n }\n\n if (probe) {\n final healthy = await _probe(\n port,\n await AppSettings.getOpenClawGatewayToken(),\n );\n if (healthy) {\n return _remember(\n OpenClawRuntimeStatus(\n state: OpenClawRuntimeState.running,\n message: '本机 OpenClaw Gateway 正在运行',\n port: port,\n platformKey: platformKey,\n runtimePath: runtimeDir.path,\n checkedAt: DateTime.now(),\n ),\n );\n }\n }\n\n return _remember(\n OpenClawRuntimeStatus(\n state: _process == null\n ? OpenClawRuntimeState.stopped\n : OpenClawRuntimeState.starting,\n message: _process == null ? '本机 OpenClaw 未启动' : '本机 OpenClaw 正在启动',\n port: port,\n platformKey: platformKey,\n runtimePath: runtimeDir.path,\n checkedAt: DateTime.now(),\n ),\n );\n } catch (error) {\n _diag('status.error', error: error);\n return _remember(\n OpenClawRuntimeStatus(\n state: OpenClawRuntimeState.failed,\n message: error.toString(),\n port: port,\n platformKey: platformKey,\n checkedAt: DateTime.now(),\n ),\n );\n }\n }\n\n Future<OpenClawGatewayTarget> ensureStarted({\n String? authToken,\n String? username,\n bool isMember = false,\n }) {\n _starting ??= _ensureStartedInternal(\n authToken: authToken,\n username: username,\n isMember: isMember,\n ).whenComplete(() => _starting = null);\n return _starting!;\n }\n\n Future<OpenClawRuntimeStatus> restart() async {\n await stop();\n try {\n await ensureStarted();\n return getStatus();\n } catch (error) {\n return _remember(\n OpenClawRuntimeStatus(\n state: OpenClawRuntimeState.failed,\n message: error.toString(),\n port: await AppSettings.getOpenClawGatewayPort(),\n platformKey: _platformKey,\n checkedAt: DateTime.now(),\n ),\n );\n }\n }\n\n Future<void> stop() async {\n final process = _process;\n _process = null;\n _processAuthToken = '';\n if (process == null) return;\n process.kill();\n try {\n await process.exitCode.timeout(const Duration(seconds: 5));\n } catch (_) {\n process.kill();\n }\n }\n\n Future<OpenClawCliResult> createMobilePairingCode({bool remote = true}) {\n return runCli([\n 'qr',\n if (remote) '--remote',\n '--json',\n ], timeout: const Duration(seconds: 30));\n }\n\n Future<OpenClawCliResult> loginWeChat() {\n return runCli([\n 'channels',\n 'login',\n '--channel',\n _weChatPluginId,\n ], timeout: const Duration(minutes: 3));\n }\n\n Future<OpenClawCliResult> inspectChannels() {\n return runCli([\n 'channels',\n 'status',\n '--probe',\n ], timeout: const Duration(seconds: 40));\n }\n\n Future<OpenClawCliResult> installWeChatPlugin() async {\n final install = await runCli(\n ['plugins', 'install', _weChatPluginPackage, '--force'],\n timeout: const Duration(minutes: 3),\n ensureGateway: false,\n );\n if (!install.succeeded) return install;\n final enable = await runCli(\n ['config', 'set', 'plugins.entries.$_weChatPluginId.enabled', 'true'],\n timeout: const Duration(seconds: 30),\n ensureGateway: false,\n );\n return install.append(enable);\n }\n\n Future<OpenClawCliResult> runCli(\n List<String> args, {\n Duration timeout = _cliDefaultTimeout,\n bool ensureGateway = true,\n }) async {\n if (args.isEmpty) {\n throw const OpenClawRuntimeException('OpenClaw CLI 参数不能为空');\n }\n final launch = await _prepareCliLaunch(ensureGateway: ensureGateway);\n final processArgs = <String>[launch.cliPath, ...args];\n _diag(\n 'cli.start',\n data: {\n 'args': args,\n 'runtimeDir': launch.runtimeDir.path,\n 'configPath': launch.configPath.path,\n 'timeoutSeconds': timeout.inSeconds,\n },\n );\n\n final process = await Process.start(\n launch.nodePath,\n processArgs,\n workingDirectory: launch.runtimeDir.path,\n environment: _buildOpenClawEnvironment(\n runtimeDir: launch.runtimeDir,\n stateRoot: launch.stateRoot,\n configPath: launch.configPath,\n port: launch.port,\n token: launch.token,\n ),\n mode: ProcessStartMode.normal,\n runInShell: false,\n );\n final stdoutBuffer = StringBuffer();\n final stderrBuffer = StringBuffer();\n final stdoutSub = process.stdout\n .transform(utf8.decoder)\n .listen((chunk) => _appendBounded(stdoutBuffer, chunk));\n final stderrSub = process.stderr\n .transform(utf8.decoder)\n .listen((chunk) => _appendBounded(stderrBuffer, chunk));\n\n var timedOut = false;\n int exitCode;\n try {\n exitCode = await process.exitCode.timeout(timeout);\n } on TimeoutException {\n timedOut = true;\n process.kill();\n exitCode = await process.exitCode.timeout(\n const Duration(seconds: 5),\n onTimeout: () => -1,\n );\n } finally {\n await stdoutSub.cancel();\n await stderrSub.cancel();\n }\n\n final result = OpenClawCliResult(\n args: args,\n exitCode: exitCode,\n stdout: stdoutBuffer.toString(),\n stderr: stderrBuffer.toString(),\n timedOut: timedOut,\n );\n _diag(\n result.succeeded ? 'cli.complete' : 'cli.failed',\n data: {\n 'args': args,\n 'exitCode': exitCode,\n 'timedOut': timedOut,\n 'stdoutPreview': _previewProcessOutput(result.stdout),\n 'stderrPreview': _previewProcessOutput(result.stderr),\n },\n );\n return result;\n }\n\n Future<OpenClawGatewayTarget> _ensureStartedInternal({\n String? authToken,\n String? username,\n bool isMember = false,\n }) async {\n final platformKey = _platformKey;\n _diag('ensure-start.start', data: {'platformKey': platformKey});\n if (platformKey == null) {\n throw const OpenClawRuntimeException('当前平台不支持内置 OpenClaw Gateway');\n }\n\n final spec = await _loadSpec(platformKey, checkUpdates: true);\n final requestedPort = await AppSettings.getOpenClawGatewayPort(\n defaultValue: spec.defaultPort,\n );\n final token = await AppSettings.getOpenClawGatewayToken();\n final savedModel = await AppSettings.getOpenClawModel(\n defaultValue: spec.defaultModel,\n );\n final model = _gatewayChatModel(savedModel);\n final requestedAuthToken = authToken?.trim() ?? '';\n final backendDeepSeekModel = _backendDeepSeekModelRef(\n await AppSettings.getOpenClawDeepSeekModel(\n defaultValue: _defaultDeepSeekModelFor(spec.defaultModel),\n ),\n );\n final deepSeekProxyBaseUrl = _deepSeekProxyBaseUrl();\n final modelOverride = await AppSettings.getOpenClawModelOverride(\n defaultValue: spec.defaultModelOverride ?? '',\n );\n final remoteGatewayUrl = await AppSettings.getOpenClawRemoteGatewayUrl();\n final desktopTools = await _ensureDesktopTools();\n _diag(\n 'ensure-start.config',\n data: {\n 'platformKey': platformKey,\n 'requestedPort': requestedPort,\n 'model': model,\n 'rawModel': savedModel,\n 'backendDeepSeekModel': backendDeepSeekModel,\n 'deepSeekProxyBaseUrl': deepSeekProxyBaseUrl,\n 'hasAuthToken': requestedAuthToken.isNotEmpty,\n 'modelOverrideSet': modelOverride.trim().isNotEmpty,\n 'remoteGatewayUrlSet': remoteGatewayUrl.trim().isNotEmpty,\n 'desktopToolsUri': desktopTools.uri?.toString(),\n 'desktopToolsStatus': desktopTools.statusJson,\n },\n );\n\n final canReuseStartedProcess =\n _process != null && _processAuthToken == requestedAuthToken;\n if (canReuseStartedProcess &&\n await _probe(requestedPort, token, context: 'pre-start')) {\n _diag('ensure-start.reuse-existing', data: {'port': requestedPort});\n return OpenClawGatewayTarget(\n baseUri: Uri.parse('http://127.0.0.1:$requestedPort'),\n token: token,\n model: model,\n modelOverride: modelOverride.trim().isEmpty\n ? null\n : modelOverride.trim(),\n desktopToolsUri: desktopTools.uri,\n desktopToolsToken: desktopTools.token,\n desktopToolsStatus: desktopTools.statusJson,\n );\n } else if (_process != null) {\n _diag(\n 'ensure-start.restart-auth-changed',\n data: {\n 'hasPreviousAuthToken': _processAuthToken.isNotEmpty,\n 'hasRequestedAuthToken': requestedAuthToken.isNotEmpty,\n },\n );\n await stop();\n }\n\n final runtimeDir = await _prepareBundle(spec, platformKey);\n final stateRoot = await _stateRoot();\n final desktopToolsPluginDir = await _ensureDesktopToolsPlugin(\n stateRoot: stateRoot,\n );\n if (_process != null) {\n _diag(\n 'process.stop-stale-before-start',\n data: {'requestedPort': requestedPort},\n );\n await stop();\n }\n final portCandidate = await _portResolver.resolve(requestedPort);\n final port = portCandidate.port;\n if (portCandidate.isFallback) {\n await AppSettings.setOpenClawGatewayPort(port);\n _diag(\n 'ensure-start.fallback-port',\n data: {\n 'requestedPort': requestedPort,\n 'selectedPort': port,\n if (portCandidate.reason != null) 'reason': portCandidate.reason,\n },\n );\n } else {\n _diag(\n 'ensure-start.port-bindable',\n data: {'requestedPort': requestedPort, 'selectedPort': port},\n );\n }\n final configPath = await _ensureConfigFile(\n runtimeDir: runtimeDir,\n stateRoot: stateRoot,\n port: port,\n token: token,\n backendDeepSeekModel: backendDeepSeekModel,\n deepSeekProxyBaseUrl: deepSeekProxyBaseUrl,\n remoteGatewayUrl: remoteGatewayUrl,\n extraPluginLoadPaths: [desktopToolsPluginDir.path],\n );\n await _ensureAgentModelConfig(\n stateRoot: stateRoot,\n deepSeekProxyBaseUrl: deepSeekProxyBaseUrl,\n );\n final desktopToolsManifestPath = await _ensureDesktopToolsManifest(\n stateRoot: stateRoot,\n desktopTools: desktopTools,\n );\n\n final nodePath = p.join(runtimeDir.path, spec.nodeExecutable);\n final cliPath = p.join(runtimeDir.path, spec.cliEntrypoint);\n if (!await File(nodePath).exists()) {\n _diag('ensure-start.missing-node', data: {'nodePath': nodePath});\n throw OpenClawRuntimeException('OpenClaw 内置 Node 不存在: $nodePath');\n }\n if (!await File(cliPath).exists()) {\n _diag('ensure-start.missing-cli', data: {'cliPath': cliPath});\n throw OpenClawRuntimeException('OpenClaw CLI 入口不存在: $cliPath');\n }\n\n _remember(\n OpenClawRuntimeStatus(\n state: OpenClawRuntimeState.starting,\n message: '正在启动内置 OpenClaw Gateway',\n port: port,\n platformKey: platformKey,\n runtimePath: runtimeDir.path,\n checkedAt: DateTime.now(),\n ),\n );\n\n final args = <String>[\n cliPath,\n ...spec.gatewayArgs.map((arg) => arg.replaceAll('{port}', '$port')),\n ];\n _recentLogs.clear();\n _diag(\n 'process.starting',\n data: {\n 'nodePath': nodePath,\n 'args': args,\n 'workingDirectory': runtimeDir.path,\n 'configPath': configPath.path,\n 'desktopToolsPluginPath': desktopToolsPluginDir.path,\n 'desktopToolsManifestPath': desktopToolsManifestPath.path,\n 'requestedPort': requestedPort,\n 'selectedPort': port,\n },\n );\n\n final env =\n _buildOpenClawEnvironment(\n runtimeDir: runtimeDir,\n stateRoot: stateRoot,\n configPath: configPath,\n port: port,\n token: token,\n )..addAll({\n 'DACHENG_DESKTOP_TOOLS_ENABLED': desktopTools.uri == null ? '0' : '1',\n 'DACHENG_DESKTOP_TOOLS_MANIFEST': desktopToolsManifestPath.path,\n if (desktopTools.uri != null)\n 'DACHENG_DESKTOP_TOOLS_URL': desktopTools.uri.toString(),\n if (desktopTools.token != null)\n 'DACHENG_DESKTOP_TOOLS_TOKEN': desktopTools.token!,\n 'DACHENG_APP_RUNTIME': '1',\n 'DACHENG_AUTH_TOKEN': (authToken != null && authToken.isNotEmpty)\n ? authToken\n : token,\n if (username != null && username.isNotEmpty)\n 'DACHENG_USERNAME': username,\n 'DACHENG_IS_MEMBER': isMember ? '1' : '0',\n 'DACHENG_OPENCLAW_PROXY_TOKEN': 'dacheng-openclaw-proxy',\n });\n\n _process = await Process.start(\n nodePath,\n args,\n workingDirectory: runtimeDir.path,\n environment: env,\n mode: ProcessStartMode.normal,\n runInShell: false,\n );\n _processAuthToken = requestedAuthToken;\n _captureLogs(_process!);\n _diag('process.started', data: {'pid': _process!.pid, 'port': port});\n\n final deadline = DateTime.now().add(_startupTimeout);\n while (DateTime.now().isBefore(deadline)) {\n if (await _probe(port, token, context: 'startup')) {\n _diag('process.ready', data: {'pid': _process?.pid, 'port': port});\n _remember(\n OpenClawRuntimeStatus(\n state: OpenClawRuntimeState.running,\n message: '本机 OpenClaw Gateway 已启动',\n port: port,\n platformKey: platformKey,\n runtimePath: runtimeDir.path,\n desktopToolsStatus: desktopTools.statusJson,\n checkedAt: DateTime.now(),\n ),\n );\n return OpenClawGatewayTarget(\n baseUri: Uri.parse('http://127.0.0.1:$port'),\n token: token,\n model: model,\n modelOverride: modelOverride.trim().isEmpty\n ? null\n : modelOverride.trim(),\n desktopToolsUri: desktopTools.uri,\n desktopToolsToken: desktopTools.token,\n desktopToolsStatus: desktopTools.statusJson,\n );\n }\n\n if (_process != null) {\n final exited = await _process!.exitCode.timeout(\n const Duration(milliseconds: 1),\n onTimeout: () => -999999,\n );\n if (exited != -999999) {\n final logs = _recentLogs.take(12).join('\\n');\n _diag(\n 'process.exited-early',\n data: {\n 'exitCode': exited,\n 'recentLogs': _recentLogs.take(12).toList(),\n },\n );\n throw OpenClawRuntimeException(\n 'OpenClaw Gateway 提前退出,exitCode=$exited${logs.isEmpty ? '' : '\\n$logs'}',\n );\n }\n }\n if (_recentLogsContainRuntimeModuleFailure()) {\n final logs = _recentLogs.take(12).join('\\n');\n _diag(\n 'process.runtime-module-failure',\n data: {'recentLogs': _recentLogs.take(12).toList()},\n );\n await stop();\n throw OpenClawRuntimeException(\n 'OpenClaw runtime 模块缓存异常${logs.isEmpty ? '' : '\\n$logs'}',\n );\n }\n await Future<void>.delayed(const Duration(milliseconds: 450));\n }\n\n final logs = _recentLogs.take(12).join('\\n');\n _diag(\n 'process.startup-timeout',\n data: {\n 'timeoutSeconds': _startupTimeout.inSeconds,\n 'recentLogs': _recentLogs.take(12).toList(),\n },\n );\n throw OpenClawRuntimeException(\n 'OpenClaw Gateway 启动超时${logs.isEmpty ? '' : '\\n$logs'}',\n );\n }\n\n Future<_OpenClawBundleSpec> _loadSpec(\n String platformKey, {\n bool checkUpdates = false,\n }) async {\n final raw = await rootBundle.loadString(_manifestAsset);\n final decoded = jsonDecode(raw) as Map<String, dynamic>;\n final platforms = Map<String, dynamic>.from(\n decoded['platforms'] as Map? ?? const {},\n );\n final platform = Map<String, dynamic>.from(\n platforms[platformKey] as Map? ?? const {},\n );\n if (platform.isEmpty) {\n throw OpenClawRuntimeException('bundle_manifest.json 缺少 $platformKey 配置');\n }\n\n final gatewayArgs =\n (platform['gatewayArgs'] as List? ??\n decoded['gatewayArgs'] as List? ??\n const ['gateway', '--port', '{port}'])\n .map((item) => item.toString())\n .toList();\n\n final spec = _OpenClawBundleSpec(\n version: (decoded['version'] ?? 'dev').toString(),\n defaultPort: _readInt(decoded['defaultPort']) ?? 18789,\n defaultModel: (decoded['defaultModel'] ?? 'openclaw/default').toString(),\n defaultModelOverride: decoded['defaultModelOverride']?.toString(),\n nodeExecutable: (platform['nodeExecutable'] ?? 'node/bin/node')\n .toString(),\n cliEntrypoint: (platform['cliEntrypoint'] ?? 'openclaw/bin/openclaw.js')\n .toString(),\n gatewayArgs: gatewayArgs,\n );\n _diag(\n 'manifest.loaded',\n data: {\n 'platformKey': platformKey,\n 'version': spec.version,\n 'defaultPort': spec.defaultPort,\n 'defaultModel': spec.defaultModel,\n 'nodeExecutable': spec.nodeExecutable,\n 'cliEntrypoint': spec.cliEntrypoint,\n 'gatewayArgs': spec.gatewayArgs,\n },\n );\n final allowDownloadedRuntime = await _allowDownloadedRuntime(platformKey);\n var effectiveSpec = spec;\n if (allowDownloadedRuntime) {\n effectiveSpec =\n await _loadActiveDownloadedSpec(platformKey, fallbackSpec: spec) ??\n spec;\n } else {\n await _clearActiveDownloadedRuntimeSpec(\n platformKey: platformKey,\n reason: 'macos-sandbox',\n );\n }\n if (checkUpdates && allowDownloadedRuntime) {\n effectiveSpec =\n await _maybeInstallRuntimeUpdate(\n platformKey: platformKey,\n currentSpec: effectiveSpec,\n ) ??\n effectiveSpec;\n } else if (checkUpdates) {\n _diag(\n 'runtime-update.skip-disabled',\n data: {'platformKey': platformKey, 'reason': 'macos-sandbox'},\n );\n }\n return effectiveSpec;\n }\n\n Future<bool> _allowDownloadedRuntime(String platformKey) async {\n if (!Platform.isMacOS) return true;\n\n final support = await getApplicationSupportDirectory();\n final sandboxContainerId =\n Platform.environment['APP_SANDBOX_CONTAINER_ID']?.trim() ?? '';\n final pathLooksSandboxed = p\n .split(p.normalize(support.path))\n .contains('Containers');\n final sandboxed = sandboxContainerId.isNotEmpty || pathLooksSandboxed;\n if (!sandboxed) return true;\n\n // App Store/TestFlight sandboxed apps cannot reliably execute downloaded\n // Mach-O payloads. Use the bundled runtime that was signed with inherited\n // sandbox entitlements during packaging.\n _diag(\n 'runtime-update.disabled-macos-sandbox',\n data: {\n 'platformKey': platformKey,\n 'supportPath': support.path,\n 'hasSandboxContainerId': sandboxContainerId.isNotEmpty,\n },\n );\n return false;\n }\n\n Future<void> _clearActiveDownloadedRuntimeSpec({\n required String platformKey,\n required String reason,\n }) async {\n final saved = await AppSettings.getOpenClawActiveRuntimeSpec();\n if (saved == null) return;\n await AppSettings.clearOpenClawActiveRuntimeSpec();\n _diag(\n 'runtime-update.active-cleared',\n data: {\n 'platformKey': platformKey,\n 'reason': reason,\n 'version': saved['version']?.toString(),\n 'downloaded': saved['downloaded'] == true,\n },\n );\n }\n\n Future<_OpenClawBundleSpec?> _loadActiveDownloadedSpec(\n String platformKey, {\n required _OpenClawBundleSpec fallbackSpec,\n }) async {\n final saved = await AppSettings.getOpenClawActiveRuntimeSpec();\n if (saved == null) return null;\n final spec = _specFromJson(saved, downloaded: true);\n if (spec == null) {\n await AppSettings.clearOpenClawActiveRuntimeSpec();\n return null;\n }\n final runtimeDir = await _cachedRuntimeDirIfComplete(spec, platformKey);\n if (runtimeDir == null) {\n await AppSettings.clearOpenClawActiveRuntimeSpec();\n _diag(\n 'runtime-update.active-missing',\n data: {'platformKey': platformKey, 'version': spec.version},\n );\n return null;\n }\n if (_isRuntimeVersionNewer(fallbackSpec.version, spec.version)) {\n await AppSettings.clearOpenClawActiveRuntimeSpec();\n _diag(\n 'runtime-update.active-cleared',\n data: {\n 'platformKey': platformKey,\n 'reason': 'bundled-newer',\n 'version': spec.version,\n 'bundledVersion': fallbackSpec.version,\n },\n );\n return null;\n }\n _diag(\n 'runtime-update.active-loaded',\n data: {\n 'platformKey': platformKey,\n 'version': spec.version,\n 'runtimeDir': runtimeDir.path,\n 'bundledVersion': fallbackSpec.version,\n },\n );\n return spec;\n }\n\n bool _isRuntimeVersionNewer(String candidate, String current) {\n final candidateRank = _runtimeVersionRank(candidate);\n final currentRank = _runtimeVersionRank(current);\n final length = math.max(candidateRank.length, currentRank.length);\n for (var i = 0; i < length; i++) {\n final a = i < candidateRank.length ? candidateRank[i] : 0;\n final b = i < currentRank.length ? currentRank[i] : 0;\n if (a != b) return a > b;\n }\n return false;\n }\n\n List<int> _runtimeVersionRank(String version) {\n return RegExp(r'\\d+')\n .allMatches(version)\n .map((match) => int.tryParse(match.group(0) ?? '') ?? 0)\n .toList(growable: false);\n }\n _OpenClawBundleSpec? _specFromJson(\n Map<String, dynamic> json, {\n required bool downloaded,\n }) {\n final version = json['version']?.toString().trim() ?? '';\n final nodeExecutable = json['nodeExecutable']?.toString().trim() ?? '';\n final cliEntrypoint = json['cliEntrypoint']?.toString().trim() ?? '';\n if (version.isEmpty || nodeExecutable.isEmpty || cliEntrypoint.isEmpty) {\n return null;\n }\n return _OpenClawBundleSpec(\n version: version,\n defaultPort: _readInt(json['defaultPort']) ?? 18789,\n defaultModel: (json['defaultModel'] ?? _defaultGatewayModel).toString(),\n defaultModelOverride: json['defaultModelOverride']?.toString(),\n nodeExecutable: nodeExecutable,\n cliEntrypoint: cliEntrypoint,\n gatewayArgs:\n (json['gatewayArgs'] as List? ??\n const ['gateway', '--port', '{port}', '--force'])\n .map((item) => item.toString())\n .toList(),\n downloaded: downloaded,\n );\n }\n\n Future<_OpenClawBundleSpec?> _maybeInstallRuntimeUpdate({\n required String platformKey,\n required _OpenClawBundleSpec currentSpec,\n }) async {\n if (_configuredRuntimeDirOverride(platformKey) != null) {\n _diag('runtime-update.skip-override', data: {'platformKey': platformKey});\n return null;\n }\n\n try {\n final release = await _fetchRuntimeRelease(\n platformKey: platformKey,\n currentVersion: currentSpec.version,\n );\n if (release == null || release.version == currentSpec.version) {\n return null;\n }\n\n final releaseSpec = release.toBundleSpec();\n final cached = await _cachedRuntimeDirIfComplete(\n releaseSpec,\n platformKey,\n );\n if (cached != null) {\n await AppSettings.setOpenClawActiveRuntimeSpec(releaseSpec.toJson());\n _diag(\n 'runtime-update.use-cached',\n data: {\n 'platformKey': platformKey,\n 'version': release.version,\n 'runtimeDir': cached.path,\n },\n );\n return releaseSpec;\n }\n\n return await _downloadAndInstallRuntimeUpdate(\n release: release,\n platformKey: platformKey,\n );\n } catch (error, stackTrace) {\n _diag(\n 'runtime-update.failed',\n data: {\n 'platformKey': platformKey,\n 'currentVersion': currentSpec.version,\n 'error': error.toString(),\n },\n error: error,\n stackTrace: stackTrace,\n );\n return null;\n }\n }\n\n Future<_OpenClawRuntimeRelease?> _fetchRuntimeRelease({\n required String platformKey,\n required String currentVersion,\n }) async {\n final baseUrl = AppConfig.currentAiBackendUrl.replaceFirst(\n RegExp(r'/+$'),\n '',\n );\n final uri = Uri.parse('$baseUrl/api/openclaw/runtime/manifest').replace(\n queryParameters: {\n 'platform': platformKey,\n 'currentVersion': currentVersion,\n 'appVersion': AppConfig.appVersion,\n },\n );\n final response = await http\n .get(uri, headers: {'Accept': 'application/json'})\n .timeout(const Duration(seconds: 20));\n if (response.statusCode < 200 || response.statusCode >= 300) {\n _diag(\n 'runtime-update.manifest-http',\n data: {'statusCode': response.statusCode, 'uri': uri.toString()},\n );\n return null;\n }\n final decoded = _decodeJsonOrNull(response.body);\n if (decoded is! Map) return null;\n final payload = Map<String, dynamic>.from(decoded);\n if (payload['updateAvailable'] != true) {\n _diag(\n 'runtime-update.none',\n data: {\n 'platformKey': platformKey,\n 'reason': payload['reason']?.toString(),\n 'currentVersion': currentVersion,\n 'latestVersion': payload['latestVersion']?.toString(),\n },\n );\n return null;\n }\n final latest = payload['latest'];\n if (latest is! Map) return null;\n return _releaseFromJson(Map<String, dynamic>.from(latest));\n }\n\n _OpenClawRuntimeRelease? _releaseFromJson(Map<String, dynamic> json) {\n final version = json['version']?.toString().trim() ?? '';\n final archiveUrl = json['archiveUrl']?.toString().trim() ?? '';\n final sha256Hex = json['sha256']?.toString().trim().toLowerCase() ?? '';\n final archiveUri = Uri.tryParse(archiveUrl);\n final gatewayArgs =\n (json['gatewayArgs'] as List? ??\n const ['gateway', '--port', '{port}', '--force'])\n .map((item) => item.toString())\n .toList();\n if (version.isEmpty ||\n archiveUri == null ||\n !archiveUri.hasScheme ||\n !RegExp(r'^[a-f0-9]{64}$').hasMatch(sha256Hex)) {\n _diag(\n 'runtime-update.invalid-release',\n data: {\n 'version': version,\n 'archiveUrlSet': archiveUrl.isNotEmpty,\n 'sha256Length': sha256Hex.length,\n },\n );\n return null;\n }\n return _OpenClawRuntimeRelease(\n version: version,\n archiveUri: archiveUri,\n sha256Hex: sha256Hex,\n size: _readInt(json['size']) ?? 0,\n defaultPort: _readInt(json['defaultPort']) ?? 18789,\n defaultModel: (json['defaultModel'] ?? _defaultGatewayModel).toString(),\n defaultModelOverride: json['defaultModelOverride']?.toString(),\n nodeExecutable: (json['nodeExecutable'] ?? 'node/bin/node').toString(),\n cliEntrypoint: (json['cliEntrypoint'] ?? 'openclaw/openclaw.mjs')\n .toString(),\n gatewayArgs: gatewayArgs,\n );\n }\n\n Future<_OpenClawBundleSpec> _downloadAndInstallRuntimeUpdate({\n required _OpenClawRuntimeRelease release,\n required String platformKey,\n }) async {\n final spec = release.toBundleSpec();\n final runtimeDir = await _runtimeDir(spec, platformKey);\n final support = await getApplicationSupportDirectory();\n final downloadDir = Directory(p.join(support.path, 'openclaw_downloads'));\n await downloadDir.create(recursive: true);\n final archivePath = p.join(\n downloadDir.path,\n '${_safePathPart(release.version)}-$platformKey${_archiveExtension(release.archiveUri)}',\n );\n final archiveFile = File(archivePath);\n final digest = await _downloadRuntimeArchive(release, archiveFile);\n if (digest != release.sha256Hex) {\n await _deleteFileIfExists(archiveFile);\n throw OpenClawRuntimeException(\n 'OpenClaw runtime 校验失败: expected=${release.sha256Hex}, actual=$digest',\n );\n }\n\n final extractDir = Directory(\n p.join(\n downloadDir.path,\n '${_safePathPart(release.version)}-$platformKey-extract',\n ),\n );\n final stagingDir = Directory(\n p.join(\n downloadDir.path,\n '${_safePathPart(release.version)}-$platformKey-stage',\n ),\n );\n if (await extractDir.exists()) await extractDir.delete(recursive: true);\n if (await stagingDir.exists()) await stagingDir.delete(recursive: true);\n await extractDir.create(recursive: true);\n await _extractRuntimeArchive(archiveFile, extractDir);\n\n final extractedRoot = await _findExtractedRuntimeRoot(\n extractDir: extractDir,\n spec: spec,\n );\n if (extractedRoot == null) {\n throw OpenClawRuntimeException('下载的 OpenClaw runtime 不包含必要的 node/cli 文件');\n }\n\n await runtimeDir.parent.create(recursive: true);\n if (await runtimeDir.exists()) await runtimeDir.delete(recursive: true);\n await _moveOrCopyRuntimeDirectory(extractedRoot, stagingDir);\n await stagingDir.rename(runtimeDir.path);\n final marker = File(p.join(runtimeDir.path, '.bundle_ready'));\n await marker.writeAsString(\n const JsonEncoder.withIndent(' ').convert({\n 'installedAt': DateTime.now().toIso8601String(),\n 'version': release.version,\n 'platform': platformKey,\n 'sha256': digest,\n 'source': release.archiveUri.toString(),\n }),\n );\n await _repairRuntimeLaunchMetadata(\n runtimeDir: runtimeDir,\n nodePath: File(p.join(runtimeDir.path, spec.nodeExecutable)),\n platformKey: platformKey,\n cached: false,\n );\n await AppSettings.setOpenClawActiveRuntimeSpec(spec.toJson());\n await _deleteDirectoryIfExists(extractDir);\n await _deleteFileIfExists(archiveFile);\n _diag(\n 'runtime-update.installed',\n data: {\n 'platformKey': platformKey,\n 'version': release.version,\n 'runtimeDir': runtimeDir.path,\n 'size': release.size,\n },\n );\n return spec;\n }\n\n Future<String> _downloadRuntimeArchive(\n _OpenClawRuntimeRelease release,\n File destination,\n ) async {\n final client = http.Client();\n IOSink? output;\n try {\n final request = http.Request('GET', release.archiveUri)\n ..headers['Accept'] = 'application/octet-stream';\n final response = await client\n .send(request)\n .timeout(const Duration(seconds: 20));\n if (response.statusCode < 200 || response.statusCode >= 300) {\n throw OpenClawRuntimeException(\n 'OpenClaw runtime 下载失败 (${response.statusCode})',\n );\n }\n await destination.parent.create(recursive: true);\n output = destination.openWrite();\n final digestSink = AccumulatorSink<Digest>();\n final hashSink = sha256.startChunkedConversion(digestSink);\n var bytes = 0;\n await for (final chunk in response.stream.timeout(\n const Duration(minutes: 5),\n )) {\n bytes += chunk.length;\n hashSink.add(chunk);\n output.add(chunk);\n }\n await output.flush();\n await output.close();\n output = null;\n hashSink.close();\n final digest = digestSink.events.single.toString();\n _diag(\n 'runtime-update.downloaded',\n data: {\n 'archiveUri': release.archiveUri.toString(),\n 'bytes': bytes,\n 'sha256': digest,\n },\n );\n return digest;\n } finally {\n await output?.close().catchError((_) {});\n client.close();\n }\n }\n\n Future<void> _extractRuntimeArchive(\n File archiveFile,\n Directory destination,\n ) async {\n final lowerPath = archiveFile.path.toLowerCase();\n if (Platform.isMacOS && lowerPath.endsWith('.zip')) {\n try {\n final result = await Process.run('/usr/bin/ditto', [\n '-x',\n '-k',\n archiveFile.path,\n destination.path,\n ]).timeout(const Duration(minutes: 5));\n _diag(\n 'runtime-update.extract-native',\n data: {\n 'archivePath': archiveFile.path,\n 'destination': destination.path,\n 'exitCode': result.exitCode,\n if ((result.stderr as Object).toString().isNotEmpty)\n 'stderr': _previewProcessOutput(result.stderr),\n },\n );\n if (result.exitCode == 0) return;\n } catch (error, stackTrace) {\n _diag(\n 'runtime-update.extract-native-failed',\n data: {\n 'archivePath': archiveFile.path,\n 'destination': destination.path,\n 'error': error.toString(),\n },\n error: error,\n stackTrace: stackTrace,\n );\n }\n }\n\n await extractFileToDisk(archiveFile.path, destination.path);\n _diag(\n 'runtime-update.extract-dart',\n data: {'archivePath': archiveFile.path, 'destination': destination.path},\n );\n }\n\n Future<Directory?> _findExtractedRuntimeRoot({\n required Directory extractDir,\n required _OpenClawBundleSpec spec,\n }) async {\n final direct = await _runtimeRootIfComplete(extractDir, spec);\n if (direct != null) return direct;\n await for (final entity in extractDir.list(followLinks: false)) {\n if (entity is! Directory) continue;\n final nested = await _runtimeRootIfComplete(entity, spec);\n if (nested != null) return nested;\n }\n return null;\n }\n\n Future<Directory?> _runtimeRootIfComplete(\n Directory dir,\n _OpenClawBundleSpec spec,\n ) async {\n final nodePath = File(p.join(dir.path, spec.nodeExecutable));\n final cliPath = File(p.join(dir.path, spec.cliEntrypoint));\n if (await nodePath.exists() && await cliPath.exists()) return dir;\n return null;\n }\n\n Future<Directory?> _cachedRuntimeDirIfComplete(\n _OpenClawBundleSpec spec,\n String platformKey,\n ) async {\n final runtimeDir = await _runtimeDir(spec, platformKey);\n final marker = File(p.join(runtimeDir.path, '.bundle_ready'));\n final nodePath = File(p.join(runtimeDir.path, spec.nodeExecutable));\n final cliPath = File(p.join(runtimeDir.path, spec.cliEntrypoint));\n if (await marker.exists() &&\n await nodePath.exists() &&\n await cliPath.exists()) {\n return runtimeDir;\n }\n return null;\n }\n\n String _safePathPart(String value) {\n final safe = value.replaceAll(RegExp(r'[^A-Za-z0-9_.-]+'), '-');\n return safe.isEmpty ? 'runtime' : safe;\n }\n\n String _archiveExtension(Uri uri) {\n final lower = uri.path.toLowerCase();\n if (lower.endsWith('.tar.gz')) return '.tar.gz';\n if (lower.endsWith('.tgz')) return '.tgz';\n if (lower.endsWith('.tar')) return '.tar';\n return '.zip';\n }\n\n Future<void> _deleteFileIfExists(File file) async {\n try {\n if (await file.exists()) await file.delete();\n } catch (_) {}\n }\n\n Future<void> _deleteDirectoryIfExists(Directory dir) async {\n try {\n if (await dir.exists()) await dir.delete(recursive: true);\n } catch (_) {}\n }\n\n Future<Directory> _prepareBundle(\n _OpenClawBundleSpec spec,\n String platformKey,\n ) async {\n final overrideRuntimeDir = await _runtimeDirOverride(spec, platformKey);\n if (overrideRuntimeDir != null) {\n final nodePath = File(\n p.join(overrideRuntimeDir.path, spec.nodeExecutable),\n );\n await _repairRuntimeLaunchMetadata(\n runtimeDir: overrideRuntimeDir,\n nodePath: nodePath,\n platformKey: platformKey,\n cached: true,\n );\n _diag(\n 'bundle.launch.override-runtime',\n data: {\n 'platformKey': platformKey,\n 'runtimeDir': overrideRuntimeDir.path,\n },\n );\n return overrideRuntimeDir;\n }\n\n if (spec.downloaded) {\n final downloadedRuntimeDir = await _cachedRuntimeDirIfComplete(\n spec,\n platformKey,\n );\n if (downloadedRuntimeDir != null) {\n await _repairRuntimeLaunchMetadata(\n runtimeDir: downloadedRuntimeDir,\n nodePath: File(\n p.join(downloadedRuntimeDir.path, spec.nodeExecutable),\n ),\n platformKey: platformKey,\n cached: true,\n );\n _diag(\n 'bundle.launch.downloaded-runtime',\n data: {\n 'platformKey': platformKey,\n 'version': spec.version,\n 'runtimeDir': downloadedRuntimeDir.path,\n },\n );\n return downloadedRuntimeDir;\n }\n await AppSettings.clearOpenClawActiveRuntimeSpec();\n throw OpenClawRuntimeException(\n '已下载的 OpenClaw runtime 不完整,已回退到内置版本,请重新启动。',\n );\n }\n\n final bundledRuntimeDir = await _bundledRuntimeDir(spec, platformKey);\n if (bundledRuntimeDir != null) {\n _diag(\n 'bundle.launch.bundled-runtime',\n data: {\n 'platformKey': platformKey,\n 'runtimeDir': bundledRuntimeDir.path,\n },\n );\n return bundledRuntimeDir;\n }\n\n if (Platform.isMacOS) {\n throw OpenClawRuntimeException(\n '当前安装包没有可执行的 $platformKey OpenClaw runtime。请先运行 scripts/build_openclaw_desktop_bundle.sh $platformKey,再通过 flutter run 启动。',\n );\n }\n\n final runtimeDir = await _runtimeDir(spec, platformKey);\n final marker = File(p.join(runtimeDir.path, '.bundle_ready'));\n final nodePath = File(p.join(runtimeDir.path, spec.nodeExecutable));\n final cliPath = File(p.join(runtimeDir.path, spec.cliEntrypoint));\n\n if (await marker.exists() &&\n await nodePath.exists() &&\n await cliPath.exists()) {\n await _repairRuntimeLaunchMetadata(\n runtimeDir: runtimeDir,\n nodePath: nodePath,\n platformKey: platformKey,\n cached: true,\n );\n _diag(\n 'bundle.prepare.cached',\n data: {\n 'platformKey': platformKey,\n 'runtimeDir': runtimeDir.path,\n 'marker': marker.path,\n },\n );\n return runtimeDir;\n }\n\n final legacyRuntimeDir = await _legacyRuntimeDir(spec, platformKey);\n if (legacyRuntimeDir.path != runtimeDir.path &&\n await File(\n p.join(legacyRuntimeDir.path, spec.nodeExecutable),\n ).exists() &&\n await File(\n p.join(legacyRuntimeDir.path, spec.cliEntrypoint),\n ).exists()) {\n _diag(\n 'bundle.prepare.migrate-legacy',\n data: {\n 'platformKey': platformKey,\n 'from': legacyRuntimeDir.path,\n 'to': runtimeDir.path,\n },\n );\n if (await runtimeDir.exists()) {\n await runtimeDir.delete(recursive: true);\n }\n await _copyRuntimeDirectory(legacyRuntimeDir, runtimeDir);\n await _repairRuntimeLaunchMetadata(\n runtimeDir: runtimeDir,\n nodePath: nodePath,\n platformKey: platformKey,\n cached: false,\n );\n await marker.writeAsString(DateTime.now().toIso8601String());\n _diag(\n 'bundle.prepare.migrate-complete',\n data: {\n 'platformKey': platformKey,\n 'runtimeDir': runtimeDir.path,\n 'nodeExists': await nodePath.exists(),\n 'cliExists': await cliPath.exists(),\n },\n );\n return runtimeDir;\n }\n\n final prefix = 'assets/openclaw/$platformKey/';\n final assets = await _listAssets(prefix);\n final hasNodeAsset = assets.contains('$prefix${spec.nodeExecutable}');\n final hasCliAsset = assets.contains('$prefix${spec.cliEntrypoint}');\n _diag(\n 'bundle.prepare.assets',\n data: {\n 'platformKey': platformKey,\n 'prefix': prefix,\n 'assetCount': assets.length,\n 'hasNodeAsset': hasNodeAsset,\n 'hasCliAsset': hasCliAsset,\n 'nodeAsset': '$prefix${spec.nodeExecutable}',\n 'cliAsset': '$prefix${spec.cliEntrypoint}',\n 'sample': assets.take(8).toList(),\n },\n );\n if (!hasNodeAsset || !hasCliAsset) {\n throw OpenClawRuntimeException(\n '当前安装包没有内置 $platformKey 的 OpenClaw runtime。请在 release 构建中运行 scripts/build_openclaw_desktop_bundle.sh 后再打包。',\n );\n }\n\n if (await runtimeDir.exists()) {\n _diag('bundle.prepare.remove-old', data: {'runtimeDir': runtimeDir.path});\n await runtimeDir.delete(recursive: true);\n }\n await runtimeDir.create(recursive: true);\n\n for (final asset in assets) {\n final relative = asset.substring(prefix.length);\n if (relative.isEmpty || relative.endsWith('/')) continue;\n final bytes = await rootBundle.load(asset);\n final file = File(p.join(runtimeDir.path, relative));\n await file.parent.create(recursive: true);\n await file.writeAsBytes(\n bytes.buffer.asUint8List(bytes.offsetInBytes, bytes.lengthInBytes),\n flush: false,\n );\n }\n\n await _repairRuntimeLaunchMetadata(\n runtimeDir: runtimeDir,\n nodePath: nodePath,\n platformKey: platformKey,\n cached: false,\n );\n await marker.writeAsString(DateTime.now().toIso8601String());\n _diag(\n 'bundle.prepare.complete',\n data: {\n 'platformKey': platformKey,\n 'runtimeDir': runtimeDir.path,\n 'assetCount': assets.length,\n 'nodeExists': await nodePath.exists(),\n 'nodeSize': await nodePath.exists() ? await nodePath.length() : 0,\n 'cliExists': await cliPath.exists(),\n 'cliSize': await cliPath.exists() ? await cliPath.length() : 0,\n },\n );\n return runtimeDir;\n }\n\n Future<void> _copyRuntimeDirectory(\n Directory source,\n Directory destination,\n ) async {\n await destination.create(recursive: true);\n await for (final entity in source.list(\n recursive: true,\n followLinks: false,\n )) {\n final relative = p.relative(entity.path, from: source.path);\n if (relative == '.') continue;\n final target = p.join(destination.path, relative);\n if (entity is Directory) {\n await Directory(target).create(recursive: true);\n } else if (entity is File) {\n final targetFile = File(target);\n await targetFile.parent.create(recursive: true);\n await entity.openRead().pipe(targetFile.openWrite());\n }\n }\n }\n\n Future<void> _moveOrCopyRuntimeDirectory(\n Directory source,\n Directory destination,\n ) async {\n if (await destination.exists()) {\n await destination.delete(recursive: true);\n }\n await destination.parent.create(recursive: true);\n try {\n await source.rename(destination.path);\n return;\n } on FileSystemException {\n await _copyRuntimeDirectory(source, destination);\n }\n }\n\n Future<void> _repairRuntimeLaunchMetadata({\n required Directory runtimeDir,\n required File nodePath,\n required String platformKey,\n required bool cached,\n }) async {\n if (Platform.isWindows) return;\n\n final chmodResult = await Process.run('/bin/chmod', ['+x', nodePath.path]);\n ProcessResult? xattrResult;\n if (Platform.isMacOS) {\n // Files created by a quarantined app can inherit the quarantine xattr.\n // macOS then rejects Process.start with \"Operation not permitted\".\n xattrResult = await Process.run('/usr/bin/xattr', [\n '-dr',\n 'com.apple.quarantine',\n runtimeDir.path,\n ]);\n await Process.run('/usr/bin/xattr', [\n '-dr',\n 'com.apple.provenance',\n runtimeDir.path,\n ]);\n }\n\n _diag(\n 'bundle.prepare.launch-metadata',\n data: {\n 'platformKey': platformKey,\n 'runtimeDir': runtimeDir.path,\n 'nodePath': nodePath.path,\n 'cached': cached,\n 'chmodExitCode': chmodResult.exitCode,\n if ((chmodResult.stderr as Object).toString().isNotEmpty)\n 'chmodStderr': _previewProcessOutput(chmodResult.stderr),\n if (xattrResult != null) 'xattrExitCode': xattrResult.exitCode,\n if (xattrResult != null &&\n (xattrResult.stderr as Object).toString().isNotEmpty)\n 'xattrStderr': _previewProcessOutput(xattrResult.stderr),\n },\n );\n }\n\n Future<bool> _hasRequiredBundleAssets(\n String platformKey,\n _OpenClawBundleSpec spec,\n ) async {\n final prefix = 'assets/openclaw/$platformKey/';\n final assets = await _listAssets(prefix);\n final hasRequired =\n assets.contains('$prefix${spec.nodeExecutable}') &&\n assets.contains('$prefix${spec.cliEntrypoint}');\n _diag(\n 'bundle.has-required-assets',\n data: {\n 'platformKey': platformKey,\n 'assetCount': assets.length,\n 'hasRequired': hasRequired,\n },\n );\n return hasRequired;\n }\n\n Future<List<String>> _listAssets(String prefix) async {\n final assets = <String>{};\n var jsonCount = 0;\n var binCount = 0;\n var indexCount = 0;\n final failures = <String>[];\n\n try {\n final raw = await rootBundle.loadString('AssetManifest.json');\n final decoded = jsonDecode(raw) as Map<String, dynamic>;\n final items = decoded.keys\n .where((key) => key.startsWith(prefix))\n .toList();\n jsonCount = items.length;\n assets.addAll(items);\n } catch (error) {\n failures.add('AssetManifest.json: $error');\n // Newer Flutter release builds may only include AssetManifest.bin.\n }\n\n try {\n final manifest = await AssetManifest.loadFromAssetBundle(rootBundle);\n final items = manifest\n .listAssets()\n .where((key) => key.startsWith(prefix))\n .toList();\n binCount = items.length;\n assets.addAll(items);\n } catch (error) {\n failures.add('AssetManifest.bin: $error');\n // App Store archives patched after flutter build carry a JSON/index file.\n }\n\n try {\n final raw = await rootBundle.loadString(\n 'assets/openclaw/asset_index.json',\n );\n final decoded = jsonDecode(raw);\n final indexedAssets = decoded is Map\n ? decoded['assets']\n : decoded is List\n ? decoded\n : const [];\n if (indexedAssets is List) {\n final items = indexedAssets\n .map((item) => item.toString())\n .where((key) => key.startsWith(prefix))\n .toList();\n indexCount = items.length;\n assets.addAll(items);\n }\n } catch (error) {\n failures.add('asset_index.json: $error');\n // Older builds do not have an OpenClaw asset index.\n }\n\n final sortedAssets = assets.toList()..sort();\n _diag(\n 'asset.list',\n data: {\n 'prefix': prefix,\n 'total': sortedAssets.length,\n 'jsonCount': jsonCount,\n 'binCount': binCount,\n 'indexCount': indexCount,\n if (failures.isNotEmpty) 'failures': failures,\n 'sample': sortedAssets.take(8).toList(),\n },\n );\n return sortedAssets;\n }\n\n Future<bool> _probe(\n int port,\n String token, {\n String context = 'probe',\n }) async {\n try {\n final healthResponse = await http\n .get(\n Uri.parse('http://127.0.0.1:$port/health'),\n headers: {'Authorization': 'Bearer $token'},\n )\n .timeout(_probeTimeout);\n final healthPayload = _decodeJsonOrNull(healthResponse.body);\n if (healthResponse.statusCode < 200 ||\n healthResponse.statusCode >= 300 ||\n !_looksLikeHealthPayload(healthPayload)) {\n _diag(\n 'probe.health-unhealthy',\n data: {\n 'context': context,\n 'port': port,\n 'statusCode': healthResponse.statusCode,\n 'contentType': healthResponse.headers['content-type'],\n 'bodyPreview': _previewBody(healthResponse.body),\n },\n );\n return false;\n }\n\n final modelsResponse = await http\n .get(\n Uri.parse('http://127.0.0.1:$port/v1/models'),\n headers: {'Authorization': 'Bearer $token'},\n )\n .timeout(_probeTimeout);\n final modelsPayload = _decodeJsonOrNull(modelsResponse.body);\n if (modelsResponse.statusCode < 200 ||\n modelsResponse.statusCode >= 300 ||\n !_looksLikeModelsPayload(modelsPayload)) {\n _diag(\n 'probe.models-unhealthy',\n data: {\n 'context': context,\n 'port': port,\n 'statusCode': modelsResponse.statusCode,\n 'contentType': modelsResponse.headers['content-type'],\n 'bodyPreview': _previewBody(modelsResponse.body),\n },\n );\n return false;\n }\n\n // `/v1/models` can be healthy while `/v1/chat/completions` is disabled\n // by an old config. Use GET so the probe only checks route wiring and\n // never starts a real model request.\n final chatResponse = await http\n .get(\n Uri.parse('http://127.0.0.1:$port/v1/chat/completions'),\n headers: {'Authorization': 'Bearer $token'},\n )\n .timeout(_probeTimeout);\n final healthy =\n chatResponse.statusCode < 500 &&\n chatResponse.statusCode != 401 &&\n chatResponse.statusCode != 403 &&\n chatResponse.statusCode != 404;\n _diag(\n healthy ? 'probe.healthy' : 'probe.chat-unhealthy',\n data: {\n 'context': context,\n 'port': port,\n 'chatStatusCode': chatResponse.statusCode,\n },\n );\n return healthy;\n } catch (error) {\n if (context != 'startup') {\n _diag(\n 'probe.error',\n data: {'context': context, 'port': port},\n error: error,\n );\n }\n return false;\n }\n }\n\n Future<File> _ensureConfigFile({\n required Directory runtimeDir,\n required Directory stateRoot,\n required int port,\n required String token,\n required String backendDeepSeekModel,\n required String deepSeekProxyBaseUrl,\n required String remoteGatewayUrl,\n List<String> extraPluginLoadPaths = const [],\n }) async {\n final configDir = Directory(p.join(stateRoot.path, 'config'));\n await configDir.create(recursive: true);\n final workspace = Directory(p.join(stateRoot.path, 'workspace'));\n await workspace.create(recursive: true);\n final configPath = File(p.join(configDir.path, 'openclaw.json'));\n final bundledPluginLoadPaths = await _bundledPluginLoadPaths(runtimeDir);\n final pluginLoadPaths = _mergeStringLists(\n extraPluginLoadPaths,\n bundledPluginLoadPaths,\n );\n final hasWeChatPlugin = pluginLoadPaths.any(\n (path) => p.basename(path) == _weChatPluginId,\n );\n final remoteUrl = remoteGatewayUrl.trim();\n\n final config = _buildEmbeddedConfig(\n stateRoot: stateRoot,\n port: port,\n token: token,\n backendDeepSeekModel: backendDeepSeekModel,\n deepSeekProxyBaseUrl: deepSeekProxyBaseUrl,\n remoteGatewayUrl: remoteUrl,\n pluginLoadPaths: pluginLoadPaths,\n hasWeChatPlugin: hasWeChatPlugin,\n );\n\n // Repair older embedded configs in-place. OpenClaw 2026.6 requires\n // gateway.mode=local, and the home screen requires the OpenAI-compatible\n // chat endpoint. Preserve unrelated user edits while fixing the fields\n // needed for the app-owned embedded gateway to start reliably.\n try {\n final mcpConfigPath = File(\n p.join(\n Platform.environment['HOME'] ?? '',\n '.gemini',\n 'config',\n 'mcp_config.json',\n ),\n );\n if (await mcpConfigPath.exists()) {\n final mcpConfig = jsonDecode(await mcpConfigPath.readAsString());\n if (mcpConfig is Map && mcpConfig.containsKey('mcpServers')) {\n config['mcpServers'] = mcpConfig['mcpServers'];\n }\n }\n } catch (e) {\n _diag('mcp.config-error', data: {'error': e.toString()});\n }\n\n final merged = await _mergeEmbeddedConfig(configPath, config);\n await configPath.writeAsString(\n const JsonEncoder.withIndent(' ').convert(merged),\n );\n _diag(\n 'config.written',\n data: {\n 'configPath': configPath.path,\n 'port': port,\n 'gatewayMode': _mutableMap(merged['gateway'])['mode'],\n 'backendDeepSeekModel': backendDeepSeekModel,\n 'deepSeekProxyBaseUrl': deepSeekProxyBaseUrl,\n 'remoteGatewayUrlSet': remoteUrl.isNotEmpty,\n 'pluginLoadPaths': pluginLoadPaths,\n 'hasWeChatPlugin': hasWeChatPlugin,\n },\n );\n return configPath;\n }\n\n @visibleForTesting\n Map<String, dynamic> buildEmbeddedConfigForTest({\n required Directory stateRoot,\n required int port,\n required String token,\n required String backendDeepSeekModel,\n required String deepSeekProxyBaseUrl,\n String remoteGatewayUrl = '',\n List<String> pluginLoadPaths = const [],\n bool hasWeChatPlugin = false,\n }) {\n return _buildEmbeddedConfig(\n stateRoot: stateRoot,\n port: port,\n token: token,\n backendDeepSeekModel: backendDeepSeekModel,\n deepSeekProxyBaseUrl: deepSeekProxyBaseUrl,\n remoteGatewayUrl: remoteGatewayUrl,\n pluginLoadPaths: pluginLoadPaths,\n hasWeChatPlugin: hasWeChatPlugin,\n );\n }\n\n Map<String, dynamic> _buildEmbeddedConfig({\n required Directory stateRoot,\n required int port,\n required String token,\n required String backendDeepSeekModel,\n required String deepSeekProxyBaseUrl,\n required String remoteGatewayUrl,\n required List<String> pluginLoadPaths,\n required bool hasWeChatPlugin,\n }) {\n final workspace = p.join(stateRoot.path, 'workspace');\n final remoteUrl = remoteGatewayUrl.trim();\n final desktopToolAllowList = DesktopControlPolicy.supportedTools.toList()\n ..sort();\n final canvasHostConfig = <String, dynamic>{\n 'enabled': true,\n 'root': p.join(stateRoot.path, 'canvas'),\n };\n\n return <String, dynamic>{\n 'gateway': {\n 'mode': 'local',\n 'port': port,\n 'bind': 'loopback',\n 'auth': {'mode': 'token', 'token': token},\n if (remoteUrl.isNotEmpty) 'remote': {'enabled': true, 'url': remoteUrl},\n 'http': {\n 'endpoints': {\n 'chatCompletions': {'enabled': true},\n 'responses': {'enabled': true},\n },\n },\n },\n 'models': {\n 'mode': 'merge',\n 'providers': {\n _backendDeepSeekProviderId: {\n 'baseUrl': deepSeekProxyBaseUrl,\n 'api': 'openai-completions',\n 'apiKey': 'DACHENG_OPENCLAW_PROXY_TOKEN',\n 'authHeader': true,\n 'headers': {'x-dacheng-auth-token': 'DACHENG_AUTH_TOKEN'},\n 'models': [\n {\n 'id': 'deepseek-chat',\n 'name': 'DeepSeek Chat',\n 'contextWindow': 131072,\n 'maxTokens': 8192,\n 'input': ['text'],\n 'compat': {\n 'requiresStringContent': true,\n 'strictMessageKeys': true,\n },\n },\n {\n 'id': 'deepseek-reasoner',\n 'name': 'DeepSeek Reasoner',\n 'contextWindow': 131072,\n 'maxTokens': 8192,\n 'reasoning': true,\n 'input': ['text'],\n 'compat': {\n 'requiresStringContent': true,\n 'strictMessageKeys': true,\n },\n },\n ],\n },\n },\n },\n 'browser': {\n 'enabled': true,\n 'defaultProfile': 'openclaw',\n 'headless': false,\n 'ssrfPolicy': {'dangerouslyAllowPrivateNetwork': true},\n },\n 'tools': {\n 'profile': 'full',\n 'alsoAllow': desktopToolAllowList,\n 'exec': {'host': 'gateway', 'security': 'full', 'ask': 'off'},\n },\n 'plugins': {\n 'enabled': true,\n 'deny': <String>[],\n 'load': {'paths': pluginLoadPaths},\n 'slots': {'memory': 'memory-core'},\n 'entries': <String, dynamic>{\n 'memory-core': {'enabled': true},\n 'bonjour': {'enabled': false},\n 'browser': {'enabled': true},\n 'canvas': {\n 'enabled': true,\n 'config': {'host': canvasHostConfig},\n },\n 'device-pair': {'enabled': true},\n 'file-transfer': {'enabled': true},\n 'phone-control': {'enabled': true},\n 'talk-voice': {'enabled': true},\n _desktopToolsPluginId: {'enabled': true},\n if (hasWeChatPlugin) _weChatPluginId: {'enabled': true},\n },\n },\n 'channels': {\n 'defaults': {'groupPolicy': 'allowlist'},\n if (hasWeChatPlugin)\n _weChatPluginId: {\n 'enabled': true,\n 'dmPolicy': 'pairing',\n 'allowFrom': <String>[],\n 'accounts': <String, dynamic>{},\n },\n },\n 'agents': {\n 'defaults': {\n 'workspace': workspace,\n 'model': {'primary': backendDeepSeekModel},\n },\n 'list': [\n {\n 'id': 'dacheng',\n 'default': true,\n 'workspace': workspace,\n 'model': {'primary': backendDeepSeekModel},\n },\n ],\n },\n };\n }\n\n Future<File> _ensureDesktopToolsManifest({\n required Directory stateRoot,\n required _DesktopToolsLaunch desktopTools,\n }) async {\n final configDir = Directory(p.join(stateRoot.path, 'config'));\n await configDir.create(recursive: true);\n final manifestPath = File(p.join(configDir.path, 'desktop_tools.json'));\n final tools =\n DesktopControlPolicy.supportedTools\n .map(\n (name) => {\n 'name': name,\n 'endpoint': '/v1/tools/execute',\n 'readOnly': DesktopControlPolicy.isReadOnly(name),\n 'requiresConfirmation':\n DesktopControlPolicy.requiresConfirmation(name),\n },\n )\n .toList()\n ..sort(\n (a, b) => a['name'].toString().compareTo(b['name'].toString()),\n );\n\n await manifestPath.writeAsString(\n const JsonEncoder.withIndent(' ').convert({\n 'version': 1,\n 'transport': 'http-loopback',\n 'baseUrl': desktopTools.uri?.toString(),\n 'auth': {'type': 'bearer', 'env': 'DACHENG_DESKTOP_TOOLS_TOKEN'},\n 'status': desktopTools.statusJson,\n 'tools': tools,\n }),\n );\n _diag(\n 'desktop-tools.manifest-written',\n data: {\n 'manifestPath': manifestPath.path,\n 'hasBaseUrl': desktopTools.uri != null,\n },\n );\n return manifestPath;\n }\n\n Future<File> _ensureAgentModelConfig({\n required Directory stateRoot,\n required String deepSeekProxyBaseUrl,\n }) async {\n final agentDir = Directory(\n p.join(stateRoot.path, 'state', 'agents', 'dacheng', 'agent'),\n );\n await agentDir.create(recursive: true);\n final modelsPath = File(p.join(agentDir.path, 'models.json'));\n final providerConfig = <String, dynamic>{\n 'baseUrl': deepSeekProxyBaseUrl,\n 'api': 'openai-completions',\n 'apiKey': 'DACHENG_OPENCLAW_PROXY_TOKEN',\n 'authHeader': true,\n 'headers': {'x-dacheng-auth-token': 'DACHENG_AUTH_TOKEN'},\n 'models': [\n {\n 'id': 'deepseek-chat',\n 'name': 'DeepSeek Chat',\n 'contextWindow': 131072,\n 'maxTokens': 8192,\n 'input': ['text'],\n 'compat': {'requiresStringContent': true, 'strictMessageKeys': true},\n },\n {\n 'id': 'deepseek-reasoner',\n 'name': 'DeepSeek Reasoner',\n 'contextWindow': 131072,\n 'maxTokens': 8192,\n 'reasoning': true,\n 'input': ['text'],\n 'compat': {'requiresStringContent': true, 'strictMessageKeys': true},\n },\n ],\n };\n await modelsPath.writeAsString(\n const JsonEncoder.withIndent(' ').convert({\n 'providers': {_backendDeepSeekProviderId: providerConfig},\n }),\n );\n _diag(\n 'agent-models.written',\n data: {\n 'modelsPath': modelsPath.path,\n 'provider': _backendDeepSeekProviderId,\n 'deepSeekProxyBaseUrl': deepSeekProxyBaseUrl,\n },\n );\n return modelsPath;\n }\n\n Future<_OpenClawCliLaunch> _prepareCliLaunch({\n required bool ensureGateway,\n }) async {\n if (ensureGateway) {\n await ensureStarted();\n }\n final platformKey = _platformKey;\n if (platformKey == null) {\n throw const OpenClawRuntimeException('当前平台不支持内置 OpenClaw Gateway');\n }\n final spec = await _loadSpec(platformKey, checkUpdates: false);\n final runtimeDir = await _prepareBundle(spec, platformKey);\n final stateRoot = await _stateRoot();\n final desktopToolsPluginDir = await _ensureDesktopToolsPlugin(\n stateRoot: stateRoot,\n );\n final port = await AppSettings.getOpenClawGatewayPort(\n defaultValue: spec.defaultPort,\n );\n final token = await AppSettings.getOpenClawGatewayToken();\n final backendDeepSeekModel = _backendDeepSeekModelRef(\n await AppSettings.getOpenClawDeepSeekModel(\n defaultValue: _defaultDeepSeekModelFor(spec.defaultModel),\n ),\n );\n final configPath = await _ensureConfigFile(\n runtimeDir: runtimeDir,\n stateRoot: stateRoot,\n port: port,\n token: token,\n backendDeepSeekModel: backendDeepSeekModel,\n deepSeekProxyBaseUrl: _deepSeekProxyBaseUrl(),\n remoteGatewayUrl: await AppSettings.getOpenClawRemoteGatewayUrl(),\n extraPluginLoadPaths: [desktopToolsPluginDir.path],\n );\n final nodePath = p.join(runtimeDir.path, spec.nodeExecutable);\n final cliPath = p.join(runtimeDir.path, spec.cliEntrypoint);\n if (!await File(nodePath).exists()) {\n throw OpenClawRuntimeException('OpenClaw 内置 Node 不存在: $nodePath');\n }\n if (!await File(cliPath).exists()) {\n throw OpenClawRuntimeException('OpenClaw CLI 入口不存在: $cliPath');\n }\n return _OpenClawCliLaunch(\n spec: spec,\n runtimeDir: runtimeDir,\n stateRoot: stateRoot,\n configPath: configPath,\n nodePath: nodePath,\n cliPath: cliPath,\n port: port,\n token: token,\n );\n }\n\n Map<String, String> _buildOpenClawEnvironment({\n required Directory runtimeDir,\n required Directory stateRoot,\n required File configPath,\n required int port,\n required String token,\n }) {\n final nodeBinDir = Platform.isWindows\n ? p.join(runtimeDir.path, 'node')\n : p.join(runtimeDir.path, 'node', 'bin');\n final pathKey = Platform.isWindows ? 'Path' : 'PATH';\n final currentPath =\n Platform.environment[pathKey] ?? Platform.environment['PATH'] ?? '';\n final separator = Platform.isWindows ? ';' : ':';\n final pathValue = currentPath.isEmpty\n ? nodeBinDir\n : '$nodeBinDir$separator$currentPath';\n final env = Map<String, String>.from(Platform.environment)\n ..remove('NODE_COMPILE_CACHE')\n ..addAll({\n 'OPENCLAW_GATEWAY_PORT': '$port',\n 'OPENCLAW_GATEWAY_BIND': 'loopback',\n 'OPENCLAW_GATEWAY_TOKEN': token,\n 'OPENCLAW_CONFIG_PATH': configPath.path,\n 'OPENCLAW_STATE_DIR': p.join(stateRoot.path, 'state'),\n 'OPENCLAW_AGENT_DIR': p.join(stateRoot.path, 'agents'),\n 'OPENCLAW_WORKSPACE': p.join(stateRoot.path, 'workspace'),\n 'OPENCLAW_DISABLE_BONJOUR': '1',\n 'NODE_DISABLE_COMPILE_CACHE': '1',\n pathKey: pathValue,\n if (pathKey != 'PATH') 'PATH': pathValue,\n });\n return env;\n }\n\n @visibleForTesting\n Map<String, String> buildOpenClawEnvironmentForTest({\n required Directory runtimeDir,\n required Directory stateRoot,\n required File configPath,\n required int port,\n required String token,\n }) {\n return _buildOpenClawEnvironment(\n runtimeDir: runtimeDir,\n stateRoot: stateRoot,\n configPath: configPath,\n port: port,\n token: token,\n );\n }\n\n Future<List<String>> _bundledPluginLoadPaths(Directory runtimeDir) async {\n final paths = <String>[];\n final weChatPluginDir = Directory(\n p.join(runtimeDir.path, 'plugins', _weChatPluginId),\n );\n if (await File(\n p.join(weChatPluginDir.path, 'openclaw.plugin.json'),\n ).exists()) {\n paths.add(weChatPluginDir.path);\n }\n return paths;\n }\n\n Future<Directory> _ensureDesktopToolsPlugin({\n required Directory stateRoot,\n }) async {\n final dir = Directory(\n p.join(stateRoot.path, 'plugins', _desktopToolsPluginId),\n );\n await dir.create(recursive: true);\n final tools = DesktopControlPolicy.supportedTools.toList()..sort();\n await File(p.join(dir.path, 'package.json')).writeAsString(\n const JsonEncoder.withIndent(' ').convert({\n 'name': '@dacheng/openclaw-desktop-tools',\n 'version': '1.0.0',\n 'type': 'module',\n 'openclaw': {\n 'extensions': ['./index.mjs'],\n },\n }),\n );\n await File(p.join(dir.path, 'openclaw.plugin.json')).writeAsString(\n const JsonEncoder.withIndent(' ').convert({\n 'id': _desktopToolsPluginId,\n 'name': 'Dacheng Desktop Tools',\n 'description':\n 'Expose Fabushi desktop and Chrome control bridge tools to embedded OpenClaw.',\n 'activation': {'onStartup': true},\n 'contracts': {'tools': tools},\n 'configSchema': {\n 'type': 'object',\n 'additionalProperties': false,\n 'properties': <String, dynamic>{},\n },\n }),\n );\n await File(\n p.join(dir.path, 'index.mjs'),\n ).writeAsString(_desktopToolsPluginSource(tools));\n _diag(\n 'desktop-tools.plugin-written',\n data: {'pluginDir': dir.path, 'tools': tools},\n );\n return dir;\n }\n\n String _desktopToolsPluginSource(List<String> tools) {\n final toolsJson = jsonEncode(tools);\n return '''\nconst tools = $toolsJson;\n\nconst descriptions = {\n \"desktop.observe\": \"Observe the active desktop application and visible windows.\",\n \"desktop.screenshot\": \"Capture a screenshot of the local desktop.\",\n \"desktop.windows\": \"List visible desktop windows.\",\n \"desktop.click\": \"Click a local desktop coordinate after user confirmation.\",\n \"desktop.type\": \"Type text into the focused local desktop app after user confirmation.\",\n \"desktop.hotkey\": \"Send a keyboard shortcut to the local desktop after user confirmation.\",\n \"desktop.scroll\": \"Scroll the local desktop after user confirmation.\",\n \"chrome.tabs\": \"List tabs from the paired Chrome connector.\",\n \"chrome.navigate\": \"Navigate the active or selected Chrome tab after user confirmation.\",\n \"chrome.dom_snapshot\": \"Read a structured DOM snapshot from the paired Chrome connector.\",\n \"chrome.screenshot\": \"Capture a screenshot from the paired Chrome connector.\",\n \"chrome.click\": \"Click an element or point in the paired Chrome tab after user confirmation.\",\n \"chrome.type\": \"Type into an element in the paired Chrome tab after user confirmation.\"\n};\n\nconst parameterSchemas = {\n \"desktop.click\": {\n type: \"object\",\n additionalProperties: true,\n properties: {\n x: { type: \"number\" },\n y: { type: \"number\" },\n button: { type: \"string\", enum: [\"left\", \"right\"] }\n },\n required: [\"x\", \"y\"]\n },\n \"desktop.type\": {\n type: \"object\",\n additionalProperties: true,\n properties: { text: { type: \"string\" } },\n required: [\"text\"]\n },\n \"desktop.hotkey\": {\n type: \"object\",\n additionalProperties: true,\n properties: {\n key: { type: \"string\" },\n keys: { type: \"array\", items: { type: \"string\" } }\n }\n },\n \"chrome.navigate\": {\n type: \"object\",\n additionalProperties: true,\n properties: { url: { type: \"string\" }, tabId: { type: \"integer\" } },\n required: [\"url\"]\n },\n \"chrome.click\": {\n type: \"object\",\n additionalProperties: true,\n properties: {\n selector: { type: \"string\" },\n x: { type: \"number\" },\n y: { type: \"number\" },\n tabId: { type: \"integer\" }\n }\n },\n \"chrome.type\": {\n type: \"object\",\n additionalProperties: true,\n properties: {\n selector: { type: \"string\" },\n x: { type: \"number\" },\n y: { type: \"number\" },\n text: { type: \"string\" },\n tabId: { type: \"integer\" }\n },\n required: [\"text\"]\n }\n};\n\nconst defaultSchema = {\n type: \"object\",\n additionalProperties: true,\n properties: {}\n};\n\nfunction toolResultText(value) {\n try {\n return JSON.stringify(value, null, 2);\n } catch {\n return String(value);\n }\n}\n\nasync function callBridge(toolName, args) {\n if (process.env.DACHENG_DESKTOP_TOOLS_ENABLED !== \"1\") {\n return {\n isError: true,\n content: [{ type: \"text\", text: \"Fabushi desktop tools bridge is not running.\" }]\n };\n }\n const rawBaseUrl = process.env.DACHENG_DESKTOP_TOOLS_URL || \"\";\n const baseUrl = rawBaseUrl.endsWith(\"/\") ? rawBaseUrl.slice(0, -1) : rawBaseUrl;\n const token = process.env.DACHENG_DESKTOP_TOOLS_TOKEN || \"\";\n if (!baseUrl || !token) {\n return {\n isError: true,\n content: [{ type: \"text\", text: \"Fabushi desktop tools bridge URL/token is missing.\" }]\n };\n }\n const response = await fetch(`\\${baseUrl}/v1/tools/execute`, {\n method: \"POST\",\n headers: {\n \"Authorization\": `Bearer \\${token}`,\n \"Content-Type\": \"application/json\"\n },\n body: JSON.stringify({ tool: toolName, arguments: args || {} })\n });\n const payload = await response.json().catch(() => ({}));\n const ok = response.ok && payload && payload.ok !== false;\n const data = payload && Object.prototype.hasOwnProperty.call(payload, \"data\")\n ? payload.data\n : payload;\n return {\n isError: !ok,\n content: [{ type: \"text\", text: toolResultText(data) }],\n details: data\n };\n}\n\nexport default {\n id: \"dacheng-desktop-tools\",\n name: \"Dacheng Desktop Tools\",\n description: \"Expose Fabushi desktop and Chrome control tools to embedded OpenClaw.\",\n configSchema: { type: \"object\", additionalProperties: false, properties: {} },\n register(api) {\n for (const name of tools) {\n api.registerTool({\n name,\n label: name,\n description: descriptions[name] || `Run \\${name} through the Fabushi desktop bridge.`,\n parameters: parameterSchemas[name] || defaultSchema,\n async execute(_toolCallId, args) {\n return await callBridge(name, args);\n }\n });\n }\n }\n};\n''';\n }\n\n Future<Map<String, dynamic>> _mergeEmbeddedConfig(\n File configPath,\n Map<String, dynamic> defaults,\n ) async {\n Map<String, dynamic> current = <String, dynamic>{};\n if (await configPath.exists()) {\n try {\n final decoded = jsonDecode(await configPath.readAsString());\n if (decoded is Map<String, dynamic>) {\n current = decoded;\n } else if (decoded is Map) {\n current = Map<String, dynamic>.from(decoded);\n }\n } catch (_) {\n current = <String, dynamic>{};\n }\n }\n\n final gateway = _mutableMap(current['gateway']);\n final defaultGateway = _mutableMap(defaults['gateway']);\n gateway['mode'] = 'local';\n gateway['port'] = defaultGateway['port'];\n gateway['bind'] = defaultGateway['bind'];\n gateway['auth'] = Map<String, dynamic>.from(defaultGateway['auth'] as Map);\n if (defaultGateway.containsKey('remote')) {\n gateway['remote'] = {\n ..._mutableMap(gateway['remote']),\n ..._mutableMap(defaultGateway['remote']),\n };\n } else {\n final remote = _mutableMap(gateway['remote'])..remove('url');\n if (remote.isEmpty) {\n gateway.remove('remote');\n } else {\n gateway['remote'] = remote;\n }\n }\n\n final gatewayHttp = _mutableMap(gateway['http']);\n final endpoints = _mutableMap(gatewayHttp['endpoints']);\n endpoints['chatCompletions'] = {'enabled': true};\n endpoints['responses'] = {'enabled': true};\n gatewayHttp['endpoints'] = endpoints;\n gateway['http'] = gatewayHttp;\n current['gateway'] = gateway;\n\n final env = _mutableMap(current['env']);\n env.remove('DEEPSEEK_API_KEY');\n if (env.isEmpty) {\n current.remove('env');\n } else {\n current['env'] = env;\n }\n\n final defaultModels = _mutableMap(defaults['models']);\n final models = _mutableMap(current['models']);\n models['mode'] ??= defaultModels['mode'];\n final providers = _mutableMap(models['providers']);\n final defaultProviders = _mutableMap(defaultModels['providers']);\n providers.remove('deepseek');\n providers.remove('dacheng-deepseek');\n providers.remove(_backendDeepSeekProviderId);\n for (final entry in defaultProviders.entries) {\n providers[entry.key] = entry.value is Map\n ? Map<String, dynamic>.from(entry.value as Map)\n : entry.value;\n }\n models['providers'] = providers;\n current['models'] = models;\n\n final defaultBrowser = _mutableMap(defaults['browser']);\n final browser = _mutableMap(current['browser']);\n browser.addAll(defaultBrowser);\n current['browser'] = browser;\n\n // OpenClaw 2026.6 rejects root-level canvas/canvasHost. Canvas is now\n // configured through plugins.entries.canvas.config.host.\n current.remove('canvas');\n current.remove('canvasHost');\n\n final defaultTools = _mutableMap(defaults['tools']);\n final tools = _mutableMap(current['tools']);\n tools.remove('toolSearch');\n tools.remove('fs');\n final alsoAllow = _mergeStringLists(\n tools['alsoAllow'],\n defaultTools['alsoAllow'],\n );\n final execTools = _mutableMap(tools['exec']);\n execTools.remove('mode');\n if (execTools.isNotEmpty) {\n tools['exec'] = execTools;\n }\n tools.addAll(defaultTools);\n if (alsoAllow.isEmpty) {\n tools.remove('alsoAllow');\n } else {\n tools['alsoAllow'] = alsoAllow;\n }\n current['tools'] = tools;\n\n final defaultPlugins = _mutableMap(defaults['plugins']);\n final plugins = _mutableMap(current['plugins']);\n plugins['enabled'] = true;\n final defaultEntries = _mutableMap(defaultPlugins['entries']);\n final requiredPluginIds = defaultEntries.keys.toSet();\n final allowList = _stringList(plugins['allow']);\n if (allowList.isEmpty) {\n plugins.remove('allow');\n } else {\n plugins['allow'] = _mergeStringLists(allowList, requiredPluginIds);\n }\n final denyList = _stringList(\n plugins['deny'],\n ).where((item) => !requiredPluginIds.contains(item)).toList();\n if (denyList.isEmpty) {\n plugins.remove('deny');\n } else {\n plugins['deny'] = denyList;\n }\n final defaultLoad = _mutableMap(defaultPlugins['load']);\n final load = _mutableMap(plugins['load']);\n load['paths'] = _mergeStringLists(load['paths'], defaultLoad['paths']);\n plugins['load'] = load;\n final slots = _mutableMap(plugins['slots']);\n slots.addAll(_mutableMap(defaultPlugins['slots']));\n plugins['slots'] = slots;\n final entries = _mutableMap(plugins['entries']);\n for (final entry in defaultEntries.entries) {\n final currentEntry = _mutableMap(entries[entry.key]);\n currentEntry.addAll(\n entry.value is Map\n ? Map<String, dynamic>.from(entry.value as Map)\n : {'enabled': true},\n );\n entries[entry.key] = currentEntry;\n }\n plugins['entries'] = entries;\n current['plugins'] = plugins;\n\n final defaultChannels = _mutableMap(defaults['channels']);\n final channels = _mutableMap(current['channels']);\n final defaultChannelDefaults = _mutableMap(defaultChannels['defaults']);\n if (defaultChannelDefaults.isNotEmpty) {\n channels['defaults'] = {\n ..._mutableMap(channels['defaults']),\n ...defaultChannelDefaults,\n };\n }\n for (final entry in defaultChannels.entries) {\n if (entry.key == 'defaults') continue;\n final currentChannel = _mutableMap(channels[entry.key]);\n currentChannel.addAll(\n entry.value is Map\n ? Map<String, dynamic>.from(entry.value as Map)\n : <String, dynamic>{},\n );\n channels[entry.key] = currentChannel;\n }\n current['channels'] = channels;\n\n final agents = _mutableMap(current['agents']);\n final defaultAgents = _mutableMap(defaults['agents']);\n final defaultAgentDefaults = _mutableMap(defaultAgents['defaults']);\n final agentDefaults = _mutableMap(agents['defaults']);\n agentDefaults['workspace'] = defaultAgentDefaults['workspace'];\n agentDefaults['model'] = defaultAgentDefaults['model'];\n agents['defaults'] = agentDefaults;\n\n final defaultList = (defaultAgents['list'] as List? ?? const [])\n .whereType<Map>()\n .map((item) => Map<String, dynamic>.from(item))\n .toList();\n final defaultDacheng = defaultList.isNotEmpty\n ? defaultList.first\n : <String, dynamic>{\n 'id': 'dacheng',\n 'default': true,\n 'workspace': agentDefaults['workspace'],\n 'model': agentDefaults['model'],\n };\n final list = (agents['list'] as List? ?? const [])\n .map((item) => item is Map ? Map<String, dynamic>.from(item) : item)\n .toList();\n var foundDacheng = false;\n for (var i = 0; i < list.length; i++) {\n final item = list[i];\n if (item is! Map<String, dynamic>) continue;\n if (item['id'] != 'dacheng') continue;\n foundDacheng = true;\n item['default'] = true;\n item['workspace'] = defaultDacheng['workspace'];\n item['model'] = defaultDacheng['model'];\n list[i] = item;\n }\n if (!foundDacheng) {\n list.add(defaultDacheng);\n }\n agents['list'] = list.isEmpty ? defaultList : list;\n current['agents'] = agents;\n\n return current;\n }\n\n @visibleForTesting\n Future<Map<String, dynamic>> mergeEmbeddedConfigForTest(\n File configPath,\n Map<String, dynamic> defaults,\n ) {\n return _mergeEmbeddedConfig(configPath, defaults);\n }\n\n String _gatewayChatModel(String model) {\n final trimmed = model.trim();\n if (trimmed == 'openclaw' || trimmed.startsWith('openclaw/')) {\n return trimmed;\n }\n return _defaultGatewayModel;\n }\n\n String _defaultDeepSeekModelFor(String model) {\n final trimmed = model.trim();\n if (trimmed.startsWith('deepseek/')) return trimmed;\n return _defaultDeepSeekModel;\n }\n\n String _backendDeepSeekModelRef(String model) {\n final trimmed = model.trim().isEmpty ? _defaultDeepSeekModel : model.trim();\n final modelId = trimmed.contains('/')\n ? trimmed.split('/').last.trim()\n : trimmed;\n final normalizedModelId = modelId.isEmpty ? 'deepseek-chat' : modelId;\n return '$_backendDeepSeekProviderId/$normalizedModelId';\n }\n\n String _deepSeekProxyBaseUrl() {\n final baseUrl = AppConfig.currentAiBackendUrl.replaceFirst(\n RegExp(r'/+$'),\n '',\n );\n return '$baseUrl/api/openclaw/deepseek/v1';\n }\n\n Map<String, dynamic> _mutableMap(Object? value) {\n if (value is Map<String, dynamic>) return Map<String, dynamic>.from(value);\n if (value is Map) return Map<String, dynamic>.from(value);\n return <String, dynamic>{};\n }\n\n List<String> _stringList(Object? value) {\n if (value is! Iterable) return <String>[];\n return value\n .map((item) => item.toString().trim())\n .where((item) => item.isNotEmpty)\n .toList();\n }\n\n List<String> _mergeStringLists(Object? first, Object? second) {\n final merged = <String>{};\n merged.addAll(_stringList(first));\n merged.addAll(_stringList(second));\n return merged.toList();\n }\n\n void _appendBounded(StringBuffer buffer, String chunk) {\n const maxChars = 120000;\n if (chunk.isEmpty || buffer.length >= maxChars) return;\n final remaining = maxChars - buffer.length;\n buffer.write(\n chunk.length <= remaining ? chunk : chunk.substring(0, remaining),\n );\n }\n\n void _captureLogs(Process process) {\n void addLog(String line) {\n final text = line.trim();\n if (text.isEmpty) return;\n _recentLogs.insert(0, text);\n if (_recentLogs.length > 80) {\n _recentLogs.removeRange(80, _recentLogs.length);\n }\n if (kDebugMode) {\n debugPrint('[OpenClaw] $text');\n }\n _diag('process.output', data: {'line': text});\n }\n\n process.stdout\n .transform(utf8.decoder)\n .transform(const LineSplitter())\n .listen(addLog, onError: (_) {});\n process.stderr\n .transform(utf8.decoder)\n .transform(const LineSplitter())\n .listen(addLog, onError: (_) {});\n }\n\n bool _recentLogsContainRuntimeModuleFailure() {\n return _recentLogs.any((line) {\n final lower = line.toLowerCase();\n return lower.contains('err_vm_module_link_failure') ||\n lower.contains('is not in cache') ||\n lower.contains('request for') && lower.contains('.js');\n });\n }\n\n Future<Directory> _stateRoot() async {\n final support = await getApplicationSupportDirectory();\n final dir = Directory(p.join(support.path, 'openclaw_embedded'));\n await dir.create(recursive: true);\n return dir;\n }\n\n Future<_DesktopToolsLaunch> _ensureDesktopTools() async {\n try {\n final status = await DesktopControlBridge.instance.ensureStarted();\n _diag(\n 'desktop-tools.started',\n data: {\n 'bridgeRunning': status.bridgeRunning,\n 'bridgeUri': status.bridgeUri?.toString(),\n 'message': status.message,\n },\n );\n return _DesktopToolsLaunch(\n uri: status.bridgeUri,\n token: await DesktopControlBridge.instance.bridgeToken,\n statusJson: status.toJson(),\n );\n } catch (error) {\n _diag('desktop-tools.error', error: error);\n return _DesktopToolsLaunch(\n uri: null,\n token: null,\n statusJson: {\n 'enabledByBuild': false,\n 'bridgeRunning': false,\n 'message': error.toString(),\n },\n );\n }\n }\n\n Future<Directory> _runtimeDir(\n _OpenClawBundleSpec spec,\n String platformKey,\n ) async {\n final support = await getApplicationSupportDirectory();\n final runtimeBase = Platform.isMacOS\n ? Directory(\n p.join(\n support.parent.path,\n '${p.basename(support.path)}.openclaw_runtime',\n ),\n )\n : Directory(p.join(support.path, 'openclaw_embedded'));\n return Directory(\n p.join(runtimeBase.path, 'runtime', spec.version, platformKey),\n );\n }\n\n Future<Directory?> _bundledRuntimeDir(\n _OpenClawBundleSpec spec,\n String platformKey,\n ) async {\n if (!Platform.isMacOS) return null;\n\n final contentsDir = Directory(\n p.dirname(p.dirname(Platform.resolvedExecutable)),\n );\n final candidates = <Directory>[\n Directory(\n p.join(\n contentsDir.path,\n 'Frameworks',\n 'App.framework',\n 'Resources',\n 'flutter_assets',\n 'assets',\n 'openclaw',\n platformKey,\n ),\n ),\n Directory(\n p.join(\n contentsDir.path,\n 'Frameworks',\n 'App.framework',\n 'Versions',\n 'A',\n 'Resources',\n 'flutter_assets',\n 'assets',\n 'openclaw',\n platformKey,\n ),\n ),\n Directory(\n p.join(\n contentsDir.path,\n 'Resources',\n 'flutter_assets',\n 'assets',\n 'openclaw',\n platformKey,\n ),\n ),\n ];\n\n for (final dir in candidates) {\n final nodePath = File(p.join(dir.path, spec.nodeExecutable));\n final cliPath = File(p.join(dir.path, spec.cliEntrypoint));\n if (await nodePath.exists() && await cliPath.exists()) {\n return dir;\n }\n }\n _diag(\n 'bundle.macos-bundled-runtime-missing',\n data: {\n 'platformKey': platformKey,\n 'contentsDir': contentsDir.path,\n 'candidates': candidates.map((item) => item.path).toList(),\n },\n );\n return null;\n }\n\n Future<Directory?> _runtimeDirOverride(\n _OpenClawBundleSpec spec,\n String platformKey,\n ) async {\n final raw = _configuredRuntimeDirOverride(platformKey);\n if (raw == null) return null;\n\n final dir = Directory(raw);\n final nodePath = File(p.join(dir.path, spec.nodeExecutable));\n final cliPath = File(p.join(dir.path, spec.cliEntrypoint));\n final nodeExists = await nodePath.exists();\n final cliExists = await cliPath.exists();\n _diag(\n 'bundle.override.paths',\n data: {\n 'platformKey': platformKey,\n 'runtimeDir': dir.path,\n 'nodePath': nodePath.path,\n 'nodeExists': nodeExists,\n 'cliPath': cliPath.path,\n 'cliExists': cliExists,\n },\n );\n\n if (!nodeExists || !cliExists) {\n throw OpenClawRuntimeException(\n 'DACHENG_OPENCLAW_RUNTIME_DIR 指向的 OpenClaw runtime 不完整: ${dir.path}',\n );\n }\n return dir;\n }\n\n String? _configuredRuntimeDirOverride(String platformKey) {\n final raw = _runtimeDirOverrideDefine.trim().isNotEmpty\n ? _runtimeDirOverrideDefine.trim()\n : Platform.environment['DACHENG_OPENCLAW_RUNTIME_DIR']?.trim();\n if (raw == null || raw.isEmpty) return null;\n return p.normalize(raw.replaceAll('{platform}', platformKey));\n }\n\n Future<Directory> _legacyRuntimeDir(\n _OpenClawBundleSpec spec,\n String platformKey,\n ) async {\n final support = await getApplicationSupportDirectory();\n return Directory(\n p.join(\n support.path,\n 'openclaw_embedded',\n 'runtime',\n spec.version,\n platformKey,\n ),\n );\n }\n\n OpenClawRuntimeStatus _remember(OpenClawRuntimeStatus status) {\n _lastStatus = status;\n return status;\n }\n\n String? get _platformKey {\n final abi = Abi.current().toString().toLowerCase();\n if (Platform.isMacOS) {\n return abi.contains('arm64') ? 'macos-arm64' : 'macos-x64';\n }\n if (Platform.isWindows) {\n return abi.contains('arm64') ? 'windows-arm64' : 'windows-x64';\n }\n if (Platform.isLinux) {\n return abi.contains('arm64') ? 'linux-arm64' : 'linux-x64';\n }\n return null;\n }\n\n OpenClawRuntimeStatus? get lastStatus => _lastStatus;\n\n void _diag(\n String message, {\n Map<String, Object?> data = const {},\n Object? error,\n StackTrace? stackTrace,\n }) {\n unawaited(\n DiagnosticLogService.instance.log(\n 'openclaw.runtime',\n message,\n data: data,\n error: error,\n stackTrace: stackTrace,\n ),\n );\n }\n}\n\nint? _readInt(Object? value) {\n if (value is int) return value;\n if (value is num) return value.toInt();\n if (value is String) return int.tryParse(value);\n return null;\n}\n\nObject? _decodeJsonOrNull(String body) {\n try {\n return jsonDecode(body);\n } catch (_) {\n return null;\n }\n}\n\nbool _looksLikeHealthPayload(Object? payload) {\n if (payload is! Map) return false;\n if (payload['ok'] == true) return true;\n final status = payload['status']?.toString().toLowerCase();\n return status == 'live' || status == 'ok' || status == 'healthy';\n}\n\nbool _looksLikeModelsPayload(Object? payload) {\n if (payload is List) return true;\n if (payload is! Map) return false;\n return payload['data'] is List ||\n payload['models'] is List ||\n payload['object'] == 'list';\n}\n\nString _previewBody(String body) {\n final trimmed = body.trim();\n return trimmed.length <= 80 ? trimmed : trimmed.substring(0, 80);\n}\n\nString _previewProcessOutput(Object? output) {\n final text = output?.toString().trim() ?? '';\n return text.length <= 200 ? text : text.substring(0, 200);\n}\n"}, "59df30a157fd591eb355cc695de50bb8e10c9ca7": {"t": 1785757950.1036398, "n": 94722, "h": "20dd1a10a021a9c1", "c": "import 'dart:async';\nimport 'dart:convert';\nimport 'dart:ffi' show Abi;\nimport 'dart:io';\nimport 'package:archive/archive_io.dart';\nimport 'package:convert/convert.dart';\nimport 'package:crypto/crypto.dart';\nimport 'package:flutter/foundation.dart';\nimport 'package:flutter/services.dart';\nimport 'package:http/http.dart' as http;\nimport 'package:path/path.dart' as p;\nimport 'package:path_provider/path_provider.dart';\n\nimport '../../core/config/app_config.dart';\nimport '../app_settings.dart';\nimport '../diagnostic_log_service.dart';\nimport '../desktop_control/desktop_control_bridge.dart';\nimport '../desktop_control/desktop_control_policy.dart';\nimport 'openclaw_port_resolver.dart';\n\nenum OpenClawRuntimeState {\n unsupported,\n notBundled,\n stopped,\n starting,\n running,\n failed,\n}\n\nclass OpenClawRuntimeStatus {\n final OpenClawRuntimeState state;\n final String message;\n final int? port;\n final String? platformKey;\n final String? runtimePath;\n final Map<String, dynamic>? desktopToolsStatus;\n final DateTime checkedAt;\n\n const OpenClawRuntimeStatus({\n required this.state,\n required this.message,\n this.port,\n this.platformKey,\n this.runtimePath,\n this.desktopToolsStatus,\n required this.checkedAt,\n });\n\n bool get isHealthy => state == OpenClawRuntimeState.running;\n\n String get label {\n switch (state) {\n case OpenClawRuntimeState.unsupported:\n return '不支持';\n case OpenClawRuntimeState.notBundled:\n return '未内置';\n case OpenClawRuntimeState.stopped:\n return '未启动';\n case OpenClawRuntimeState.starting:\n return '启动中';\n case OpenClawRuntimeState.running:\n return '运行中';\n case OpenClawRuntimeState.failed:\n return '异常';\n }\n }\n}\n\nclass OpenClawGatewayTarget {\n final Uri baseUri;\n final String token;\n final String model;\n final String? modelOverride;\n final Uri? desktopToolsUri;\n final String? desktopToolsToken;\n final Map<String, dynamic>? desktopToolsStatus;\n\n const OpenClawGatewayTarget({\n required this.baseUri,\n required this.token,\n required this.model,\n this.modelOverride,\n this.desktopToolsUri,\n this.desktopToolsToken,\n this.desktopToolsStatus,\n });\n}\n\nclass OpenClawCliResult {\n final List<String> args;\n final int exitCode;\n final String stdout;\n final String stderr;\n final bool timedOut;\n\n const OpenClawCliResult({\n required this.args,\n required this.exitCode,\n required this.stdout,\n required this.stderr,\n this.timedOut = false,\n });\n\n bool get succeeded => exitCode == 0 && !timedOut;\n\n String get command => 'openclaw ${args.join(' ')}';\n\n String get combinedOutput {\n final parts = <String>[\n if (stdout.trim().isNotEmpty) stdout.trimRight(),\n if (stderr.trim().isNotEmpty) stderr.trimRight(),\n ];\n return parts.join('\\n');\n }\n\n OpenClawCliResult append(OpenClawCliResult next) {\n return OpenClawCliResult(\n args: [...args, '&&', ...next.args],\n exitCode: next.exitCode,\n stdout: [\n if (stdout.trim().isNotEmpty) stdout.trimRight(),\n if (next.stdout.trim().isNotEmpty) next.stdout.trimRight(),\n ].join('\\n'),\n stderr: [\n if (stderr.trim().isNotEmpty) stderr.trimRight(),\n if (next.stderr.trim().isNotEmpty) next.stderr.trimRight(),\n ].join('\\n'),\n timedOut: timedOut || next.timedOut,\n );\n }\n}\n\nclass _OpenClawBundleSpec {\n final String version;\n final int defaultPort;\n final String defaultModel;\n final String? defaultModelOverride;\n final String nodeExecutable;\n final String cliEntrypoint;\n final List<String> gatewayArgs;\n final bool downloaded;\n\n const _OpenClawBundleSpec({\n required this.version,\n required this.defaultPort,\n required this.defaultModel,\n this.defaultModelOverride,\n required this.nodeExecutable,\n required this.cliEntrypoint,\n required this.gatewayArgs,\n this.downloaded = false,\n });\n\n Map<String, dynamic> toJson() => {\n 'schema': 1,\n 'version': version,\n 'defaultPort': defaultPort,\n 'defaultModel': defaultModel,\n if (defaultModelOverride != null)\n 'defaultModelOverride': defaultModelOverride,\n 'nodeExecutable': nodeExecutable,\n 'cliEntrypoint': cliEntrypoint,\n 'gatewayArgs': gatewayArgs,\n 'downloaded': downloaded,\n };\n}\n\nclass _OpenClawRuntimeRelease {\n final String version;\n final Uri archiveUri;\n final String sha256Hex;\n final int size;\n final int defaultPort;\n final String defaultModel;\n final String? defaultModelOverride;\n final String nodeExecutable;\n final String cliEntrypoint;\n final List<String> gatewayArgs;\n\n const _OpenClawRuntimeRelease({\n required this.version,\n required this.archiveUri,\n required this.sha256Hex,\n required this.size,\n required this.defaultPort,\n required this.defaultModel,\n this.defaultModelOverride,\n required this.nodeExecutable,\n required this.cliEntrypoint,\n required this.gatewayArgs,\n });\n\n _OpenClawBundleSpec toBundleSpec() => _OpenClawBundleSpec(\n version: version,\n defaultPort: defaultPort,\n defaultModel: defaultModel,\n defaultModelOverride: defaultModelOverride,\n nodeExecutable: nodeExecutable,\n cliEntrypoint: cliEntrypoint,\n gatewayArgs: gatewayArgs,\n downloaded: true,\n );\n}\n\nclass _DesktopToolsLaunch {\n final Uri? uri;\n final String? token;\n final Map<String, dynamic> statusJson;\n\n const _DesktopToolsLaunch({\n required this.uri,\n required this.token,\n required this.statusJson,\n });\n}\n\nclass _OpenClawCliLaunch {\n final _OpenClawBundleSpec spec;\n final Directory runtimeDir;\n final Directory stateRoot;\n final File configPath;\n final String nodePath;\n final String cliPath;\n final int port;\n final String token;\n\n const _OpenClawCliLaunch({\n required this.spec,\n required this.runtimeDir,\n required this.stateRoot,\n required this.configPath,\n required this.nodePath,\n required this.cliPath,\n required this.port,\n required this.token,\n });\n}\n\nclass OpenClawRuntimeException implements Exception {\n final String message;\n\n const OpenClawRuntimeException(this.message);\n\n @override\n String toString() => message;\n}\n\nclass OpenClawRuntime {\n OpenClawRuntime._();\n\n static final OpenClawRuntime instance = OpenClawRuntime._();\n\n static const String _manifestAsset = 'assets/openclaw/bundle_manifest.json';\n static const String _runtimeDirOverrideDefine = String.fromEnvironment(\n 'DACHENG_OPENCLAW_RUNTIME_DIR',\n );\n static const String _defaultGatewayModel =\n AppSettings.defaultOpenClawGatewayModel;\n static const String _defaultDeepSeekModel =\n AppSettings.defaultOpenClawDeepSeekModel;\n static const String _backendDeepSeekProviderId = 'dacheng-deepseek-proxy';\n static const String _desktopToolsPluginId = 'dacheng-desktop-tools';\n static const String _weChatPluginId = 'openclaw-weixin';\n static const String _weChatPluginPackage = '@tencent-weixin/openclaw-weixin';\n static const Duration _startupTimeout = Duration(seconds: 120);\n static const Duration _probeTimeout = Duration(seconds: 3);\n static const Duration _cliDefaultTimeout = Duration(seconds: 45);\n\n Process? _process;\n Future<OpenClawGatewayTarget>? _starting;\n String _processAuthToken = '';\n OpenClawRuntimeStatus? _lastStatus;\n final List<String> _recentLogs = <String>[];\n final OpenClawPortResolver _portResolver = const OpenClawPortResolver();\n\n Future<OpenClawRuntimeStatus> getStatus({bool probe = true}) async {\n final platformKey = _platformKey;\n final port = await AppSettings.getOpenClawGatewayPort();\n _diag(\n 'status.start',\n data: {'platformKey': platformKey, 'port': port, 'probe': probe},\n );\n\n if (platformKey == null) {\n return _remember(\n OpenClawRuntimeStatus(\n state: OpenClawRuntimeState.unsupported,\n message: '当前平台不是 macOS / Windows / Linux 桌面端',\n port: port,\n checkedAt: DateTime.now(),\n ),\n );\n }\n\n try {\n final spec = await _loadSpec(platformKey, checkUpdates: false);\n final overrideRuntimeDir = await _runtimeDirOverride(spec, platformKey);\n final bundledRuntimeDir = overrideRuntimeDir == null\n ? await _bundledRuntimeDir(spec, platformKey)\n : null;\n final runtimeDir =\n overrideRuntimeDir ??\n bundledRuntimeDir ??\n await _runtimeDir(spec, platformKey);\n final nodePath = p.join(runtimeDir.path, spec.nodeExecutable);\n final cliPath = p.join(runtimeDir.path, spec.cliEntrypoint);\n final nodeExists = await File(nodePath).exists();\n final cliExists = await File(cliPath).exists();\n _diag(\n 'status.paths',\n data: {\n 'platformKey': platformKey,\n 'runtimeDir': runtimeDir.path,\n 'nodePath': nodePath,\n 'nodeExists': nodeExists,\n 'cliPath': cliPath,\n 'cliExists': cliExists,\n 'runtimeSource': overrideRuntimeDir != null\n ? 'override'\n : (bundledRuntimeDir == null ? 'cache' : 'bundle'),\n },\n );\n\n if (!nodeExists || !cliExists) {\n final hasBundleAssets = await _hasRequiredBundleAssets(\n platformKey,\n spec,\n );\n _diag(\n 'status.bundle-assets',\n data: {\n 'platformKey': platformKey,\n 'hasBundleAssets': hasBundleAssets,\n 'nodeAsset': 'assets/openclaw/$platformKey/${spec.nodeExecutable}',\n 'cliAsset': 'assets/openclaw/$platformKey/${spec.cliEntrypoint}',\n },\n );\n return _remember(\n OpenClawRuntimeStatus(\n state: hasBundleAssets\n ? OpenClawRuntimeState.stopped\n : OpenClawRuntimeState.notBundled,\n message: hasBundleAssets\n ? '内置 OpenClaw 已随 App 打包,尚未释放或启动'\n : '当前 App 包未包含 $platformKey 的 OpenClaw runtime',\n port: port,\n platformKey: platformKey,\n runtimePath: runtimeDir.path,\n checkedAt: DateTime.now(),\n ),\n );\n }\n\n if (probe) {\n final healthy = await _probe(\n port,\n await AppSettings.getOpenClawGatewayToken(),\n );\n if (healthy) {\n return _remember(\n OpenClawRuntimeStatus(\n state: OpenClawRuntimeState.running,\n message: '本机 OpenClaw Gateway 正在运行',\n port: port,\n platformKey: platformKey,\n runtimePath: runtimeDir.path,\n checkedAt: DateTime.now(),\n ),\n );\n }\n }\n\n return _remember(\n OpenClawRuntimeStatus(\n state: _process == null\n ? OpenClawRuntimeState.stopped\n : OpenClawRuntimeState.starting,\n message: _process == null ? '本机 OpenClaw 未启动' : '本机 OpenClaw 正在启动',\n port: port,\n platformKey: platformKey,\n runtimePath: runtimeDir.path,\n checkedAt: DateTime.now(),\n ),\n );\n } catch (error) {\n _diag('status.error', error: error);\n return _remember(\n OpenClawRuntimeStatus(\n state: OpenClawRuntimeState.failed,\n message: error.toString(),\n port: port,\n platformKey: platformKey,\n checkedAt: DateTime.now(),\n ),\n );\n }\n }\n\n Future<OpenClawGatewayTarget> ensureStarted({\n String? authToken,\n String? username,\n bool isMember = false,\n }) {\n _starting ??= _ensureStartedInternal(\n authToken: authToken,\n username: username,\n isMember: isMember,\n ).whenComplete(() => _starting = null);\n return _starting!;\n }\n\n Future<OpenClawRuntimeStatus> restart() async {\n await stop();\n try {\n await ensureStarted();\n return getStatus();\n } catch (error) {\n return _remember(\n OpenClawRuntimeStatus(\n state: OpenClawRuntimeState.failed,\n message: error.toString(),\n port: await AppSettings.getOpenClawGatewayPort(),\n platformKey: _platformKey,\n checkedAt: DateTime.now(),\n ),\n );\n }\n }\n\n Future<void> stop() async {\n final process = _process;\n _process = null;\n _processAuthToken = '';\n if (process == null) return;\n process.kill();\n try {\n await process.exitCode.timeout(const Duration(seconds: 5));\n } catch (_) {\n process.kill();\n }\n }\n\n Future<OpenClawCliResult> createMobilePairingCode({bool remote = true}) {\n return runCli([\n 'qr',\n if (remote) '--remote',\n '--json',\n ], timeout: const Duration(seconds: 30));\n }\n\n Future<OpenClawCliResult> loginWeChat() {\n return runCli([\n 'channels',\n 'login',\n '--channel',\n _weChatPluginId,\n ], timeout: const Duration(minutes: 3));\n }\n\n Future<OpenClawCliResult> inspectChannels() {\n return runCli([\n 'channels',\n 'status',\n '--probe',\n ], timeout: const Duration(seconds: 40));\n }\n\n Future<OpenClawCliResult> installWeChatPlugin() async {\n final install = await runCli(\n ['plugins', 'install', _weChatPluginPackage, '--force'],\n timeout: const Duration(minutes: 3),\n ensureGateway: false,\n );\n if (!install.succeeded) return install;\n final enable = await runCli(\n ['config', 'set', 'plugins.entries.$_weChatPluginId.enabled', 'true'],\n timeout: const Duration(seconds: 30),\n ensureGateway: false,\n );\n return install.append(enable);\n }\n\n Future<OpenClawCliResult> runCli(\n List<String> args, {\n Duration timeout = _cliDefaultTimeout,\n bool ensureGateway = true,\n }) async {\n if (args.isEmpty) {\n throw const OpenClawRuntimeException('OpenClaw CLI 参数不能为空');\n }\n final launch = await _prepareCliLaunch(ensureGateway: ensureGateway);\n final processArgs = <String>[launch.cliPath, ...args];\n _diag(\n 'cli.start',\n data: {\n 'args': args,\n 'runtimeDir': launch.runtimeDir.path,\n 'configPath': launch.configPath.path,\n 'timeoutSeconds': timeout.inSeconds,\n },\n );\n\n final process = await Process.start(\n launch.nodePath,\n processArgs,\n workingDirectory: launch.runtimeDir.path,\n environment: _buildOpenClawEnvironment(\n runtimeDir: launch.runtimeDir,\n stateRoot: launch.stateRoot,\n configPath: launch.configPath,\n port: launch.port,\n token: launch.token,\n ),\n mode: ProcessStartMode.normal,\n runInShell: false,\n );\n final stdoutBuffer = StringBuffer();\n final stderrBuffer = StringBuffer();\n final stdoutSub = process.stdout\n .transform(utf8.decoder)\n .listen((chunk) => _appendBounded(stdoutBuffer, chunk));\n final stderrSub = process.stderr\n .transform(utf8.decoder)\n .listen((chunk) => _appendBounded(stderrBuffer, chunk));\n\n var timedOut = false;\n int exitCode;\n try {\n exitCode = await process.exitCode.timeout(timeout);\n } on TimeoutException {\n timedOut = true;\n process.kill();\n exitCode = await process.exitCode.timeout(\n const Duration(seconds: 5),\n onTimeout: () => -1,\n );\n } finally {\n await stdoutSub.cancel();\n await stderrSub.cancel();\n }\n\n final result = OpenClawCliResult(\n args: args,\n exitCode: exitCode,\n stdout: stdoutBuffer.toString(),\n stderr: stderrBuffer.toString(),\n timedOut: timedOut,\n );\n _diag(\n result.succeeded ? 'cli.complete' : 'cli.failed',\n data: {\n 'args': args,\n 'exitCode': exitCode,\n 'timedOut': timedOut,\n 'stdoutPreview': _previewProcessOutput(result.stdout),\n 'stderrPreview': _previewProcessOutput(result.stderr),\n },\n );\n return result;\n }\n\n Future<OpenClawGatewayTarget> _ensureStartedInternal({\n String? authToken,\n String? username,\n bool isMember = false,\n }) async {\n final platformKey = _platformKey;\n _diag('ensure-start.start', data: {'platformKey': platformKey});\n if (platformKey == null) {\n throw const OpenClawRuntimeException('当前平台不支持内置 OpenClaw Gateway');\n }\n\n final spec = await _loadSpec(platformKey, checkUpdates: true);\n final requestedPort = await AppSettings.getOpenClawGatewayPort(\n defaultValue: spec.defaultPort,\n );\n final token = await AppSettings.getOpenClawGatewayToken();\n final savedModel = await AppSettings.getOpenClawModel(\n defaultValue: spec.defaultModel,\n );\n final model = _gatewayChatModel(savedModel);\n final requestedAuthToken = authToken?.trim() ?? '';\n final backendDeepSeekModel = _backendDeepSeekModelRef(\n await AppSettings.getOpenClawDeepSeekModel(\n defaultValue: _defaultDeepSeekModelFor(spec.defaultModel),\n ),\n );\n final deepSeekProxyBaseUrl = _deepSeekProxyBaseUrl();\n final modelOverride = await AppSettings.getOpenClawModelOverride(\n defaultValue: spec.defaultModelOverride ?? '',\n );\n final remoteGatewayUrl = await AppSettings.getOpenClawRemoteGatewayUrl();\n final desktopTools = await _ensureDesktopTools();\n _diag(\n 'ensure-start.config',\n data: {\n 'platformKey': platformKey,\n 'requestedPort': requestedPort,\n 'model': model,\n 'rawModel': savedModel,\n 'backendDeepSeekModel': backendDeepSeekModel,\n 'deepSeekProxyBaseUrl': deepSeekProxyBaseUrl,\n 'hasAuthToken': requestedAuthToken.isNotEmpty,\n 'modelOverrideSet': modelOverride.trim().isNotEmpty,\n 'remoteGatewayUrlSet': remoteGatewayUrl.trim().isNotEmpty,\n 'desktopToolsUri': desktopTools.uri?.toString(),\n 'desktopToolsStatus': desktopTools.statusJson,\n },\n );\n\n final canReuseStartedProcess =\n _process != null && _processAuthToken == requestedAuthToken;\n if (canReuseStartedProcess &&\n await _probe(requestedPort, token, context: 'pre-start')) {\n _diag('ensure-start.reuse-existing', data: {'port': requestedPort});\n return OpenClawGatewayTarget(\n baseUri: Uri.parse('http://127.0.0.1:$requestedPort'),\n token: token,\n model: model,\n modelOverride: modelOverride.trim().isEmpty\n ? null\n : modelOverride.trim(),\n desktopToolsUri: desktopTools.uri,\n desktopToolsToken: desktopTools.token,\n desktopToolsStatus: desktopTools.statusJson,\n );\n } else if (_process != null) {\n _diag(\n 'ensure-start.restart-auth-changed',\n data: {\n 'hasPreviousAuthToken': _processAuthToken.isNotEmpty,\n 'hasRequestedAuthToken': requestedAuthToken.isNotEmpty,\n },\n );\n await stop();\n }\n\n final runtimeDir = await _prepareBundle(spec, platformKey);\n final stateRoot = await _stateRoot();\n final desktopToolsPluginDir = await _ensureDesktopToolsPlugin(\n stateRoot: stateRoot,\n );\n if (_process != null) {\n _diag(\n 'process.stop-stale-before-start',\n data: {'requestedPort': requestedPort},\n );\n await stop();\n }\n final portCandidate = await _portResolver.resolve(requestedPort);\n final port = portCandidate.port;\n if (portCandidate.isFallback) {\n await AppSettings.setOpenClawGatewayPort(port);\n _diag(\n 'ensure-start.fallback-port',\n data: {\n 'requestedPort': requestedPort,\n 'selectedPort': port,\n if (portCandidate.reason != null) 'reason': portCandidate.reason,\n },\n );\n } else {\n _diag(\n 'ensure-start.port-bindable',\n data: {'requestedPort': requestedPort, 'selectedPort': port},\n );\n }\n final configPath = await _ensureConfigFile(\n runtimeDir: runtimeDir,\n stateRoot: stateRoot,\n port: port,\n token: token,\n backendDeepSeekModel: backendDeepSeekModel,\n deepSeekProxyBaseUrl: deepSeekProxyBaseUrl,\n remoteGatewayUrl: remoteGatewayUrl,\n extraPluginLoadPaths: [desktopToolsPluginDir.path],\n );\n await _ensureAgentModelConfig(\n stateRoot: stateRoot,\n deepSeekProxyBaseUrl: deepSeekProxyBaseUrl,\n );\n final desktopToolsManifestPath = await _ensureDesktopToolsManifest(\n stateRoot: stateRoot,\n desktopTools: desktopTools,\n );\n\n final nodePath = p.join(runtimeDir.path, spec.nodeExecutable);\n final cliPath = p.join(runtimeDir.path, spec.cliEntrypoint);\n if (!await File(nodePath).exists()) {\n _diag('ensure-start.missing-node', data: {'nodePath': nodePath});\n throw OpenClawRuntimeException('OpenClaw 内置 Node 不存在: $nodePath');\n }\n if (!await File(cliPath).exists()) {\n _diag('ensure-start.missing-cli', data: {'cliPath': cliPath});\n throw OpenClawRuntimeException('OpenClaw CLI 入口不存在: $cliPath');\n }\n\n _remember(\n OpenClawRuntimeStatus(\n state: OpenClawRuntimeState.starting,\n message: '正在启动内置 OpenClaw Gateway',\n port: port,\n platformKey: platformKey,\n runtimePath: runtimeDir.path,\n checkedAt: DateTime.now(),\n ),\n );\n\n final args = <String>[\n cliPath,\n ...spec.gatewayArgs.map((arg) => arg.replaceAll('{port}', '$port')),\n ];\n _recentLogs.clear();\n _diag(\n 'process.starting',\n data: {\n 'nodePath': nodePath,\n 'args': args,\n 'workingDirectory': runtimeDir.path,\n 'configPath': configPath.path,\n 'desktopToolsPluginPath': desktopToolsPluginDir.path,\n 'desktopToolsManifestPath': desktopToolsManifestPath.path,\n 'requestedPort': requestedPort,\n 'selectedPort': port,\n },\n );\n\n final env =\n _buildOpenClawEnvironment(\n runtimeDir: runtimeDir,\n stateRoot: stateRoot,\n configPath: configPath,\n port: port,\n token: token,\n )..addAll({\n 'DACHENG_DESKTOP_TOOLS_ENABLED': desktopTools.uri == null ? '0' : '1',\n 'DACHENG_DESKTOP_TOOLS_MANIFEST': desktopToolsManifestPath.path,\n if (desktopTools.uri != null)\n 'DACHENG_DESKTOP_TOOLS_URL': desktopTools.uri.toString(),\n if (desktopTools.token != null)\n 'DACHENG_DESKTOP_TOOLS_TOKEN': desktopTools.token!,\n 'DACHENG_APP_RUNTIME': '1',\n 'DACHENG_AUTH_TOKEN': (authToken != null && authToken.isNotEmpty)\n ? authToken\n : token,\n if (username != null && username.isNotEmpty)\n 'DACHENG_USERNAME': username,\n 'DACHENG_IS_MEMBER': isMember ? '1' : '0',\n 'DACHENG_OPENCLAW_PROXY_TOKEN': 'dacheng-openclaw-proxy',\n });\n\n _process = await Process.start(\n nodePath,\n args,\n workingDirectory: runtimeDir.path,\n environment: env,\n mode: ProcessStartMode.normal,\n runInShell: false,\n );\n _processAuthToken = requestedAuthToken;\n _captureLogs(_process!);\n _diag('process.started', data: {'pid': _process!.pid, 'port': port});\n\n final deadline = DateTime.now().add(_startupTimeout);\n while (DateTime.now().isBefore(deadline)) {\n if (await _probe(port, token, context: 'startup')) {\n _diag('process.ready', data: {'pid': _process?.pid, 'port': port});\n _remember(\n OpenClawRuntimeStatus(\n state: OpenClawRuntimeState.running,\n message: '本机 OpenClaw Gateway 已启动',\n port: port,\n platformKey: platformKey,\n runtimePath: runtimeDir.path,\n desktopToolsStatus: desktopTools.statusJson,\n checkedAt: DateTime.now(),\n ),\n );\n return OpenClawGatewayTarget(\n baseUri: Uri.parse('http://127.0.0.1:$port'),\n token: token,\n model: model,\n modelOverride: modelOverride.trim().isEmpty\n ? null\n : modelOverride.trim(),\n desktopToolsUri: desktopTools.uri,\n desktopToolsToken: desktopTools.token,\n desktopToolsStatus: desktopTools.statusJson,\n );\n }\n\n if (_process != null) {\n final exited = await _process!.exitCode.timeout(\n const Duration(milliseconds: 1),\n onTimeout: () => -999999,\n );\n if (exited != -999999) {\n final logs = _recentLogs.take(12).join('\\n');\n _diag(\n 'process.exited-early',\n data: {\n 'exitCode': exited,\n 'recentLogs': _recentLogs.take(12).toList(),\n },\n );\n throw OpenClawRuntimeException(\n 'OpenClaw Gateway 提前退出,exitCode=$exited${logs.isEmpty ? '' : '\\n$logs'}',\n );\n }\n }\n await Future<void>.delayed(const Duration(milliseconds: 450));\n }\n\n final logs = _recentLogs.take(12).join('\\n');\n _diag(\n 'process.startup-timeout',\n data: {\n 'timeoutSeconds': _startupTimeout.inSeconds,\n 'recentLogs': _recentLogs.take(12).toList(),\n },\n );\n throw OpenClawRuntimeException(\n 'OpenClaw Gateway 启动超时${logs.isEmpty ? '' : '\\n$logs'}',\n );\n }\n\n Future<_OpenClawBundleSpec> _loadSpec(\n String platformKey, {\n bool checkUpdates = false,\n }) async {\n final raw = await rootBundle.loadString(_manifestAsset);\n final decoded = jsonDecode(raw) as Map<String, dynamic>;\n final platforms = Map<String, dynamic>.from(\n decoded['platforms'] as Map? ?? const {},\n );\n final platform = Map<String, dynamic>.from(\n platforms[platformKey] as Map? ?? const {},\n );\n if (platform.isEmpty) {\n throw OpenClawRuntimeException('bundle_manifest.json 缺少 $platformKey 配置');\n }\n\n final gatewayArgs =\n (platform['gatewayArgs'] as List? ??\n decoded['gatewayArgs'] as List? ??\n const ['gateway', '--port', '{port}'])\n .map((item) => item.toString())\n .toList();\n\n final spec = _OpenClawBundleSpec(\n version: (decoded['version'] ?? 'dev').toString(),\n defaultPort: _readInt(decoded['defaultPort']) ?? 18789,\n defaultModel: (decoded['defaultModel'] ?? 'openclaw/default').toString(),\n defaultModelOverride: decoded['defaultModelOverride']?.toString(),\n nodeExecutable: (platform['nodeExecutable'] ?? 'node/bin/node')\n .toString(),\n cliEntrypoint: (platform['cliEntrypoint'] ?? 'openclaw/bin/openclaw.js')\n .toString(),\n gatewayArgs: gatewayArgs,\n );\n _diag(\n 'manifest.loaded',\n data: {\n 'platformKey': platformKey,\n 'version': spec.version,\n 'defaultPort': spec.defaultPort,\n 'defaultModel': spec.defaultModel,\n 'nodeExecutable': spec.nodeExecutable,\n 'cliEntrypoint': spec.cliEntrypoint,\n 'gatewayArgs': spec.gatewayArgs,\n },\n );\n final allowDownloadedRuntime = await _allowDownloadedRuntime(platformKey);\n var effectiveSpec = spec;\n if (allowDownloadedRuntime) {\n effectiveSpec =\n await _loadActiveDownloadedSpec(platformKey, fallbackSpec: spec) ??\n spec;\n } else {\n await _clearActiveDownloadedRuntimeSpec(\n platformKey: platformKey,\n reason: 'macos-sandbox',\n );\n }\n if (checkUpdates && allowDownloadedRuntime) {\n effectiveSpec =\n await _maybeInstallRuntimeUpdate(\n platformKey: platformKey,\n currentSpec: effectiveSpec,\n ) ??\n effectiveSpec;\n } else if (checkUpdates) {\n _diag(\n 'runtime-update.skip-disabled',\n data: {'platformKey': platformKey, 'reason': 'macos-sandbox'},\n );\n }\n return effectiveSpec;\n }\n\n Future<bool> _allowDownloadedRuntime(String platformKey) async {\n if (!Platform.isMacOS) return true;\n\n final support = await getApplicationSupportDirectory();\n final sandboxContainerId =\n Platform.environment['APP_SANDBOX_CONTAINER_ID']?.trim() ?? '';\n final pathLooksSandboxed = p\n .split(p.normalize(support.path))\n .contains('Containers');\n final sandboxed = sandboxContainerId.isNotEmpty || pathLooksSandboxed;\n if (!sandboxed) return true;\n\n // App Store/TestFlight sandboxed apps cannot reliably execute downloaded\n // Mach-O payloads. Use the bundled runtime that was signed with inherited\n // sandbox entitlements during packaging.\n _diag(\n 'runtime-update.disabled-macos-sandbox',\n data: {\n 'platformKey': platformKey,\n 'supportPath': support.path,\n 'hasSandboxContainerId': sandboxContainerId.isNotEmpty,\n },\n );\n return false;\n }\n\n Future<void> _clearActiveDownloadedRuntimeSpec({\n required String platformKey,\n required String reason,\n }) async {\n final saved = await AppSettings.getOpenClawActiveRuntimeSpec();\n if (saved == null) return;\n await AppSettings.clearOpenClawActiveRuntimeSpec();\n _diag(\n 'runtime-update.active-cleared',\n data: {\n 'platformKey': platformKey,\n 'reason': reason,\n 'version': saved['version']?.toString(),\n 'downloaded': saved['downloaded'] == true,\n },\n );\n }\n\n Future<_OpenClawBundleSpec?> _loadActiveDownloadedSpec(\n String platformKey, {\n required _OpenClawBundleSpec fallbackSpec,\n }) async {\n final saved = await AppSettings.getOpenClawActiveRuntimeSpec();\n if (saved == null) return null;\n final spec = _specFromJson(saved, downloaded: true);\n if (spec == null) {\n await AppSettings.clearOpenClawActiveRuntimeSpec();\n return null;\n }\n final runtimeDir = await _cachedRuntimeDirIfComplete(spec, platformKey);\n if (runtimeDir == null) {\n await AppSettings.clearOpenClawActiveRuntimeSpec();\n _diag(\n 'runtime-update.active-missing',\n data: {'platformKey': platformKey, 'version': spec.version},\n );\n return null;\n }\n _diag(\n 'runtime-update.active-loaded',\n data: {\n 'platformKey': platformKey,\n 'version': spec.version,\n 'runtimeDir': runtimeDir.path,\n 'bundledVersion': fallbackSpec.version,\n },\n );\n return spec;\n }\n\n _OpenClawBundleSpec? _specFromJson(\n Map<String, dynamic> json, {\n required bool downloaded,\n }) {\n final version = json['version']?.toString().trim() ?? '';\n final nodeExecutable = json['nodeExecutable']?.toString().trim() ?? '';\n final cliEntrypoint = json['cliEntrypoint']?.toString().trim() ?? '';\n if (version.isEmpty || nodeExecutable.isEmpty || cliEntrypoint.isEmpty) {\n return null;\n }\n return _OpenClawBundleSpec(\n version: version,\n defaultPort: _readInt(json['defaultPort']) ?? 18789,\n defaultModel: (json['defaultModel'] ?? _defaultGatewayModel).toString(),\n defaultModelOverride: json['defaultModelOverride']?.toString(),\n nodeExecutable: nodeExecutable,\n cliEntrypoint: cliEntrypoint,\n gatewayArgs:\n (json['gatewayArgs'] as List? ??\n const ['gateway', '--port', '{port}', '--force'])\n .map((item) => item.toString())\n .toList(),\n downloaded: downloaded,\n );\n }\n\n Future<_OpenClawBundleSpec?> _maybeInstallRuntimeUpdate({\n required String platformKey,\n required _OpenClawBundleSpec currentSpec,\n }) async {\n if (_configuredRuntimeDirOverride(platformKey) != null) {\n _diag('runtime-update.skip-override', data: {'platformKey': platformKey});\n return null;\n }\n\n try {\n final release = await _fetchRuntimeRelease(\n platformKey: platformKey,\n currentVersion: currentSpec.version,\n );\n if (release == null || release.version == currentSpec.version) {\n return null;\n }\n\n final releaseSpec = release.toBundleSpec();\n final cached = await _cachedRuntimeDirIfComplete(\n releaseSpec,\n platformKey,\n );\n if (cached != null) {\n await AppSettings.setOpenClawActiveRuntimeSpec(releaseSpec.toJson());\n _diag(\n 'runtime-update.use-cached',\n data: {\n 'platformKey': platformKey,\n 'version': release.version,\n 'runtimeDir': cached.path,\n },\n );\n return releaseSpec;\n }\n\n return await _downloadAndInstallRuntimeUpdate(\n release: release,\n platformKey: platformKey,\n );\n } catch (error, stackTrace) {\n _diag(\n 'runtime-update.failed',\n data: {\n 'platformKey': platformKey,\n 'currentVersion': currentSpec.version,\n 'error': error.toString(),\n },\n error: error,\n stackTrace: stackTrace,\n );\n return null;\n }\n }\n\n Future<_OpenClawRuntimeRelease?> _fetchRuntimeRelease({\n required String platformKey,\n required String currentVersion,\n }) async {\n final baseUrl = AppConfig.currentAiBackendUrl.replaceFirst(\n RegExp(r'/+$'),\n '',\n );\n final uri = Uri.parse('$baseUrl/api/openclaw/runtime/manifest').replace(\n queryParameters: {\n 'platform': platformKey,\n 'currentVersion': currentVersion,\n 'appVersion': AppConfig.appVersion,\n },\n );\n final response = await http\n .get(uri, headers: {'Accept': 'application/json'})\n .timeout(const Duration(seconds: 20));\n if (response.statusCode < 200 || response.statusCode >= 300) {\n _diag(\n 'runtime-update.manifest-http',\n data: {'statusCode': response.statusCode, 'uri': uri.toString()},\n );\n return null;\n }\n final decoded = _decodeJsonOrNull(response.body);\n if (decoded is! Map) return null;\n final payload = Map<String, dynamic>.from(decoded);\n if (payload['updateAvailable'] != true) {\n _diag(\n 'runtime-update.none',\n data: {\n 'platformKey': platformKey,\n 'reason': payload['reason']?.toString(),\n 'currentVersion': currentVersion,\n 'latestVersion': payload['latestVersion']?.toString(),\n },\n );\n return null;\n }\n final latest = payload['latest'];\n if (latest is! Map) return null;\n return _releaseFromJson(Map<String, dynamic>.from(latest));\n }\n\n _OpenClawRuntimeRelease? _releaseFromJson(Map<String, dynamic> json) {\n final version = json['version']?.toString().trim() ?? '';\n final archiveUrl = json['archiveUrl']?.toString().trim() ?? '';\n final sha256Hex = json['sha256']?.toString().trim().toLowerCase() ?? '';\n final archiveUri = Uri.tryParse(archiveUrl);\n final gatewayArgs =\n (json['gatewayArgs'] as List? ??\n const ['gateway', '--port', '{port}', '--force'])\n .map((item) => item.toString())\n .toList();\n if (version.isEmpty ||\n archiveUri == null ||\n !archiveUri.hasScheme ||\n !RegExp(r'^[a-f0-9]{64}$').hasMatch(sha256Hex)) {\n _diag(\n 'runtime-update.invalid-release',\n data: {\n 'version': version,\n 'archiveUrlSet': archiveUrl.isNotEmpty,\n 'sha256Length': sha256Hex.length,\n },\n );\n return null;\n }\n return _OpenClawRuntimeRelease(\n version: version,\n archiveUri: archiveUri,\n sha256Hex: sha256Hex,\n size: _readInt(json['size']) ?? 0,\n defaultPort: _readInt(json['defaultPort']) ?? 18789,\n defaultModel: (json['defaultModel'] ?? _defaultGatewayModel).toString(),\n defaultModelOverride: json['defaultModelOverride']?.toString(),\n nodeExecutable: (json['nodeExecutable'] ?? 'node/bin/node').toString(),\n cliEntrypoint: (json['cliEntrypoint'] ?? 'openclaw/openclaw.mjs')\n .toString(),\n gatewayArgs: gatewayArgs,\n );\n }\n\n Future<_OpenClawBundleSpec> _downloadAndInstallRuntimeUpdate({\n required _OpenClawRuntimeRelease release,\n required String platformKey,\n }) async {\n final spec = release.toBundleSpec();\n final runtimeDir = await _runtimeDir(spec, platformKey);\n final support = await getApplicationSupportDirectory();\n final downloadDir = Directory(p.join(support.path, 'openclaw_downloads'));\n await downloadDir.create(recursive: true);\n final archivePath = p.join(\n downloadDir.path,\n '${_safePathPart(release.version)}-$platformKey${_archiveExtension(release.archiveUri)}',\n );\n final archiveFile = File(archivePath);\n final digest = await _downloadRuntimeArchive(release, archiveFile);\n if (digest != release.sha256Hex) {\n await _deleteFileIfExists(archiveFile);\n throw OpenClawRuntimeException(\n 'OpenClaw runtime 校验失败: expected=${release.sha256Hex}, actual=$digest',\n );\n }\n\n final extractDir = Directory(\n p.join(\n downloadDir.path,\n '${_safePathPart(release.version)}-$platformKey-extract',\n ),\n );\n final stagingDir = Directory(\n p.join(\n downloadDir.path,\n '${_safePathPart(release.version)}-$platformKey-stage',\n ),\n );\n if (await extractDir.exists()) await extractDir.delete(recursive: true);\n if (await stagingDir.exists()) await stagingDir.delete(recursive: true);\n await extractDir.create(recursive: true);\n await _extractRuntimeArchive(archiveFile, extractDir);\n\n final extractedRoot = await _findExtractedRuntimeRoot(\n extractDir: extractDir,\n spec: spec,\n );\n if (extractedRoot == null) {\n throw OpenClawRuntimeException('下载的 OpenClaw runtime 不包含必要的 node/cli 文件');\n }\n\n await runtimeDir.parent.create(recursive: true);\n if (await runtimeDir.exists()) await runtimeDir.delete(recursive: true);\n await _moveOrCopyRuntimeDirectory(extractedRoot, stagingDir);\n await stagingDir.rename(runtimeDir.path);\n final marker = File(p.join(runtimeDir.path, '.bundle_ready'));\n await marker.writeAsString(\n const JsonEncoder.withIndent(' ').convert({\n 'installedAt': DateTime.now().toIso8601String(),\n 'version': release.version,\n 'platform': platformKey,\n 'sha256': digest,\n 'source': release.archiveUri.toString(),\n }),\n );\n await _repairRuntimeLaunchMetadata(\n runtimeDir: runtimeDir,\n nodePath: File(p.join(runtimeDir.path, spec.nodeExecutable)),\n platformKey: platformKey,\n cached: false,\n );\n await AppSettings.setOpenClawActiveRuntimeSpec(spec.toJson());\n await _deleteDirectoryIfExists(extractDir);\n await _deleteFileIfExists(archiveFile);\n _diag(\n 'runtime-update.installed',\n data: {\n 'platformKey': platformKey,\n 'version': release.version,\n 'runtimeDir': runtimeDir.path,\n 'size': release.size,\n },\n );\n return spec;\n }\n\n Future<String> _downloadRuntimeArchive(\n _OpenClawRuntimeRelease release,\n File destination,\n ) async {\n final client = http.Client();\n IOSink? output;\n try {\n final request = http.Request('GET', release.archiveUri)\n ..headers['Accept'] = 'application/octet-stream';\n final response = await client\n .send(request)\n .timeout(const Duration(seconds: 20));\n if (response.statusCode < 200 || response.statusCode >= 300) {\n throw OpenClawRuntimeException(\n 'OpenClaw runtime 下载失败 (${response.statusCode})',\n );\n }\n await destination.parent.create(recursive: true);\n output = destination.openWrite();\n final digestSink = AccumulatorSink<Digest>();\n final hashSink = sha256.startChunkedConversion(digestSink);\n var bytes = 0;\n await for (final chunk in response.stream.timeout(\n const Duration(minutes: 5),\n )) {\n bytes += chunk.length;\n hashSink.add(chunk);\n output.add(chunk);\n }\n await output.flush();\n await output.close();\n output = null;\n hashSink.close();\n final digest = digestSink.events.single.toString();\n _diag(\n 'runtime-update.downloaded',\n data: {\n 'archiveUri': release.archiveUri.toString(),\n 'bytes': bytes,\n 'sha256': digest,\n },\n );\n return digest;\n } finally {\n await output?.close().catchError((_) {});\n client.close();\n }\n }\n\n Future<void> _extractRuntimeArchive(\n File archiveFile,\n Directory destination,\n ) async {\n final lowerPath = archiveFile.path.toLowerCase();\n if (Platform.isMacOS && lowerPath.endsWith('.zip')) {\n try {\n final result = await Process.run('/usr/bin/ditto', [\n '-x',\n '-k',\n archiveFile.path,\n destination.path,\n ]).timeout(const Duration(minutes: 5));\n _diag(\n 'runtime-update.extract-native',\n data: {\n 'archivePath': archiveFile.path,\n 'destination': destination.path,\n 'exitCode': result.exitCode,\n if ((result.stderr as Object).toString().isNotEmpty)\n 'stderr': _previewProcessOutput(result.stderr),\n },\n );\n if (result.exitCode == 0) return;\n } catch (error, stackTrace) {\n _diag(\n 'runtime-update.extract-native-failed',\n data: {\n 'archivePath': archiveFile.path,\n 'destination': destination.path,\n 'error': error.toString(),\n },\n error: error,\n stackTrace: stackTrace,\n );\n }\n }\n\n await extractFileToDisk(archiveFile.path, destination.path);\n _diag(\n 'runtime-update.extract-dart',\n data: {'archivePath': archiveFile.path, 'destination': destination.path},\n );\n }\n\n Future<Directory?> _findExtractedRuntimeRoot({\n required Directory extractDir,\n required _OpenClawBundleSpec spec,\n }) async {\n final direct = await _runtimeRootIfComplete(extractDir, spec);\n if (direct != null) return direct;\n await for (final entity in extractDir.list(followLinks: false)) {\n if (entity is! Directory) continue;\n final nested = await _runtimeRootIfComplete(entity, spec);\n if (nested != null) return nested;\n }\n return null;\n }\n\n Future<Directory?> _runtimeRootIfComplete(\n Directory dir,\n _OpenClawBundleSpec spec,\n ) async {\n final nodePath = File(p.join(dir.path, spec.nodeExecutable));\n final cliPath = File(p.join(dir.path, spec.cliEntrypoint));\n if (await nodePath.exists() && await cliPath.exists()) return dir;\n return null;\n }\n\n Future<Directory?> _cachedRuntimeDirIfComplete(\n _OpenClawBundleSpec spec,\n String platformKey,\n ) async {\n final runtimeDir = await _runtimeDir(spec, platformKey);\n final marker = File(p.join(runtimeDir.path, '.bundle_ready'));\n final nodePath = File(p.join(runtimeDir.path, spec.nodeExecutable));\n final cliPath = File(p.join(runtimeDir.path, spec.cliEntrypoint));\n if (await marker.exists() &&\n await nodePath.exists() &&\n await cliPath.exists()) {\n return runtimeDir;\n }\n return null;\n }\n\n String _safePathPart(String value) {\n final safe = value.replaceAll(RegExp(r'[^A-Za-z0-9_.-]+'), '-');\n return safe.isEmpty ? 'runtime' : safe;\n }\n\n String _archiveExtension(Uri uri) {\n final lower = uri.path.toLowerCase();\n if (lower.endsWith('.tar.gz')) return '.tar.gz';\n if (lower.endsWith('.tgz')) return '.tgz';\n if (lower.endsWith('.tar')) return '.tar';\n return '.zip';\n }\n\n Future<void> _deleteFileIfExists(File file) async {\n try {\n if (await file.exists()) await file.delete();\n } catch (_) {}\n }\n\n Future<void> _deleteDirectoryIfExists(Directory dir) async {\n try {\n if (await dir.exists()) await dir.delete(recursive: true);\n } catch (_) {}\n }\n\n Future<Directory> _prepareBundle(\n _OpenClawBundleSpec spec,\n String platformKey,\n ) async {\n final overrideRuntimeDir = await _runtimeDirOverride(spec, platformKey);\n if (overrideRuntimeDir != null) {\n final nodePath = File(\n p.join(overrideRuntimeDir.path, spec.nodeExecutable),\n );\n await _repairRuntimeLaunchMetadata(\n runtimeDir: overrideRuntimeDir,\n nodePath: nodePath,\n platformKey: platformKey,\n cached: true,\n );\n _diag(\n 'bundle.launch.override-runtime',\n data: {\n 'platformKey': platformKey,\n 'runtimeDir': overrideRuntimeDir.path,\n },\n );\n return overrideRuntimeDir;\n }\n\n if (spec.downloaded) {\n final downloadedRuntimeDir = await _cachedRuntimeDirIfComplete(\n spec,\n platformKey,\n );\n if (downloadedRuntimeDir != null) {\n await _repairRuntimeLaunchMetadata(\n runtimeDir: downloadedRuntimeDir,\n nodePath: File(\n p.join(downloadedRuntimeDir.path, spec.nodeExecutable),\n ),\n platformKey: platformKey,\n cached: true,\n );\n _diag(\n 'bundle.launch.downloaded-runtime',\n data: {\n 'platformKey': platformKey,\n 'version': spec.version,\n 'runtimeDir': downloadedRuntimeDir.path,\n },\n );\n return downloadedRuntimeDir;\n }\n await AppSettings.clearOpenClawActiveRuntimeSpec();\n throw OpenClawRuntimeException(\n '已下载的 OpenClaw runtime 不完整,已回退到内置版本,请重新启动。',\n );\n }\n\n final bundledRuntimeDir = await _bundledRuntimeDir(spec, platformKey);\n if (bundledRuntimeDir != null) {\n _diag(\n 'bundle.launch.bundled-runtime',\n data: {\n 'platformKey': platformKey,\n 'runtimeDir': bundledRuntimeDir.path,\n },\n );\n return bundledRuntimeDir;\n }\n\n if (Platform.isMacOS) {\n throw OpenClawRuntimeException(\n '当前安装包没有可执行的 $platformKey OpenClaw runtime。请先运行 scripts/build_openclaw_desktop_bundle.sh $platformKey,再通过 flutter run 启动。',\n );\n }\n\n final runtimeDir = await _runtimeDir(spec, platformKey);\n final marker = File(p.join(runtimeDir.path, '.bundle_ready'));\n final nodePath = File(p.join(runtimeDir.path, spec.nodeExecutable));\n final cliPath = File(p.join(runtimeDir.path, spec.cliEntrypoint));\n\n if (await marker.exists() &&\n await nodePath.exists() &&\n await cliPath.exists()) {\n await _repairRuntimeLaunchMetadata(\n runtimeDir: runtimeDir,\n nodePath: nodePath,\n platformKey: platformKey,\n cached: true,\n );\n _diag(\n 'bundle.prepare.cached',\n data: {\n 'platformKey': platformKey,\n 'runtimeDir': runtimeDir.path,\n 'marker': marker.path,\n },\n );\n return runtimeDir;\n }\n\n final legacyRuntimeDir = await _legacyRuntimeDir(spec, platformKey);\n if (legacyRuntimeDir.path != runtimeDir.path &&\n await File(\n p.join(legacyRuntimeDir.path, spec.nodeExecutable),\n ).exists() &&\n await File(\n p.join(legacyRuntimeDir.path, spec.cliEntrypoint),\n ).exists()) {\n _diag(\n 'bundle.prepare.migrate-legacy',\n data: {\n 'platformKey': platformKey,\n 'from': legacyRuntimeDir.path,\n 'to': runtimeDir.path,\n },\n );\n if (await runtimeDir.exists()) {\n await runtimeDir.delete(recursive: true);\n }\n await _copyRuntimeDirectory(legacyRuntimeDir, runtimeDir);\n await _repairRuntimeLaunchMetadata(\n runtimeDir: runtimeDir,\n nodePath: nodePath,\n platformKey: platformKey,\n cached: false,\n );\n await marker.writeAsString(DateTime.now().toIso8601String());\n _diag(\n 'bundle.prepare.migrate-complete',\n data: {\n 'platformKey': platformKey,\n 'runtimeDir': runtimeDir.path,\n 'nodeExists': await nodePath.exists(),\n 'cliExists': await cliPath.exists(),\n },\n );\n return runtimeDir;\n }\n\n final prefix = 'assets/openclaw/$platformKey/';\n final assets = await _listAssets(prefix);\n final hasNodeAsset = assets.contains('$prefix${spec.nodeExecutable}');\n final hasCliAsset = assets.contains('$prefix${spec.cliEntrypoint}');\n _diag(\n 'bundle.prepare.assets',\n data: {\n 'platformKey': platformKey,\n 'prefix': prefix,\n 'assetCount': assets.length,\n 'hasNodeAsset': hasNodeAsset,\n 'hasCliAsset': hasCliAsset,\n 'nodeAsset': '$prefix${spec.nodeExecutable}',\n 'cliAsset': '$prefix${spec.cliEntrypoint}',\n 'sample': assets.take(8).toList(),\n },\n );\n if (!hasNodeAsset || !hasCliAsset) {\n throw OpenClawRuntimeException(\n '当前安装包没有内置 $platformKey 的 OpenClaw runtime。请在 release 构建中运行 scripts/build_openclaw_desktop_bundle.sh 后再打包。',\n );\n }\n\n if (await runtimeDir.exists()) {\n _diag('bundle.prepare.remove-old', data: {'runtimeDir': runtimeDir.path});\n await runtimeDir.delete(recursive: true);\n }\n await runtimeDir.create(recursive: true);\n\n for (final asset in assets) {\n final relative = asset.substring(prefix.length);\n if (relative.isEmpty || relative.endsWith('/')) continue;\n final bytes = await rootBundle.load(asset);\n final file = File(p.join(runtimeDir.path, relative));\n await file.parent.create(recursive: true);\n await file.writeAsBytes(\n bytes.buffer.asUint8List(bytes.offsetInBytes, bytes.lengthInBytes),\n flush: false,\n );\n }\n\n await _repairRuntimeLaunchMetadata(\n runtimeDir: runtimeDir,\n nodePath: nodePath,\n platformKey: platformKey,\n cached: false,\n );\n await marker.writeAsString(DateTime.now().toIso8601String());\n _diag(\n 'bundle.prepare.complete',\n data: {\n 'platformKey': platformKey,\n 'runtimeDir': runtimeDir.path,\n 'assetCount': assets.length,\n 'nodeExists': await nodePath.exists(),\n 'nodeSize': await nodePath.exists() ? await nodePath.length() : 0,\n 'cliExists': await cliPath.exists(),\n 'cliSize': await cliPath.exists() ? await cliPath.length() : 0,\n },\n );\n return runtimeDir;\n }\n\n Future<void> _copyRuntimeDirectory(\n Directory source,\n Directory destination,\n ) async {\n await destination.create(recursive: true);\n await for (final entity in source.list(\n recursive: true,\n followLinks: false,\n )) {\n final relative = p.relative(entity.path, from: source.path);\n if (relative == '.') continue;\n final target = p.join(destination.path, relative);\n if (entity is Directory) {\n await Directory(target).create(recursive: true);\n } else if (entity is File) {\n final targetFile = File(target);\n await targetFile.parent.create(recursive: true);\n await entity.openRead().pipe(targetFile.openWrite());\n }\n }\n }\n\n Future<void> _moveOrCopyRuntimeDirectory(\n Directory source,\n Directory destination,\n ) async {\n if (await destination.exists()) {\n await destination.delete(recursive: true);\n }\n await destination.parent.create(recursive: true);\n try {\n await source.rename(destination.path);\n return;\n } on FileSystemException {\n await _copyRuntimeDirectory(source, destination);\n }\n }\n\n Future<void> _repairRuntimeLaunchMetadata({\n required Directory runtimeDir,\n required File nodePath,\n required String platformKey,\n required bool cached,\n }) async {\n if (Platform.isWindows) return;\n\n final chmodResult = await Process.run('/bin/chmod', ['+x', nodePath.path]);\n ProcessResult? xattrResult;\n if (Platform.isMacOS) {\n // Files created by a quarantined app can inherit the quarantine xattr.\n // macOS then rejects Process.start with \"Operation not permitted\".\n xattrResult = await Process.run('/usr/bin/xattr', [\n '-dr',\n 'com.apple.quarantine',\n runtimeDir.path,\n ]);\n await Process.run('/usr/bin/xattr', [\n '-dr',\n 'com.apple.provenance',\n runtimeDir.path,\n ]);\n }\n\n _diag(\n 'bundle.prepare.launch-metadata',\n data: {\n 'platformKey': platformKey,\n 'runtimeDir': runtimeDir.path,\n 'nodePath': nodePath.path,\n 'cached': cached,\n 'chmodExitCode': chmodResult.exitCode,\n if ((chmodResult.stderr as Object).toString().isNotEmpty)\n 'chmodStderr': _previewProcessOutput(chmodResult.stderr),\n if (xattrResult != null) 'xattrExitCode': xattrResult.exitCode,\n if (xattrResult != null &&\n (xattrResult.stderr as Object).toString().isNotEmpty)\n 'xattrStderr': _previewProcessOutput(xattrResult.stderr),\n },\n );\n }\n\n Future<bool> _hasRequiredBundleAssets(\n String platformKey,\n _OpenClawBundleSpec spec,\n ) async {\n final prefix = 'assets/openclaw/$platformKey/';\n final assets = await _listAssets(prefix);\n final hasRequired =\n assets.contains('$prefix${spec.nodeExecutable}') &&\n assets.contains('$prefix${spec.cliEntrypoint}');\n _diag(\n 'bundle.has-required-assets',\n data: {\n 'platformKey': platformKey,\n 'assetCount': assets.length,\n 'hasRequired': hasRequired,\n },\n );\n return hasRequired;\n }\n\n Future<List<String>> _listAssets(String prefix) async {\n final assets = <String>{};\n var jsonCount = 0;\n var binCount = 0;\n var indexCount = 0;\n final failures = <String>[];\n\n try {\n final raw = await rootBundle.loadString('AssetManifest.json');\n final decoded = jsonDecode(raw) as Map<String, dynamic>;\n final items = decoded.keys\n .where((key) => key.startsWith(prefix))\n .toList();\n jsonCount = items.length;\n assets.addAll(items);\n } catch (error) {\n failures.add('AssetManifest.json: $error');\n // Newer Flutter release builds may only include AssetManifest.bin.\n }\n\n try {\n final manifest = await AssetManifest.loadFromAssetBundle(rootBundle);\n final items = manifest\n .listAssets()\n .where((key) => key.startsWith(prefix))\n .toList();\n binCount = items.length;\n assets.addAll(items);\n } catch (error) {\n failures.add('AssetManifest.bin: $error');\n // App Store archives patched after flutter build carry a JSON/index file.\n }\n\n try {\n final raw = await rootBundle.loadString(\n 'assets/openclaw/asset_index.json',\n );\n final decoded = jsonDecode(raw);\n final indexedAssets = decoded is Map\n ? decoded['assets']\n : decoded is List\n ? decoded\n : const [];\n if (indexedAssets is List) {\n final items = indexedAssets\n .map((item) => item.toString())\n .where((key) => key.startsWith(prefix))\n .toList();\n indexCount = items.length;\n assets.addAll(items);\n }\n } catch (error) {\n failures.add('asset_index.json: $error');\n // Older builds do not have an OpenClaw asset index.\n }\n\n final sortedAssets = assets.toList()..sort();\n _diag(\n 'asset.list',\n data: {\n 'prefix': prefix,\n 'total': sortedAssets.length,\n 'jsonCount': jsonCount,\n 'binCount': binCount,\n 'indexCount': indexCount,\n if (failures.isNotEmpty) 'failures': failures,\n 'sample': sortedAssets.take(8).toList(),\n },\n );\n return sortedAssets;\n }\n\n Future<bool> _probe(\n int port,\n String token, {\n String context = 'probe',\n }) async {\n try {\n final healthResponse = await http\n .get(\n Uri.parse('http://127.0.0.1:$port/health'),\n headers: {'Authorization': 'Bearer $token'},\n )\n .timeout(_probeTimeout);\n final healthPayload = _decodeJsonOrNull(healthResponse.body);\n if (healthResponse.statusCode < 200 ||\n healthResponse.statusCode >= 300 ||\n !_looksLikeHealthPayload(healthPayload)) {\n _diag(\n 'probe.health-unhealthy',\n data: {\n 'context': context,\n 'port': port,\n 'statusCode': healthResponse.statusCode,\n 'contentType': healthResponse.headers['content-type'],\n 'bodyPreview': _previewBody(healthResponse.body),\n },\n );\n return false;\n }\n\n final modelsResponse = await http\n .get(\n Uri.parse('http://127.0.0.1:$port/v1/models'),\n headers: {'Authorization': 'Bearer $token'},\n )\n .timeout(_probeTimeout);\n final modelsPayload = _decodeJsonOrNull(modelsResponse.body);\n if (modelsResponse.statusCode < 200 ||\n modelsResponse.statusCode >= 300 ||\n !_looksLikeModelsPayload(modelsPayload)) {\n _diag(\n 'probe.models-unhealthy',\n data: {\n 'context': context,\n 'port': port,\n 'statusCode': modelsResponse.statusCode,\n 'contentType': modelsResponse.headers['content-type'],\n 'bodyPreview': _previewBody(modelsResponse.body),\n },\n );\n return false;\n }\n\n // `/v1/models` can be healthy while `/v1/chat/completions` is disabled\n // by an old config. Use GET so the probe only checks route wiring and\n // never starts a real model request.\n final chatResponse = await http\n .get(\n Uri.parse('http://127.0.0.1:$port/v1/chat/completions'),\n headers: {'Authorization': 'Bearer $token'},\n )\n .timeout(_probeTimeout);\n final healthy =\n chatResponse.statusCode != 401 &&\n chatResponse.statusCode != 403 &&\n chatResponse.statusCode != 404;\n _diag(\n healthy ? 'probe.healthy' : 'probe.chat-unhealthy',\n data: {\n 'context': context,\n 'port': port,\n 'chatStatusCode': chatResponse.statusCode,\n },\n );\n return healthy;\n } catch (error) {\n if (context != 'startup') {\n _diag(\n 'probe.error',\n data: {'context': context, 'port': port},\n error: error,\n );\n }\n return false;\n }\n }\n\n Future<File> _ensureConfigFile({\n required Directory runtimeDir,\n required Directory stateRoot,\n required int port,\n required String token,\n required String backendDeepSeekModel,\n required String deepSeekProxyBaseUrl,\n required String remoteGatewayUrl,\n List<String> extraPluginLoadPaths = const [],\n }) async {\n final configDir = Directory(p.join(stateRoot.path, 'config'));\n await configDir.create(recursive: true);\n final workspace = Directory(p.join(stateRoot.path, 'workspace'));\n await workspace.create(recursive: true);\n final configPath = File(p.join(configDir.path, 'openclaw.json'));\n final bundledPluginLoadPaths = await _bundledPluginLoadPaths(runtimeDir);\n final pluginLoadPaths = _mergeStringLists(\n extraPluginLoadPaths,\n bundledPluginLoadPaths,\n );\n final hasWeChatPlugin = pluginLoadPaths.any(\n (path) => p.basename(path) == _weChatPluginId,\n );\n final remoteUrl = remoteGatewayUrl.trim();\n\n final config = _buildEmbeddedConfig(\n stateRoot: stateRoot,\n port: port,\n token: token,\n backendDeepSeekModel: backendDeepSeekModel,\n deepSeekProxyBaseUrl: deepSeekProxyBaseUrl,\n remoteGatewayUrl: remoteUrl,\n pluginLoadPaths: pluginLoadPaths,\n hasWeChatPlugin: hasWeChatPlugin,\n );\n\n // Repair older embedded configs in-place. OpenClaw 2026.6 requires\n // gateway.mode=local, and the home screen requires the OpenAI-compatible\n // chat endpoint. Preserve unrelated user edits while fixing the fields\n // needed for the app-owned embedded gateway to start reliably.\n final merged = await _mergeEmbeddedConfig(configPath, config);\n await configPath.writeAsString(\n const JsonEncoder.withIndent(' ').convert(merged),\n );\n _diag(\n 'config.written',\n data: {\n 'configPath': configPath.path,\n 'port': port,\n 'gatewayMode': _mutableMap(merged['gateway'])['mode'],\n 'backendDeepSeekModel': backendDeepSeekModel,\n 'deepSeekProxyBaseUrl': deepSeekProxyBaseUrl,\n 'remoteGatewayUrlSet': remoteUrl.isNotEmpty,\n 'pluginLoadPaths': pluginLoadPaths,\n 'hasWeChatPlugin': hasWeChatPlugin,\n },\n );\n return configPath;\n }\n\n @visibleForTesting\n Map<String, dynamic> buildEmbeddedConfigForTest({\n required Directory stateRoot,\n required int port,\n required String token,\n required String backendDeepSeekModel,\n required String deepSeekProxyBaseUrl,\n String remoteGatewayUrl = '',\n List<String> pluginLoadPaths = const [],\n bool hasWeChatPlugin = false,\n }) {\n return _buildEmbeddedConfig(\n stateRoot: stateRoot,\n port: port,\n token: token,\n backendDeepSeekModel: backendDeepSeekModel,\n deepSeekProxyBaseUrl: deepSeekProxyBaseUrl,\n remoteGatewayUrl: remoteGatewayUrl,\n pluginLoadPaths: pluginLoadPaths,\n hasWeChatPlugin: hasWeChatPlugin,\n );\n }\n\n Map<String, dynamic> _buildEmbeddedConfig({\n required Directory stateRoot,\n required int port,\n required String token,\n required String backendDeepSeekModel,\n required String deepSeekProxyBaseUrl,\n required String remoteGatewayUrl,\n required List<String> pluginLoadPaths,\n required bool hasWeChatPlugin,\n }) {\n final workspace = p.join(stateRoot.path, 'workspace');\n final remoteUrl = remoteGatewayUrl.trim();\n final desktopToolAllowList = DesktopControlPolicy.supportedTools.toList()\n ..sort();\n final canvasHostConfig = <String, dynamic>{\n 'enabled': true,\n 'root': p.join(stateRoot.path, 'canvas'),\n };\n\n return <String, dynamic>{\n 'gateway': {\n 'mode': 'local',\n 'port': port,\n 'bind': 'loopback',\n 'auth': {'mode': 'token', 'token': token},\n if (remoteUrl.isNotEmpty) 'remote': {'enabled': true, 'url': remoteUrl},\n 'http': {\n 'endpoints': {\n 'chatCompletions': {'enabled': true},\n 'responses': {'enabled': true},\n },\n },\n },\n 'models': {\n 'mode': 'merge',\n 'providers': {\n _backendDeepSeekProviderId: {\n 'baseUrl': deepSeekProxyBaseUrl,\n 'api': 'openai-completions',\n 'apiKey': 'DACHENG_OPENCLAW_PROXY_TOKEN',\n 'authHeader': true,\n 'headers': {'x-dacheng-auth-token': 'DACHENG_AUTH_TOKEN'},\n 'models': [\n {\n 'id': 'deepseek-chat',\n 'name': 'DeepSeek Chat',\n 'contextWindow': 131072,\n 'maxTokens': 8192,\n 'input': ['text'],\n 'compat': {\n 'requiresStringContent': true,\n 'strictMessageKeys': true,\n },\n },\n {\n 'id': 'deepseek-reasoner',\n 'name': 'DeepSeek Reasoner',\n 'contextWindow': 131072,\n 'maxTokens': 8192,\n 'reasoning': true,\n 'input': ['text'],\n 'compat': {\n 'requiresStringContent': true,\n 'strictMessageKeys': true,\n },\n },\n ],\n },\n },\n },\n 'browser': {\n 'enabled': true,\n 'defaultProfile': 'openclaw',\n 'headless': false,\n 'ssrfPolicy': {'dangerouslyAllowPrivateNetwork': true},\n },\n 'tools': {\n 'profile': 'full',\n 'alsoAllow': desktopToolAllowList,\n 'exec': {'host': 'gateway', 'security': 'full', 'ask': 'off'},\n },\n 'plugins': {\n 'enabled': true,\n 'deny': <String>[],\n 'load': {'paths': pluginLoadPaths},\n 'slots': {'memory': 'memory-core'},\n 'entries': <String, dynamic>{\n 'memory-core': {'enabled': true},\n 'bonjour': {'enabled': false},\n 'browser': {'enabled': true},\n 'canvas': {\n 'enabled': true,\n 'config': {'host': canvasHostConfig},\n },\n 'device-pair': {'enabled': true},\n 'file-transfer': {'enabled': true},\n 'phone-control': {'enabled': true},\n 'talk-voice': {'enabled': true},\n _desktopToolsPluginId: {'enabled': true},\n if (hasWeChatPlugin) _weChatPluginId: {'enabled': true},\n },\n },\n 'channels': {\n 'defaults': {'groupPolicy': 'allowlist'},\n if (hasWeChatPlugin)\n _weChatPluginId: {\n 'enabled': true,\n 'dmPolicy': 'pairing',\n 'allowFrom': <String>[],\n 'accounts': <String, dynamic>{},\n },\n },\n 'agents': {\n 'defaults': {\n 'workspace': workspace,\n 'model': {'primary': backendDeepSeekModel},\n },\n 'list': [\n {\n 'id': 'dacheng',\n 'default': true,\n 'workspace': workspace,\n 'model': {'primary': backendDeepSeekModel},\n },\n ],\n },\n };\n }\n\n Future<File> _ensureDesktopToolsManifest({\n required Directory stateRoot,\n required _DesktopToolsLaunch desktopTools,\n }) async {\n final configDir = Directory(p.join(stateRoot.path, 'config'));\n await configDir.create(recursive: true);\n final manifestPath = File(p.join(configDir.path, 'desktop_tools.json'));\n final tools =\n DesktopControlPolicy.supportedTools\n .map(\n (name) => {\n 'name': name,\n 'endpoint': '/v1/tools/execute',\n 'readOnly': DesktopControlPolicy.isReadOnly(name),\n 'requiresConfirmation':\n DesktopControlPolicy.requiresConfirmation(name),\n },\n )\n .toList()\n ..sort(\n (a, b) => a['name'].toString().compareTo(b['name'].toString()),\n );\n\n await manifestPath.writeAsString(\n const JsonEncoder.withIndent(' ').convert({\n 'version': 1,\n 'transport': 'http-loopback',\n 'baseUrl': desktopTools.uri?.toString(),\n 'auth': {'type': 'bearer', 'env': 'DACHENG_DESKTOP_TOOLS_TOKEN'},\n 'status': desktopTools.statusJson,\n 'tools': tools,\n }),\n );\n _diag(\n 'desktop-tools.manifest-written',\n data: {\n 'manifestPath': manifestPath.path,\n 'hasBaseUrl': desktopTools.uri != null,\n },\n );\n return manifestPath;\n }\n\n Future<File> _ensureAgentModelConfig({\n required Directory stateRoot,\n required String deepSeekProxyBaseUrl,\n }) async {\n final agentDir = Directory(\n p.join(stateRoot.path, 'state', 'agents', 'dacheng', 'agent'),\n );\n await agentDir.create(recursive: true);\n final modelsPath = File(p.join(agentDir.path, 'models.json'));\n final providerConfig = <String, dynamic>{\n 'baseUrl': deepSeekProxyBaseUrl,\n 'api': 'openai-completions',\n 'apiKey': 'DACHENG_OPENCLAW_PROXY_TOKEN',\n 'authHeader': true,\n 'headers': {'x-dacheng-auth-token': 'DACHENG_AUTH_TOKEN'},\n 'models': [\n {\n 'id': 'deepseek-chat',\n 'name': 'DeepSeek Chat',\n 'contextWindow': 131072,\n 'maxTokens': 8192,\n 'input': ['text'],\n 'compat': {'requiresStringContent': true, 'strictMessageKeys': true},\n },\n {\n 'id': 'deepseek-reasoner',\n 'name': 'DeepSeek Reasoner',\n 'contextWindow': 131072,\n 'maxTokens': 8192,\n 'reasoning': true,\n 'input': ['text'],\n 'compat': {'requiresStringContent': true, 'strictMessageKeys': true},\n },\n ],\n };\n await modelsPath.writeAsString(\n const JsonEncoder.withIndent(' ').convert({\n 'providers': {_backendDeepSeekProviderId: providerConfig},\n }),\n );\n _diag(\n 'agent-models.written',\n data: {\n 'modelsPath': modelsPath.path,\n 'provider': _backendDeepSeekProviderId,\n 'deepSeekProxyBaseUrl': deepSeekProxyBaseUrl,\n },\n );\n return modelsPath;\n }\n\n Future<_OpenClawCliLaunch> _prepareCliLaunch({\n required bool ensureGateway,\n }) async {\n if (ensureGateway) {\n await ensureStarted();\n }\n final platformKey = _platformKey;\n if (platformKey == null) {\n throw const OpenClawRuntimeException('当前平台不支持内置 OpenClaw Gateway');\n }\n final spec = await _loadSpec(platformKey, checkUpdates: false);\n final runtimeDir = await _prepareBundle(spec, platformKey);\n final stateRoot = await _stateRoot();\n final desktopToolsPluginDir = await _ensureDesktopToolsPlugin(\n stateRoot: stateRoot,\n );\n final port = await AppSettings.getOpenClawGatewayPort(\n defaultValue: spec.defaultPort,\n );\n final token = await AppSettings.getOpenClawGatewayToken();\n final backendDeepSeekModel = _backendDeepSeekModelRef(\n await AppSettings.getOpenClawDeepSeekModel(\n defaultValue: _defaultDeepSeekModelFor(spec.defaultModel),\n ),\n );\n final configPath = await _ensureConfigFile(\n runtimeDir: runtimeDir,\n stateRoot: stateRoot,\n port: port,\n token: token,\n backendDeepSeekModel: backendDeepSeekModel,\n deepSeekProxyBaseUrl: _deepSeekProxyBaseUrl(),\n remoteGatewayUrl: await AppSettings.getOpenClawRemoteGatewayUrl(),\n extraPluginLoadPaths: [desktopToolsPluginDir.path],\n );\n final nodePath = p.join(runtimeDir.path, spec.nodeExecutable);\n final cliPath = p.join(runtimeDir.path, spec.cliEntrypoint);\n if (!await File(nodePath).exists()) {\n throw OpenClawRuntimeException('OpenClaw 内置 Node 不存在: $nodePath');\n }\n if (!await File(cliPath).exists()) {\n throw OpenClawRuntimeException('OpenClaw CLI 入口不存在: $cliPath');\n }\n return _OpenClawCliLaunch(\n spec: spec,\n runtimeDir: runtimeDir,\n stateRoot: stateRoot,\n configPath: configPath,\n nodePath: nodePath,\n cliPath: cliPath,\n port: port,\n token: token,\n );\n }\n\n Map<String, String> _buildOpenClawEnvironment({\n required Directory runtimeDir,\n required Directory stateRoot,\n required File configPath,\n required int port,\n required String token,\n }) {\n final nodeBinDir = Platform.isWindows\n ? p.join(runtimeDir.path, 'node')\n : p.join(runtimeDir.path, 'node', 'bin');\n final pathKey = Platform.isWindows ? 'Path' : 'PATH';\n final currentPath =\n Platform.environment[pathKey] ?? Platform.environment['PATH'] ?? '';\n final separator = Platform.isWindows ? ';' : ':';\n final pathValue = currentPath.isEmpty\n ? nodeBinDir\n : '$nodeBinDir$separator$currentPath';\n final env = Map<String, String>.from(Platform.environment)\n ..remove('NODE_COMPILE_CACHE')\n ..addAll({\n 'OPENCLAW_GATEWAY_PORT': '$port',\n 'OPENCLAW_GATEWAY_BIND': 'loopback',\n 'OPENCLAW_GATEWAY_TOKEN': token,\n 'OPENCLAW_CONFIG_PATH': configPath.path,\n 'OPENCLAW_STATE_DIR': p.join(stateRoot.path, 'state'),\n 'OPENCLAW_AGENT_DIR': p.join(stateRoot.path, 'agents'),\n 'OPENCLAW_WORKSPACE': p.join(stateRoot.path, 'workspace'),\n 'OPENCLAW_DISABLE_BONJOUR': '1',\n 'NODE_DISABLE_COMPILE_CACHE': '1',\n pathKey: pathValue,\n if (pathKey != 'PATH') 'PATH': pathValue,\n });\n return env;\n }\n\n @visibleForTesting\n Map<String, String> buildOpenClawEnvironmentForTest({\n required Directory runtimeDir,\n required Directory stateRoot,\n required File configPath,\n required int port,\n required String token,\n }) {\n return _buildOpenClawEnvironment(\n runtimeDir: runtimeDir,\n stateRoot: stateRoot,\n configPath: configPath,\n port: port,\n token: token,\n );\n }\n\n Future<List<String>> _bundledPluginLoadPaths(Directory runtimeDir) async {\n final paths = <String>[];\n final weChatPluginDir = Directory(\n p.join(runtimeDir.path, 'plugins', _weChatPluginId),\n );\n if (await File(\n p.join(weChatPluginDir.path, 'openclaw.plugin.json'),\n ).exists()) {\n paths.add(weChatPluginDir.path);\n }\n return paths;\n }\n\n Future<Directory> _ensureDesktopToolsPlugin({\n required Directory stateRoot,\n }) async {\n final dir = Directory(\n p.join(stateRoot.path, 'plugins', _desktopToolsPluginId),\n );\n await dir.create(recursive: true);\n final tools = DesktopControlPolicy.supportedTools.toList()..sort();\n await File(p.join(dir.path, 'package.json')).writeAsString(\n const JsonEncoder.withIndent(' ').convert({\n 'name': '@dacheng/openclaw-desktop-tools',\n 'version': '1.0.0',\n 'type': 'module',\n 'openclaw': {\n 'extensions': ['./index.mjs'],\n },\n }),\n );\n await File(p.join(dir.path, 'openclaw.plugin.json')).writeAsString(\n const JsonEncoder.withIndent(' ').convert({\n 'id': _desktopToolsPluginId,\n 'name': 'Dacheng Desktop Tools',\n 'description':\n 'Expose Fabushi desktop and Chrome control bridge tools to embedded OpenClaw.',\n 'activation': {'onStartup': true},\n 'contracts': {'tools': tools},\n 'configSchema': {\n 'type': 'object',\n 'additionalProperties': false,\n 'properties': <String, dynamic>{},\n },\n }),\n );\n await File(\n p.join(dir.path, 'index.mjs'),\n ).writeAsString(_desktopToolsPluginSource(tools));\n _diag(\n 'desktop-tools.plugin-written',\n data: {'pluginDir': dir.path, 'tools': tools},\n );\n return dir;\n }\n\n String _desktopToolsPluginSource(List<String> tools) {\n final toolsJson = jsonEncode(tools);\n return '''\nconst tools = $toolsJson;\n\nconst descriptions = {\n \"desktop.observe\": \"Observe the active desktop application and visible windows.\",\n \"desktop.screenshot\": \"Capture a screenshot of the local desktop.\",\n \"desktop.windows\": \"List visible desktop windows.\",\n \"desktop.click\": \"Click a local desktop coordinate after user confirmation.\",\n \"desktop.type\": \"Type text into the focused local desktop app after user confirmation.\",\n \"desktop.hotkey\": \"Send a keyboard shortcut to the local desktop after user confirmation.\",\n \"desktop.scroll\": \"Scroll the local desktop after user confirmation.\",\n \"chrome.tabs\": \"List tabs from the paired Chrome connector.\",\n \"chrome.navigate\": \"Navigate the active or selected Chrome tab after user confirmation.\",\n \"chrome.dom_snapshot\": \"Read a structured DOM snapshot from the paired Chrome connector.\",\n \"chrome.screenshot\": \"Capture a screenshot from the paired Chrome connector.\",\n \"chrome.click\": \"Click an element or point in the paired Chrome tab after user confirmation.\",\n \"chrome.type\": \"Type into an element in the paired Chrome tab after user confirmation.\"\n};\n\nconst parameterSchemas = {\n \"desktop.click\": {\n type: \"object\",\n additionalProperties: true,\n properties: {\n x: { type: \"number\" },\n y: { type: \"number\" },\n button: { type: \"string\", enum: [\"left\", \"right\"] }\n },\n required: [\"x\", \"y\"]\n },\n \"desktop.type\": {\n type: \"object\",\n additionalProperties: true,\n properties: { text: { type: \"string\" } },\n required: [\"text\"]\n },\n \"desktop.hotkey\": {\n type: \"object\",\n additionalProperties: true,\n properties: {\n key: { type: \"string\" },\n keys: { type: \"array\", items: { type: \"string\" } }\n }\n },\n \"chrome.navigate\": {\n type: \"object\",\n additionalProperties: true,\n properties: { url: { type: \"string\" }, tabId: { type: \"integer\" } },\n required: [\"url\"]\n },\n \"chrome.click\": {\n type: \"object\",\n additionalProperties: true,\n properties: {\n selector: { type: \"string\" },\n x: { type: \"number\" },\n y: { type: \"number\" },\n tabId: { type: \"integer\" }\n }\n },\n \"chrome.type\": {\n type: \"object\",\n additionalProperties: true,\n properties: {\n selector: { type: \"string\" },\n x: { type: \"number\" },\n y: { type: \"number\" },\n text: { type: \"string\" },\n tabId: { type: \"integer\" }\n },\n required: [\"text\"]\n }\n};\n\nconst defaultSchema = {\n type: \"object\",\n additionalProperties: true,\n properties: {}\n};\n\nfunction toolResultText(value) {\n try {\n return JSON.stringify(value, null, 2);\n } catch {\n return String(value);\n }\n}\n\nasync function callBridge(toolName, args) {\n if (process.env.DACHENG_DESKTOP_TOOLS_ENABLED !== \"1\") {\n return {\n isError: true,\n content: [{ type: \"text\", text: \"Fabushi desktop tools bridge is not running.\" }]\n };\n }\n const rawBaseUrl = process.env.DACHENG_DESKTOP_TOOLS_URL || \"\";\n const baseUrl = rawBaseUrl.endsWith(\"/\") ? rawBaseUrl.slice(0, -1) : rawBaseUrl;\n const token = process.env.DACHENG_DESKTOP_TOOLS_TOKEN || \"\";\n if (!baseUrl || !token) {\n return {\n isError: true,\n content: [{ type: \"text\", text: \"Fabushi desktop tools bridge URL/token is missing.\" }]\n };\n }\n const response = await fetch(`\\${baseUrl}/v1/tools/execute`, {\n method: \"POST\",\n headers: {\n \"Authorization\": `Bearer \\${token}`,\n \"Content-Type\": \"application/json\"\n },\n body: JSON.stringify({ tool: toolName, arguments: args || {} })\n });\n const payload = await response.json().catch(() => ({}));\n const ok = response.ok && payload && payload.ok !== false;\n const data = payload && Object.prototype.hasOwnProperty.call(payload, \"data\")\n ? payload.data\n : payload;\n return {\n isError: !ok,\n content: [{ type: \"text\", text: toolResultText(data) }],\n details: data\n };\n}\n\nexport default {\n id: \"dacheng-desktop-tools\",\n name: \"Dacheng Desktop Tools\",\n description: \"Expose Fabushi desktop and Chrome control tools to embedded OpenClaw.\",\n configSchema: { type: \"object\", additionalProperties: false, properties: {} },\n register(api) {\n for (const name of tools) {\n api.registerTool({\n name,\n label: name,\n description: descriptions[name] || `Run \\${name} through the Fabushi desktop bridge.`,\n parameters: parameterSchemas[name] || defaultSchema,\n async execute(_toolCallId, args) {\n return await callBridge(name, args);\n }\n });\n }\n }\n};\n''';\n }\n\n Future<Map<String, dynamic>> _mergeEmbeddedConfig(\n File configPath,\n Map<String, dynamic> defaults,\n ) async {\n Map<String, dynamic> current = <String, dynamic>{};\n if (await configPath.exists()) {\n try {\n final decoded = jsonDecode(await configPath.readAsString());\n if (decoded is Map<String, dynamic>) {\n current = decoded;\n } else if (decoded is Map) {\n current = Map<String, dynamic>.from(decoded);\n }\n } catch (_) {\n current = <String, dynamic>{};\n }\n }\n\n final gateway = _mutableMap(current['gateway']);\n final defaultGateway = _mutableMap(defaults['gateway']);\n gateway['mode'] = 'local';\n gateway['port'] = defaultGateway['port'];\n gateway['bind'] = defaultGateway['bind'];\n gateway['auth'] = Map<String, dynamic>.from(defaultGateway['auth'] as Map);\n if (defaultGateway.containsKey('remote')) {\n gateway['remote'] = {\n ..._mutableMap(gateway['remote']),\n ..._mutableMap(defaultGateway['remote']),\n };\n } else {\n final remote = _mutableMap(gateway['remote'])..remove('url');\n if (remote.isEmpty) {\n gateway.remove('remote');\n } else {\n gateway['remote'] = remote;\n }\n }\n\n final gatewayHttp = _mutableMap(gateway['http']);\n final endpoints = _mutableMap(gatewayHttp['endpoints']);\n endpoints['chatCompletions'] = {'enabled': true};\n endpoints['responses'] = {'enabled': true};\n gatewayHttp['endpoints'] = endpoints;\n gateway['http'] = gatewayHttp;\n current['gateway'] = gateway;\n\n final env = _mutableMap(current['env']);\n env.remove('DEEPSEEK_API_KEY');\n if (env.isEmpty) {\n current.remove('env');\n } else {\n current['env'] = env;\n }\n\n final defaultModels = _mutableMap(defaults['models']);\n final models = _mutableMap(current['models']);\n models['mode'] ??= defaultModels['mode'];\n final providers = _mutableMap(models['providers']);\n final defaultProviders = _mutableMap(defaultModels['providers']);\n providers.remove('deepseek');\n providers.remove('dacheng-deepseek');\n providers.remove(_backendDeepSeekProviderId);\n for (final entry in defaultProviders.entries) {\n providers[entry.key] = entry.value is Map\n ? Map<String, dynamic>.from(entry.value as Map)\n : entry.value;\n }\n models['providers'] = providers;\n current['models'] = models;\n\n final defaultBrowser = _mutableMap(defaults['browser']);\n final browser = _mutableMap(current['browser']);\n browser.addAll(defaultBrowser);\n current['browser'] = browser;\n\n // OpenClaw 2026.6 rejects root-level canvas/canvasHost. Canvas is now\n // configured through plugins.entries.canvas.config.host.\n current.remove('canvas');\n current.remove('canvasHost');\n\n final defaultTools = _mutableMap(defaults['tools']);\n final tools = _mutableMap(current['tools']);\n tools.remove('toolSearch');\n tools.remove('fs');\n final alsoAllow = _mergeStringLists(\n tools['alsoAllow'],\n defaultTools['alsoAllow'],\n );\n final execTools = _mutableMap(tools['exec']);\n execTools.remove('mode');\n if (execTools.isNotEmpty) {\n tools['exec'] = execTools;\n }\n tools.addAll(defaultTools);\n if (alsoAllow.isEmpty) {\n tools.remove('alsoAllow');\n } else {\n tools['alsoAllow'] = alsoAllow;\n }\n current['tools'] = tools;\n\n final defaultPlugins = _mutableMap(defaults['plugins']);\n final plugins = _mutableMap(current['plugins']);\n plugins['enabled'] = true;\n final defaultEntries = _mutableMap(defaultPlugins['entries']);\n final requiredPluginIds = defaultEntries.keys.toSet();\n final allowList = _stringList(plugins['allow']);\n if (allowList.isEmpty) {\n plugins.remove('allow');\n } else {\n plugins['allow'] = _mergeStringLists(allowList, requiredPluginIds);\n }\n final denyList = _stringList(\n plugins['deny'],\n ).where((item) => !requiredPluginIds.contains(item)).toList();\n if (denyList.isEmpty) {\n plugins.remove('deny');\n } else {\n plugins['deny'] = denyList;\n }\n final defaultLoad = _mutableMap(defaultPlugins['load']);\n final load = _mutableMap(plugins['load']);\n load['paths'] = _mergeStringLists(load['paths'], defaultLoad['paths']);\n plugins['load'] = load;\n final slots = _mutableMap(plugins['slots']);\n slots.addAll(_mutableMap(defaultPlugins['slots']));\n plugins['slots'] = slots;\n final entries = _mutableMap(plugins['entries']);\n for (final entry in defaultEntries.entries) {\n final currentEntry = _mutableMap(entries[entry.key]);\n currentEntry.addAll(\n entry.value is Map\n ? Map<String, dynamic>.from(entry.value as Map)\n : {'enabled': true},\n );\n entries[entry.key] = currentEntry;\n }\n plugins['entries'] = entries;\n current['plugins'] = plugins;\n\n final defaultChannels = _mutableMap(defaults['channels']);\n final channels = _mutableMap(current['channels']);\n final defaultChannelDefaults = _mutableMap(defaultChannels['defaults']);\n if (defaultChannelDefaults.isNotEmpty) {\n channels['defaults'] = {\n ..._mutableMap(channels['defaults']),\n ...defaultChannelDefaults,\n };\n }\n for (final entry in defaultChannels.entries) {\n if (entry.key == 'defaults') continue;\n final currentChannel = _mutableMap(channels[entry.key]);\n currentChannel.addAll(\n entry.value is Map\n ? Map<String, dynamic>.from(entry.value as Map)\n : <String, dynamic>{},\n );\n channels[entry.key] = currentChannel;\n }\n current['channels'] = channels;\n\n final agents = _mutableMap(current['agents']);\n final defaultAgents = _mutableMap(defaults['agents']);\n final defaultAgentDefaults = _mutableMap(defaultAgents['defaults']);\n final agentDefaults = _mutableMap(agents['defaults']);\n agentDefaults['workspace'] = defaultAgentDefaults['workspace'];\n agentDefaults['model'] = defaultAgentDefaults['model'];\n agents['defaults'] = agentDefaults;\n\n final defaultList = (defaultAgents['list'] as List? ?? const [])\n .whereType<Map>()\n .map((item) => Map<String, dynamic>.from(item))\n .toList();\n final defaultDacheng = defaultList.isNotEmpty\n ? defaultList.first\n : <String, dynamic>{\n 'id': 'dacheng',\n 'default': true,\n 'workspace': agentDefaults['workspace'],\n 'model': agentDefaults['model'],\n };\n final list = (agents['list'] as List? ?? const [])\n .map((item) => item is Map ? Map<String, dynamic>.from(item) : item)\n .toList();\n var foundDacheng = false;\n for (var i = 0; i < list.length; i++) {\n final item = list[i];\n if (item is! Map<String, dynamic>) continue;\n if (item['id'] != 'dacheng') continue;\n foundDacheng = true;\n item['default'] = true;\n item['workspace'] = defaultDacheng['workspace'];\n item['model'] = defaultDacheng['model'];\n list[i] = item;\n }\n if (!foundDacheng) {\n list.add(defaultDacheng);\n }\n agents['list'] = list.isEmpty ? defaultList : list;\n current['agents'] = agents;\n\n return current;\n }\n\n @visibleForTesting\n Future<Map<String, dynamic>> mergeEmbeddedConfigForTest(\n File configPath,\n Map<String, dynamic> defaults,\n ) {\n return _mergeEmbeddedConfig(configPath, defaults);\n }\n\n String _gatewayChatModel(String model) {\n final trimmed = model.trim();\n if (trimmed == 'openclaw' || trimmed.startsWith('openclaw/')) {\n return trimmed;\n }\n return _defaultGatewayModel;\n }\n\n String _defaultDeepSeekModelFor(String model) {\n final trimmed = model.trim();\n if (trimmed.startsWith('deepseek/')) return trimmed;\n return _defaultDeepSeekModel;\n }\n\n String _backendDeepSeekModelRef(String model) {\n final trimmed = model.trim().isEmpty ? _defaultDeepSeekModel : model.trim();\n final modelId = trimmed.contains('/')\n ? trimmed.split('/').last.trim()\n : trimmed;\n final normalizedModelId = modelId.isEmpty ? 'deepseek-chat' : modelId;\n return '$_backendDeepSeekProviderId/$normalizedModelId';\n }\n\n String _deepSeekProxyBaseUrl() {\n final baseUrl = AppConfig.currentAiBackendUrl.replaceFirst(\n RegExp(r'/+$'),\n '',\n );\n return '$baseUrl/api/openclaw/deepseek/v1';\n }\n\n Map<String, dynamic> _mutableMap(Object? value) {\n if (value is Map<String, dynamic>) return Map<String, dynamic>.from(value);\n if (value is Map) return Map<String, dynamic>.from(value);\n return <String, dynamic>{};\n }\n\n List<String> _stringList(Object? value) {\n if (value is! Iterable) return <String>[];\n return value\n .map((item) => item.toString().trim())\n .where((item) => item.isNotEmpty)\n .toList();\n }\n\n List<String> _mergeStringLists(Object? first, Object? second) {\n final merged = <String>{};\n merged.addAll(_stringList(first));\n merged.addAll(_stringList(second));\n return merged.toList();\n }\n\n void _appendBounded(StringBuffer buffer, String chunk) {\n const maxChars = 120000;\n if (chunk.isEmpty || buffer.length >= maxChars) return;\n final remaining = maxChars - buffer.length;\n buffer.write(\n chunk.length <= remaining ? chunk : chunk.substring(0, remaining),\n );\n }\n\n void _captureLogs(Process process) {\n void addLog(String line) {\n final text = line.trim();\n if (text.isEmpty) return;\n _recentLogs.insert(0, text);\n if (_recentLogs.length > 80) {\n _recentLogs.removeRange(80, _recentLogs.length);\n }\n if (kDebugMode) {\n debugPrint('[OpenClaw] $text');\n }\n _diag('process.output', data: {'line': text});\n }\n\n process.stdout\n .transform(utf8.decoder)\n .transform(const LineSplitter())\n .listen(addLog, onError: (_) {});\n process.stderr\n .transform(utf8.decoder)\n .transform(const LineSplitter())\n .listen(addLog, onError: (_) {});\n }\n\n Future<Directory> _stateRoot() async {\n final support = await getApplicationSupportDirectory();\n final dir = Directory(p.join(support.path, 'openclaw_embedded'));\n await dir.create(recursive: true);\n return dir;\n }\n\n Future<_DesktopToolsLaunch> _ensureDesktopTools() async {\n try {\n final status = await DesktopControlBridge.instance.ensureStarted();\n _diag(\n 'desktop-tools.started',\n data: {\n 'bridgeRunning': status.bridgeRunning,\n 'bridgeUri': status.bridgeUri?.toString(),\n 'message': status.message,\n },\n );\n return _DesktopToolsLaunch(\n uri: status.bridgeUri,\n token: await DesktopControlBridge.instance.bridgeToken,\n statusJson: status.toJson(),\n );\n } catch (error) {\n _diag('desktop-tools.error', error: error);\n return _DesktopToolsLaunch(\n uri: null,\n token: null,\n statusJson: {\n 'enabledByBuild': false,\n 'bridgeRunning': false,\n 'message': error.toString(),\n },\n );\n }\n }\n\n Future<Directory> _runtimeDir(\n _OpenClawBundleSpec spec,\n String platformKey,\n ) async {\n final support = await getApplicationSupportDirectory();\n final runtimeBase = Platform.isMacOS\n ? Directory(\n p.join(\n support.parent.path,\n '${p.basename(support.path)}.openclaw_runtime',\n ),\n )\n : Directory(p.join(support.path, 'openclaw_embedded'));\n return Directory(\n p.join(runtimeBase.path, 'runtime', spec.version, platformKey),\n );\n }\n\n Future<Directory?> _bundledRuntimeDir(\n _OpenClawBundleSpec spec,\n String platformKey,\n ) async {\n if (!Platform.isMacOS) return null;\n\n final contentsDir = Directory(\n p.dirname(p.dirname(Platform.resolvedExecutable)),\n );\n final candidates = <Directory>[\n Directory(\n p.join(\n contentsDir.path,\n 'Frameworks',\n 'App.framework',\n 'Resources',\n 'flutter_assets',\n 'assets',\n 'openclaw',\n platformKey,\n ),\n ),\n Directory(\n p.join(\n contentsDir.path,\n 'Frameworks',\n 'App.framework',\n 'Versions',\n 'A',\n 'Resources',\n 'flutter_assets',\n 'assets',\n 'openclaw',\n platformKey,\n ),\n ),\n Directory(\n p.join(\n contentsDir.path,\n 'Resources',\n 'flutter_assets',\n 'assets',\n 'openclaw',\n platformKey,\n ),\n ),\n ];\n\n for (final dir in candidates) {\n final nodePath = File(p.join(dir.path, spec.nodeExecutable));\n final cliPath = File(p.join(dir.path, spec.cliEntrypoint));\n if (await nodePath.exists() && await cliPath.exists()) {\n return dir;\n }\n }\n _diag(\n 'bundle.macos-bundled-runtime-missing',\n data: {\n 'platformKey': platformKey,\n 'contentsDir': contentsDir.path,\n 'candidates': candidates.map((item) => item.path).toList(),\n },\n );\n return null;\n }\n\n Future<Directory?> _runtimeDirOverride(\n _OpenClawBundleSpec spec,\n String platformKey,\n ) async {\n final raw = _configuredRuntimeDirOverride(platformKey);\n if (raw == null) return null;\n\n final dir = Directory(raw);\n final nodePath = File(p.join(dir.path, spec.nodeExecutable));\n final cliPath = File(p.join(dir.path, spec.cliEntrypoint));\n final nodeExists = await nodePath.exists();\n final cliExists = await cliPath.exists();\n _diag(\n 'bundle.override.paths',\n data: {\n 'platformKey': platformKey,\n 'runtimeDir': dir.path,\n 'nodePath': nodePath.path,\n 'nodeExists': nodeExists,\n 'cliPath': cliPath.path,\n 'cliExists': cliExists,\n },\n );\n\n if (!nodeExists || !cliExists) {\n throw OpenClawRuntimeException(\n 'DACHENG_OPENCLAW_RUNTIME_DIR 指向的 OpenClaw runtime 不完整: ${dir.path}',\n );\n }\n return dir;\n }\n\n String? _configuredRuntimeDirOverride(String platformKey) {\n final raw = _runtimeDirOverrideDefine.trim().isNotEmpty\n ? _runtimeDirOverrideDefine.trim()\n : Platform.environment['DACHENG_OPENCLAW_RUNTIME_DIR']?.trim();\n if (raw == null || raw.isEmpty) return null;\n return p.normalize(raw.replaceAll('{platform}', platformKey));\n }\n\n Future<Directory> _legacyRuntimeDir(\n _OpenClawBundleSpec spec,\n String platformKey,\n ) async {\n final support = await getApplicationSupportDirectory();\n return Directory(\n p.join(\n support.path,\n 'openclaw_embedded',\n 'runtime',\n spec.version,\n platformKey,\n ),\n );\n }\n\n OpenClawRuntimeStatus _remember(OpenClawRuntimeStatus status) {\n _lastStatus = status;\n return status;\n }\n\n String? get _platformKey {\n final abi = Abi.current().toString().toLowerCase();\n if (Platform.isMacOS) {\n return abi.contains('arm64') ? 'macos-arm64' : 'macos-x64';\n }\n if (Platform.isWindows) {\n return abi.contains('arm64') ? 'windows-arm64' : 'windows-x64';\n }\n if (Platform.isLinux) {\n return abi.contains('arm64') ? 'linux-arm64' : 'linux-x64';\n }\n return null;\n }\n\n OpenClawRuntimeStatus? get lastStatus => _lastStatus;\n\n void _diag(\n String message, {\n Map<String, Object?> data = const {},\n Object? error,\n StackTrace? stackTrace,\n }) {\n unawaited(\n DiagnosticLogService.instance.log(\n 'openclaw.runtime',\n message,\n data: data,\n error: error,\n stackTrace: stackTrace,\n ),\n );\n }\n}\n\nint? _readInt(Object? value) {\n if (value is int) return value;\n if (value is num) return value.toInt();\n if (value is String) return int.tryParse(value);\n return null;\n}\n\nObject? _decodeJsonOrNull(String body) {\n try {\n return jsonDecode(body);\n } catch (_) {\n return null;\n }\n}\n\nbool _looksLikeHealthPayload(Object? payload) {\n if (payload is! Map) return false;\n if (payload['ok'] == true) return true;\n final status = payload['status']?.toString().toLowerCase();\n return status == 'live' || status == 'ok' || status == 'healthy';\n}\n\nbool _looksLikeModelsPayload(Object? payload) {\n if (payload is List) return true;\n if (payload is! Map) return false;\n return payload['data'] is List ||\n payload['models'] is List ||\n payload['object'] == 'list';\n}\n\nString _previewBody(String body) {\n final trimmed = body.trim();\n return trimmed.length <= 80 ? trimmed : trimmed.substring(0, 80);\n}\n\nString _previewProcessOutput(Object? output) {\n final text = output?.toString().trim() ?? '';\n return text.length <= 200 ? text : text.substring(0, 200);\n}\n"}, "4a4ffc73bacebe78769ca7d483f7f446e16f5cce": {"t": 1785757955.5960627, "n": 94233, "h": "46dc1952965e83c7", "c": "import 'dart:async';\nimport 'dart:convert';\nimport 'dart:ffi' show Abi;\nimport 'dart:io';\nimport 'package:archive/archive_io.dart';\nimport 'package:convert/convert.dart';\nimport 'package:crypto/crypto.dart';\nimport 'package:flutter/foundation.dart';\nimport 'package:flutter/services.dart';\nimport 'package:http/http.dart' as http;\nimport 'package:path/path.dart' as p;\nimport 'package:path_provider/path_provider.dart';\n\nimport '../../core/config/app_config.dart';\nimport '../app_settings.dart';\nimport '../diagnostic_log_service.dart';\nimport '../desktop_control/desktop_control_bridge.dart';\nimport '../desktop_control/desktop_control_policy.dart';\nimport 'openclaw_port_resolver.dart';\n\nenum OpenClawRuntimeState {\n unsupported,\n notBundled,\n stopped,\n starting,\n running,\n failed,\n}\n\nclass OpenClawRuntimeStatus {\n final OpenClawRuntimeState state;\n final String message;\n final int? port;\n final String? platformKey;\n final String? runtimePath;\n final Map<String, dynamic>? desktopToolsStatus;\n final DateTime checkedAt;\n\n const OpenClawRuntimeStatus({\n required this.state,\n required this.message,\n this.port,\n this.platformKey,\n this.runtimePath,\n this.desktopToolsStatus,\n required this.checkedAt,\n });\n\n bool get isHealthy => state == OpenClawRuntimeState.running;\n\n String get label {\n switch (state) {\n case OpenClawRuntimeState.unsupported:\n return '不支持';\n case OpenClawRuntimeState.notBundled:\n return '未内置';\n case OpenClawRuntimeState.stopped:\n return '未启动';\n case OpenClawRuntimeState.starting:\n return '启动中';\n case OpenClawRuntimeState.running:\n return '运行中';\n case OpenClawRuntimeState.failed:\n return '异常';\n }\n }\n}\n\nclass OpenClawGatewayTarget {\n final Uri baseUri;\n final String token;\n final String model;\n final String? modelOverride;\n final Uri? desktopToolsUri;\n final String? desktopToolsToken;\n final Map<String, dynamic>? desktopToolsStatus;\n\n const OpenClawGatewayTarget({\n required this.baseUri,\n required this.token,\n required this.model,\n this.modelOverride,\n this.desktopToolsUri,\n this.desktopToolsToken,\n this.desktopToolsStatus,\n });\n}\n\nclass OpenClawCliResult {\n final List<String> args;\n final int exitCode;\n final String stdout;\n final String stderr;\n final bool timedOut;\n\n const OpenClawCliResult({\n required this.args,\n required this.exitCode,\n required this.stdout,\n required this.stderr,\n this.timedOut = false,\n });\n\n bool get succeeded => exitCode == 0 && !timedOut;\n\n String get command => 'openclaw ${args.join(' ')}';\n\n String get combinedOutput {\n final parts = <String>[\n if (stdout.trim().isNotEmpty) stdout.trimRight(),\n if (stderr.trim().isNotEmpty) stderr.trimRight(),\n ];\n return parts.join('\\n');\n }\n\n OpenClawCliResult append(OpenClawCliResult next) {\n return OpenClawCliResult(\n args: [...args, '&&', ...next.args],\n exitCode: next.exitCode,\n stdout: [\n if (stdout.trim().isNotEmpty) stdout.trimRight(),\n if (next.stdout.trim().isNotEmpty) next.stdout.trimRight(),\n ].join('\\n'),\n stderr: [\n if (stderr.trim().isNotEmpty) stderr.trimRight(),\n if (next.stderr.trim().isNotEmpty) next.stderr.trimRight(),\n ].join('\\n'),\n timedOut: timedOut || next.timedOut,\n );\n }\n}\n\nclass _OpenClawBundleSpec {\n final String version;\n final int defaultPort;\n final String defaultModel;\n final String? defaultModelOverride;\n final String nodeExecutable;\n final String cliEntrypoint;\n final List<String> gatewayArgs;\n final bool downloaded;\n\n const _OpenClawBundleSpec({\n required this.version,\n required this.defaultPort,\n required this.defaultModel,\n this.defaultModelOverride,\n required this.nodeExecutable,\n required this.cliEntrypoint,\n required this.gatewayArgs,\n this.downloaded = false,\n });\n\n Map<String, dynamic> toJson() => {\n 'schema': 1,\n 'version': version,\n 'defaultPort': defaultPort,\n 'defaultModel': defaultModel,\n if (defaultModelOverride != null)\n 'defaultModelOverride': defaultModelOverride,\n 'nodeExecutable': nodeExecutable,\n 'cliEntrypoint': cliEntrypoint,\n 'gatewayArgs': gatewayArgs,\n 'downloaded': downloaded,\n };\n}\n\nclass _OpenClawRuntimeRelease {\n final String version;\n final Uri archiveUri;\n final String sha256Hex;\n final int size;\n final int defaultPort;\n final String defaultModel;\n final String? defaultModelOverride;\n final String nodeExecutable;\n final String cliEntrypoint;\n final List<String> gatewayArgs;\n\n const _OpenClawRuntimeRelease({\n required this.version,\n required this.archiveUri,\n required this.sha256Hex,\n required this.size,\n required this.defaultPort,\n required this.defaultModel,\n this.defaultModelOverride,\n required this.nodeExecutable,\n required this.cliEntrypoint,\n required this.gatewayArgs,\n });\n\n _OpenClawBundleSpec toBundleSpec() => _OpenClawBundleSpec(\n version: version,\n defaultPort: defaultPort,\n defaultModel: defaultModel,\n defaultModelOverride: defaultModelOverride,\n nodeExecutable: nodeExecutable,\n cliEntrypoint: cliEntrypoint,\n gatewayArgs: gatewayArgs,\n downloaded: true,\n );\n}\n\nclass _DesktopToolsLaunch {\n final Uri? uri;\n final String? token;\n final Map<String, dynamic> statusJson;\n\n const _DesktopToolsLaunch({\n required this.uri,\n required this.token,\n required this.statusJson,\n });\n}\n\nclass _OpenClawCliLaunch {\n final _OpenClawBundleSpec spec;\n final Directory runtimeDir;\n final Directory stateRoot;\n final File configPath;\n final String nodePath;\n final String cliPath;\n final int port;\n final String token;\n\n const _OpenClawCliLaunch({\n required this.spec,\n required this.runtimeDir,\n required this.stateRoot,\n required this.configPath,\n required this.nodePath,\n required this.cliPath,\n required this.port,\n required this.token,\n });\n}\n\nclass OpenClawRuntimeException implements Exception {\n final String message;\n\n const OpenClawRuntimeException(this.message);\n\n @override\n String toString() => message;\n}\n\nclass OpenClawRuntime {\n OpenClawRuntime._();\n\n static final OpenClawRuntime instance = OpenClawRuntime._();\n\n static const String _manifestAsset = 'assets/openclaw/bundle_manifest.json';\n static const String _runtimeDirOverrideDefine = String.fromEnvironment(\n 'DACHENG_OPENCLAW_RUNTIME_DIR',\n );\n static const String _defaultGatewayModel =\n AppSettings.defaultOpenClawGatewayModel;\n static const String _defaultDeepSeekModel =\n AppSettings.defaultOpenClawDeepSeekModel;\n static const String _backendDeepSeekProviderId = 'dacheng-deepseek-proxy';\n static const String _desktopToolsPluginId = 'dacheng-desktop-tools';\n static const String _weChatPluginId = 'openclaw-weixin';\n static const String _weChatPluginPackage = '@tencent-weixin/openclaw-weixin';\n static const Duration _startupTimeout = Duration(seconds: 120);\n static const Duration _probeTimeout = Duration(seconds: 3);\n static const Duration _cliDefaultTimeout = Duration(seconds: 45);\n\n Process? _process;\n Future<OpenClawGatewayTarget>? _starting;\n String _processAuthToken = '';\n OpenClawRuntimeStatus? _lastStatus;\n final List<String> _recentLogs = <String>[];\n final OpenClawPortResolver _portResolver = const OpenClawPortResolver();\n\n Future<OpenClawRuntimeStatus> getStatus({bool probe = true}) async {\n final platformKey = _platformKey;\n final port = await AppSettings.getOpenClawGatewayPort();\n _diag(\n 'status.start',\n data: {'platformKey': platformKey, 'port': port, 'probe': probe},\n );\n\n if (platformKey == null) {\n return _remember(\n OpenClawRuntimeStatus(\n state: OpenClawRuntimeState.unsupported,\n message: '当前平台不是 macOS / Windows / Linux 桌面端',\n port: port,\n checkedAt: DateTime.now(),\n ),\n );\n }\n\n try {\n final spec = await _loadSpec(platformKey, checkUpdates: false);\n final overrideRuntimeDir = await _runtimeDirOverride(spec, platformKey);\n final bundledRuntimeDir = overrideRuntimeDir == null\n ? await _bundledRuntimeDir(spec, platformKey)\n : null;\n final runtimeDir =\n overrideRuntimeDir ??\n bundledRuntimeDir ??\n await _runtimeDir(spec, platformKey);\n final nodePath = p.join(runtimeDir.path, spec.nodeExecutable);\n final cliPath = p.join(runtimeDir.path, spec.cliEntrypoint);\n final nodeExists = await File(nodePath).exists();\n final cliExists = await File(cliPath).exists();\n _diag(\n 'status.paths',\n data: {\n 'platformKey': platformKey,\n 'runtimeDir': runtimeDir.path,\n 'nodePath': nodePath,\n 'nodeExists': nodeExists,\n 'cliPath': cliPath,\n 'cliExists': cliExists,\n 'runtimeSource': overrideRuntimeDir != null\n ? 'override'\n : (bundledRuntimeDir == null ? 'cache' : 'bundle'),\n },\n );\n\n if (!nodeExists || !cliExists) {\n final hasBundleAssets = await _hasRequiredBundleAssets(\n platformKey,\n spec,\n );\n _diag(\n 'status.bundle-assets',\n data: {\n 'platformKey': platformKey,\n 'hasBundleAssets': hasBundleAssets,\n 'nodeAsset': 'assets/openclaw/$platformKey/${spec.nodeExecutable}',\n 'cliAsset': 'assets/openclaw/$platformKey/${spec.cliEntrypoint}',\n },\n );\n return _remember(\n OpenClawRuntimeStatus(\n state: hasBundleAssets\n ? OpenClawRuntimeState.stopped\n : OpenClawRuntimeState.notBundled,\n message: hasBundleAssets\n ? '内置 OpenClaw 已随 App 打包,尚未释放或启动'\n : '当前 App 包未包含 $platformKey 的 OpenClaw runtime',\n port: port,\n platformKey: platformKey,\n runtimePath: runtimeDir.path,\n checkedAt: DateTime.now(),\n ),\n );\n }\n\n if (probe) {\n final healthy = await _probe(\n port,\n await AppSettings.getOpenClawGatewayToken(),\n );\n if (healthy) {\n return _remember(\n OpenClawRuntimeStatus(\n state: OpenClawRuntimeState.running,\n message: '本机 OpenClaw Gateway 正在运行',\n port: port,\n platformKey: platformKey,\n runtimePath: runtimeDir.path,\n checkedAt: DateTime.now(),\n ),\n );\n }\n }\n\n return _remember(\n OpenClawRuntimeStatus(\n state: _process == null\n ? OpenClawRuntimeState.stopped\n : OpenClawRuntimeState.starting,\n message: _process == null ? '本机 OpenClaw 未启动' : '本机 OpenClaw 正在启动',\n port: port,\n platformKey: platformKey,\n runtimePath: runtimeDir.path,\n checkedAt: DateTime.now(),\n ),\n );\n } catch (error) {\n _diag('status.error', error: error);\n return _remember(\n OpenClawRuntimeStatus(\n state: OpenClawRuntimeState.failed,\n message: error.toString(),\n port: port,\n platformKey: platformKey,\n checkedAt: DateTime.now(),\n ),\n );\n }\n }\n\n Future<OpenClawGatewayTarget> ensureStarted({\n String? authToken,\n String? username,\n bool isMember = false,\n }) {\n _starting ??= _ensureStartedInternal(\n authToken: authToken,\n username: username,\n isMember: isMember,\n ).whenComplete(() => _starting = null);\n return _starting!;\n }\n\n Future<OpenClawRuntimeStatus> restart() async {\n await stop();\n try {\n await ensureStarted();\n return getStatus();\n } catch (error) {\n return _remember(\n OpenClawRuntimeStatus(\n state: OpenClawRuntimeState.failed,\n message: error.toString(),\n port: await AppSettings.getOpenClawGatewayPort(),\n platformKey: _platformKey,\n checkedAt: DateTime.now(),\n ),\n );\n }\n }\n\n Future<void> stop() async {\n final process = _process;\n _process = null;\n _processAuthToken = '';\n if (process == null) return;\n process.kill();\n try {\n await process.exitCode.timeout(const Duration(seconds: 5));\n } catch (_) {\n process.kill();\n }\n }\n\n Future<OpenClawCliResult> createMobilePairingCode({bool remote = true}) {\n return runCli([\n 'qr',\n if (remote) '--remote',\n '--json',\n ], timeout: const Duration(seconds: 30));\n }\n\n Future<OpenClawCliResult> loginWeChat() {\n return runCli([\n 'channels',\n 'login',\n '--channel',\n _weChatPluginId,\n ], timeout: const Duration(minutes: 3));\n }\n\n Future<OpenClawCliResult> inspectChannels() {\n return runCli([\n 'channels',\n 'status',\n '--probe',\n ], timeout: const Duration(seconds: 40));\n }\n\n Future<OpenClawCliResult> installWeChatPlugin() async {\n final install = await runCli(\n ['plugins', 'install', _weChatPluginPackage, '--force'],\n timeout: const Duration(minutes: 3),\n ensureGateway: false,\n );\n if (!install.succeeded) return install;\n final enable = await runCli(\n ['config', 'set', 'plugins.entries.$_weChatPluginId.enabled', 'true'],\n timeout: const Duration(seconds: 30),\n ensureGateway: false,\n );\n return install.append(enable);\n }\n\n Future<OpenClawCliResult> runCli(\n List<String> args, {\n Duration timeout = _cliDefaultTimeout,\n bool ensureGateway = true,\n }) async {\n if (args.isEmpty) {\n throw const OpenClawRuntimeException('OpenClaw CLI 参数不能为空');\n }\n final launch = await _prepareCliLaunch(ensureGateway: ensureGateway);\n final processArgs = <String>[launch.cliPath, ...args];\n _diag(\n 'cli.start',\n data: {\n 'args': args,\n 'runtimeDir': launch.runtimeDir.path,\n 'configPath': launch.configPath.path,\n 'timeoutSeconds': timeout.inSeconds,\n },\n );\n\n final process = await Process.start(\n launch.nodePath,\n processArgs,\n workingDirectory: launch.runtimeDir.path,\n environment: _buildOpenClawEnvironment(\n runtimeDir: launch.runtimeDir,\n stateRoot: launch.stateRoot,\n configPath: launch.configPath,\n port: launch.port,\n token: launch.token,\n ),\n mode: ProcessStartMode.normal,\n runInShell: false,\n );\n final stdoutBuffer = StringBuffer();\n final stderrBuffer = StringBuffer();\n final stdoutSub = process.stdout\n .transform(utf8.decoder)\n .listen((chunk) => _appendBounded(stdoutBuffer, chunk));\n final stderrSub = process.stderr\n .transform(utf8.decoder)\n .listen((chunk) => _appendBounded(stderrBuffer, chunk));\n\n var timedOut = false;\n int exitCode;\n try {\n exitCode = await process.exitCode.timeout(timeout);\n } on TimeoutException {\n timedOut = true;\n process.kill();\n exitCode = await process.exitCode.timeout(\n const Duration(seconds: 5),\n onTimeout: () => -1,\n );\n } finally {\n await stdoutSub.cancel();\n await stderrSub.cancel();\n }\n\n final result = OpenClawCliResult(\n args: args,\n exitCode: exitCode,\n stdout: stdoutBuffer.toString(),\n stderr: stderrBuffer.toString(),\n timedOut: timedOut,\n );\n _diag(\n result.succeeded ? 'cli.complete' : 'cli.failed',\n data: {\n 'args': args,\n 'exitCode': exitCode,\n 'timedOut': timedOut,\n 'stdoutPreview': _previewProcessOutput(result.stdout),\n 'stderrPreview': _previewProcessOutput(result.stderr),\n },\n );\n return result;\n }\n\n Future<OpenClawGatewayTarget> _ensureStartedInternal({\n String? authToken,\n String? username,\n bool isMember = false,\n }) async {\n final platformKey = _platformKey;\n _diag('ensure-start.start', data: {'platformKey': platformKey});\n if (platformKey == null) {\n throw const OpenClawRuntimeException('当前平台不支持内置 OpenClaw Gateway');\n }\n\n final spec = await _loadSpec(platformKey, checkUpdates: true);\n final requestedPort = await AppSettings.getOpenClawGatewayPort(\n defaultValue: spec.defaultPort,\n );\n final token = await AppSettings.getOpenClawGatewayToken();\n final savedModel = await AppSettings.getOpenClawModel(\n defaultValue: spec.defaultModel,\n );\n final model = _gatewayChatModel(savedModel);\n final requestedAuthToken = authToken?.trim() ?? '';\n final backendDeepSeekModel = _backendDeepSeekModelRef(\n await AppSettings.getOpenClawDeepSeekModel(\n defaultValue: _defaultDeepSeekModelFor(spec.defaultModel),\n ),\n );\n final deepSeekProxyBaseUrl = _deepSeekProxyBaseUrl();\n final modelOverride = await AppSettings.getOpenClawModelOverride(\n defaultValue: spec.defaultModelOverride ?? '',\n );\n final remoteGatewayUrl = await AppSettings.getOpenClawRemoteGatewayUrl();\n final desktopTools = await _ensureDesktopTools();\n _diag(\n 'ensure-start.config',\n data: {\n 'platformKey': platformKey,\n 'requestedPort': requestedPort,\n 'model': model,\n 'rawModel': savedModel,\n 'backendDeepSeekModel': backendDeepSeekModel,\n 'deepSeekProxyBaseUrl': deepSeekProxyBaseUrl,\n 'hasAuthToken': requestedAuthToken.isNotEmpty,\n 'modelOverrideSet': modelOverride.trim().isNotEmpty,\n 'remoteGatewayUrlSet': remoteGatewayUrl.trim().isNotEmpty,\n 'desktopToolsUri': desktopTools.uri?.toString(),\n 'desktopToolsStatus': desktopTools.statusJson,\n },\n );\n\n final canReuseStartedProcess =\n _process != null && _processAuthToken == requestedAuthToken;\n if (canReuseStartedProcess &&\n await _probe(requestedPort, token, context: 'pre-start')) {\n _diag('ensure-start.reuse-existing', data: {'port': requestedPort});\n return OpenClawGatewayTarget(\n baseUri: Uri.parse('http://127.0.0.1:$requestedPort'),\n token: token,\n model: model,\n modelOverride: modelOverride.trim().isEmpty\n ? null\n : modelOverride.trim(),\n desktopToolsUri: desktopTools.uri,\n desktopToolsToken: desktopTools.token,\n desktopToolsStatus: desktopTools.statusJson,\n );\n } else if (_process != null) {\n _diag(\n 'ensure-start.restart-auth-changed',\n data: {\n 'hasPreviousAuthToken': _processAuthToken.isNotEmpty,\n 'hasRequestedAuthToken': requestedAuthToken.isNotEmpty,\n },\n );\n await stop();\n }\n\n final runtimeDir = await _prepareBundle(spec, platformKey);\n final stateRoot = await _stateRoot();\n final desktopToolsPluginDir = await _ensureDesktopToolsPlugin(\n stateRoot: stateRoot,\n );\n if (_process != null) {\n _diag(\n 'process.stop-stale-before-start',\n data: {'requestedPort': requestedPort},\n );\n await stop();\n }\n final portCandidate = await _portResolver.resolve(requestedPort);\n final port = portCandidate.port;\n if (portCandidate.isFallback) {\n await AppSettings.setOpenClawGatewayPort(port);\n _diag(\n 'ensure-start.fallback-port',\n data: {\n 'requestedPort': requestedPort,\n 'selectedPort': port,\n if (portCandidate.reason != null) 'reason': portCandidate.reason,\n },\n );\n } else {\n _diag(\n 'ensure-start.port-bindable',\n data: {'requestedPort': requestedPort, 'selectedPort': port},\n );\n }\n final configPath = await _ensureConfigFile(\n runtimeDir: runtimeDir,\n stateRoot: stateRoot,\n port: port,\n token: token,\n backendDeepSeekModel: backendDeepSeekModel,\n deepSeekProxyBaseUrl: deepSeekProxyBaseUrl,\n remoteGatewayUrl: remoteGatewayUrl,\n extraPluginLoadPaths: [desktopToolsPluginDir.path],\n );\n await _ensureAgentModelConfig(\n stateRoot: stateRoot,\n deepSeekProxyBaseUrl: deepSeekProxyBaseUrl,\n );\n final desktopToolsManifestPath = await _ensureDesktopToolsManifest(\n stateRoot: stateRoot,\n desktopTools: desktopTools,\n );\n\n final nodePath = p.join(runtimeDir.path, spec.nodeExecutable);\n final cliPath = p.join(runtimeDir.path, spec.cliEntrypoint);\n if (!await File(nodePath).exists()) {\n _diag('ensure-start.missing-node', data: {'nodePath': nodePath});\n throw OpenClawRuntimeException('OpenClaw 内置 Node 不存在: $nodePath');\n }\n if (!await File(cliPath).exists()) {\n _diag('ensure-start.missing-cli', data: {'cliPath': cliPath});\n throw OpenClawRuntimeException('OpenClaw CLI 入口不存在: $cliPath');\n }\n\n _remember(\n OpenClawRuntimeStatus(\n state: OpenClawRuntimeState.starting,\n message: '正在启动内置 OpenClaw Gateway',\n port: port,\n platformKey: platformKey,\n runtimePath: runtimeDir.path,\n checkedAt: DateTime.now(),\n ),\n );\n\n final args = <String>[\n cliPath,\n ...spec.gatewayArgs.map((arg) => arg.replaceAll('{port}', '$port')),\n ];\n _recentLogs.clear();\n _diag(\n 'process.starting',\n data: {\n 'nodePath': nodePath,\n 'args': args,\n 'workingDirectory': runtimeDir.path,\n 'configPath': configPath.path,\n 'desktopToolsPluginPath': desktopToolsPluginDir.path,\n 'desktopToolsManifestPath': desktopToolsManifestPath.path,\n 'requestedPort': requestedPort,\n 'selectedPort': port,\n },\n );\n\n final env =\n _buildOpenClawEnvironment(\n runtimeDir: runtimeDir,\n stateRoot: stateRoot,\n configPath: configPath,\n port: port,\n token: token,\n )..addAll({\n 'DACHENG_DESKTOP_TOOLS_ENABLED': desktopTools.uri == null ? '0' : '1',\n 'DACHENG_DESKTOP_TOOLS_MANIFEST': desktopToolsManifestPath.path,\n if (desktopTools.uri != null)\n 'DACHENG_DESKTOP_TOOLS_URL': desktopTools.uri.toString(),\n if (desktopTools.token != null)\n 'DACHENG_DESKTOP_TOOLS_TOKEN': desktopTools.token!,\n 'DACHENG_APP_RUNTIME': '1',\n 'DACHENG_AUTH_TOKEN': (authToken != null && authToken.isNotEmpty)\n ? authToken\n : token,\n if (username != null && username.isNotEmpty)\n 'DACHENG_USERNAME': username,\n 'DACHENG_IS_MEMBER': isMember ? '1' : '0',\n 'DACHENG_OPENCLAW_PROXY_TOKEN': 'dacheng-openclaw-proxy',\n });\n\n _process = await Process.start(\n nodePath,\n args,\n workingDirectory: runtimeDir.path,\n environment: env,\n mode: ProcessStartMode.normal,\n runInShell: false,\n );\n _processAuthToken = requestedAuthToken;\n _captureLogs(_process!);\n _diag('process.started', data: {'pid': _process!.pid, 'port': port});\n\n final deadline = DateTime.now().add(_startupTimeout);\n while (DateTime.now().isBefore(deadline)) {\n if (await _probe(port, token, context: 'startup')) {\n _diag('process.ready', data: {'pid': _process?.pid, 'port': port});\n _remember(\n OpenClawRuntimeStatus(\n state: OpenClawRuntimeState.running,\n message: '本机 OpenClaw Gateway 已启动',\n port: port,\n platformKey: platformKey,\n runtimePath: runtimeDir.path,\n desktopToolsStatus: desktopTools.statusJson,\n checkedAt: DateTime.now(),\n ),\n );\n return OpenClawGatewayTarget(\n baseUri: Uri.parse('http://127.0.0.1:$port'),\n token: token,\n model: model,\n modelOverride: modelOverride.trim().isEmpty\n ? null\n : modelOverride.trim(),\n desktopToolsUri: desktopTools.uri,\n desktopToolsToken: desktopTools.token,\n desktopToolsStatus: desktopTools.statusJson,\n );\n }\n\n if (_process != null) {\n final exited = await _process!.exitCode.timeout(\n const Duration(milliseconds: 1),\n onTimeout: () => -999999,\n );\n if (exited != -999999) {\n final logs = _recentLogs.take(12).join('\\n');\n _diag(\n 'process.exited-early',\n data: {\n 'exitCode': exited,\n 'recentLogs': _recentLogs.take(12).toList(),\n },\n );\n throw OpenClawRuntimeException(\n 'OpenClaw Gateway 提前退出,exitCode=$exited${logs.isEmpty ? '' : '\\n$logs'}',\n );\n }\n }\n await Future<void>.delayed(const Duration(milliseconds: 450));\n }\n\n final logs = _recentLogs.take(12).join('\\n');\n _diag(\n 'process.startup-timeout',\n data: {\n 'timeoutSeconds': _startupTimeout.inSeconds,\n 'recentLogs': _recentLogs.take(12).toList(),\n },\n );\n throw OpenClawRuntimeException(\n 'OpenClaw Gateway 启动超时${logs.isEmpty ? '' : '\\n$logs'}',\n );\n }\n\n Future<_OpenClawBundleSpec> _loadSpec(\n String platformKey, {\n bool checkUpdates = false,\n }) async {\n final raw = await rootBundle.loadString(_manifestAsset);\n final decoded = jsonDecode(raw) as Map<String, dynamic>;\n final platforms = Map<String, dynamic>.from(\n decoded['platforms'] as Map? ?? const {},\n );\n final platform = Map<String, dynamic>.from(\n platforms[platformKey] as Map? ?? const {},\n );\n if (platform.isEmpty) {\n throw OpenClawRuntimeException('bundle_manifest.json 缺少 $platformKey 配置');\n }\n\n final gatewayArgs =\n (platform['gatewayArgs'] as List? ??\n decoded['gatewayArgs'] as List? ??\n const ['gateway', '--port', '{port}'])\n .map((item) => item.toString())\n .toList();\n\n final spec = _OpenClawBundleSpec(\n version: (decoded['version'] ?? 'dev').toString(),\n defaultPort: _readInt(decoded['defaultPort']) ?? 18789,\n defaultModel: (decoded['defaultModel'] ?? 'openclaw/default').toString(),\n defaultModelOverride: decoded['defaultModelOverride']?.toString(),\n nodeExecutable: (platform['nodeExecutable'] ?? 'node/bin/node')\n .toString(),\n cliEntrypoint: (platform['cliEntrypoint'] ?? 'openclaw/bin/openclaw.js')\n .toString(),\n gatewayArgs: gatewayArgs,\n );\n _diag(\n 'manifest.loaded',\n data: {\n 'platformKey': platformKey,\n 'version': spec.version,\n 'defaultPort': spec.defaultPort,\n 'defaultModel': spec.defaultModel,\n 'nodeExecutable': spec.nodeExecutable,\n 'cliEntrypoint': spec.cliEntrypoint,\n 'gatewayArgs': spec.gatewayArgs,\n },\n );\n final allowDownloadedRuntime = await _allowDownloadedRuntime(platformKey);\n var effectiveSpec = spec;\n if (allowDownloadedRuntime) {\n effectiveSpec =\n await _loadActiveDownloadedSpec(platformKey, fallbackSpec: spec) ??\n spec;\n } else {\n await _clearActiveDownloadedRuntimeSpec(\n platformKey: platformKey,\n reason: 'macos-sandbox',\n );\n }\n if (checkUpdates && allowDownloadedRuntime) {\n effectiveSpec =\n await _maybeInstallRuntimeUpdate(\n platformKey: platformKey,\n currentSpec: effectiveSpec,\n ) ??\n effectiveSpec;\n } else if (checkUpdates) {\n _diag(\n 'runtime-update.skip-disabled',\n data: {'platformKey': platformKey, 'reason': 'macos-sandbox'},\n );\n }\n return effectiveSpec;\n }\n\n Future<bool> _allowDownloadedRuntime(String platformKey) async {\n if (!Platform.isMacOS) return true;\n\n final support = await getApplicationSupportDirectory();\n final sandboxContainerId =\n Platform.environment['APP_SANDBOX_CONTAINER_ID']?.trim() ?? '';\n final pathLooksSandboxed = p\n .split(p.normalize(support.path))\n .contains('Containers');\n final sandboxed = sandboxContainerId.isNotEmpty || pathLooksSandboxed;\n if (!sandboxed) return true;\n\n // App Store/TestFlight sandboxed apps cannot reliably execute downloaded\n // Mach-O payloads. Use the bundled runtime that was signed with inherited\n // sandbox entitlements during packaging.\n _diag(\n 'runtime-update.disabled-macos-sandbox',\n data: {\n 'platformKey': platformKey,\n 'supportPath': support.path,\n 'hasSandboxContainerId': sandboxContainerId.isNotEmpty,\n },\n );\n return false;\n }\n\n Future<void> _clearActiveDownloadedRuntimeSpec({\n required String platformKey,\n required String reason,\n }) async {\n final saved = await AppSettings.getOpenClawActiveRuntimeSpec();\n if (saved == null) return;\n await AppSettings.clearOpenClawActiveRuntimeSpec();\n _diag(\n 'runtime-update.active-cleared',\n data: {\n 'platformKey': platformKey,\n 'reason': reason,\n 'version': saved['version']?.toString(),\n 'downloaded': saved['downloaded'] == true,\n },\n );\n }\n\n Future<_OpenClawBundleSpec?> _loadActiveDownloadedSpec(\n String platformKey, {\n required _OpenClawBundleSpec fallbackSpec,\n }) async {\n final saved = await AppSettings.getOpenClawActiveRuntimeSpec();\n if (saved == null) return null;\n final spec = _specFromJson(saved, downloaded: true);\n if (spec == null) {\n await AppSettings.clearOpenClawActiveRuntimeSpec();\n return null;\n }\n final runtimeDir = await _cachedRuntimeDirIfComplete(spec, platformKey);\n if (runtimeDir == null) {\n await AppSettings.clearOpenClawActiveRuntimeSpec();\n _diag(\n 'runtime-update.active-missing',\n data: {'platformKey': platformKey, 'version': spec.version},\n );\n return null;\n }\n _diag(\n 'runtime-update.active-loaded',\n data: {\n 'platformKey': platformKey,\n 'version': spec.version,\n 'runtimeDir': runtimeDir.path,\n 'bundledVersion': fallbackSpec.version,\n },\n );\n return spec;\n }\n\n _OpenClawBundleSpec? _specFromJson(\n Map<String, dynamic> json, {\n required bool downloaded,\n }) {\n final version = json['version']?.toString().trim() ?? '';\n final nodeExecutable = json['nodeExecutable']?.toString().trim() ?? '';\n final cliEntrypoint = json['cliEntrypoint']?.toString().trim() ?? '';\n if (version.isEmpty || nodeExecutable.isEmpty || cliEntrypoint.isEmpty) {\n return null;\n }\n return _OpenClawBundleSpec(\n version: version,\n defaultPort: _readInt(json['defaultPort']) ?? 18789,\n defaultModel: (json['defaultModel'] ?? _defaultGatewayModel).toString(),\n defaultModelOverride: json['defaultModelOverride']?.toString(),\n nodeExecutable: nodeExecutable,\n cliEntrypoint: cliEntrypoint,\n gatewayArgs:\n (json['gatewayArgs'] as List? ??\n const ['gateway', '--port', '{port}', '--force'])\n .map((item) => item.toString())\n .toList(),\n downloaded: downloaded,\n );\n }\n\n Future<_OpenClawBundleSpec?> _maybeInstallRuntimeUpdate({\n required String platformKey,\n required _OpenClawBundleSpec currentSpec,\n }) async {\n if (_configuredRuntimeDirOverride(platformKey) != null) {\n _diag('runtime-update.skip-override', data: {'platformKey': platformKey});\n return null;\n }\n\n try {\n final release = await _fetchRuntimeRelease(\n platformKey: platformKey,\n currentVersion: currentSpec.version,\n );\n if (release == null || release.version == currentSpec.version) {\n return null;\n }\n\n final releaseSpec = release.toBundleSpec();\n final cached = await _cachedRuntimeDirIfComplete(\n releaseSpec,\n platformKey,\n );\n if (cached != null) {\n await AppSettings.setOpenClawActiveRuntimeSpec(releaseSpec.toJson());\n _diag(\n 'runtime-update.use-cached',\n data: {\n 'platformKey': platformKey,\n 'version': release.version,\n 'runtimeDir': cached.path,\n },\n );\n return releaseSpec;\n }\n\n return await _downloadAndInstallRuntimeUpdate(\n release: release,\n platformKey: platformKey,\n );\n } catch (error, stackTrace) {\n _diag(\n 'runtime-update.failed',\n data: {\n 'platformKey': platformKey,\n 'currentVersion': currentSpec.version,\n 'error': error.toString(),\n },\n error: error,\n stackTrace: stackTrace,\n );\n return null;\n }\n }\n\n Future<_OpenClawRuntimeRelease?> _fetchRuntimeRelease({\n required String platformKey,\n required String currentVersion,\n }) async {\n final baseUrl = AppConfig.currentAiBackendUrl.replaceFirst(\n RegExp(r'/+$'),\n '',\n );\n final uri = Uri.parse('$baseUrl/api/openclaw/runtime/manifest').replace(\n queryParameters: {\n 'platform': platformKey,\n 'currentVersion': currentVersion,\n 'appVersion': AppConfig.appVersion,\n },\n );\n final response = await http\n .get(uri, headers: {'Accept': 'application/json'})\n .timeout(const Duration(seconds: 20));\n if (response.statusCode < 200 || response.statusCode >= 300) {\n _diag(\n 'runtime-update.manifest-http',\n data: {'statusCode': response.statusCode, 'uri': uri.toString()},\n );\n return null;\n }\n final decoded = _decodeJsonOrNull(response.body);\n if (decoded is! Map) return null;\n final payload = Map<String, dynamic>.from(decoded);\n if (payload['updateAvailable'] != true) {\n _diag(\n 'runtime-update.none',\n data: {\n 'platformKey': platformKey,\n 'reason': payload['reason']?.toString(),\n 'currentVersion': currentVersion,\n 'latestVersion': payload['latestVersion']?.toString(),\n },\n );\n return null;\n }\n final latest = payload['latest'];\n if (latest is! Map) return null;\n return _releaseFromJson(Map<String, dynamic>.from(latest));\n }\n\n _OpenClawRuntimeRelease? _releaseFromJson(Map<String, dynamic> json) {\n final version = json['version']?.toString().trim() ?? '';\n final archiveUrl = json['archiveUrl']?.toString().trim() ?? '';\n final sha256Hex = json['sha256']?.toString().trim().toLowerCase() ?? '';\n final archiveUri = Uri.tryParse(archiveUrl);\n final gatewayArgs =\n (json['gatewayArgs'] as List? ??\n const ['gateway', '--port', '{port}', '--force'])\n .map((item) => item.toString())\n .toList();\n if (version.isEmpty ||\n archiveUri == null ||\n !archiveUri.hasScheme ||\n !RegExp(r'^[a-f0-9]{64}$').hasMatch(sha256Hex)) {\n _diag(\n 'runtime-update.invalid-release',\n data: {\n 'version': version,\n 'archiveUrlSet': archiveUrl.isNotEmpty,\n 'sha256Length': sha256Hex.length,\n },\n );\n return null;\n }\n return _OpenClawRuntimeRelease(\n version: version,\n archiveUri: archiveUri,\n sha256Hex: sha256Hex,\n size: _readInt(json['size']) ?? 0,\n defaultPort: _readInt(json['defaultPort']) ?? 18789,\n defaultModel: (json['defaultModel'] ?? _defaultGatewayModel).toString(),\n defaultModelOverride: json['defaultModelOverride']?.toString(),\n nodeExecutable: (json['nodeExecutable'] ?? 'node/bin/node').toString(),\n cliEntrypoint: (json['cliEntrypoint'] ?? 'openclaw/openclaw.mjs')\n .toString(),\n gatewayArgs: gatewayArgs,\n );\n }\n\n Future<_OpenClawBundleSpec> _downloadAndInstallRuntimeUpdate({\n required _OpenClawRuntimeRelease release,\n required String platformKey,\n }) async {\n final spec = release.toBundleSpec();\n final runtimeDir = await _runtimeDir(spec, platformKey);\n final support = await getApplicationSupportDirectory();\n final downloadDir = Directory(p.join(support.path, 'openclaw_downloads'));\n await downloadDir.create(recursive: true);\n final archivePath = p.join(\n downloadDir.path,\n '${_safePathPart(release.version)}-$platformKey${_archiveExtension(release.archiveUri)}',\n );\n final archiveFile = File(archivePath);\n final digest = await _downloadRuntimeArchive(release, archiveFile);\n if (digest != release.sha256Hex) {\n await _deleteFileIfExists(archiveFile);\n throw OpenClawRuntimeException(\n 'OpenClaw runtime 校验失败: expected=${release.sha256Hex}, actual=$digest',\n );\n }\n\n final extractDir = Directory(\n p.join(\n downloadDir.path,\n '${_safePathPart(release.version)}-$platformKey-extract',\n ),\n );\n final stagingDir = Directory(\n p.join(\n downloadDir.path,\n '${_safePathPart(release.version)}-$platformKey-stage',\n ),\n );\n if (await extractDir.exists()) await extractDir.delete(recursive: true);\n if (await stagingDir.exists()) await stagingDir.delete(recursive: true);\n await extractDir.create(recursive: true);\n await _extractRuntimeArchive(archiveFile, extractDir);\n\n final extractedRoot = await _findExtractedRuntimeRoot(\n extractDir: extractDir,\n spec: spec,\n );\n if (extractedRoot == null) {\n throw OpenClawRuntimeException('下载的 OpenClaw runtime 不包含必要的 node/cli 文件');\n }\n\n await runtimeDir.parent.create(recursive: true);\n if (await runtimeDir.exists()) await runtimeDir.delete(recursive: true);\n await _moveOrCopyRuntimeDirectory(extractedRoot, stagingDir);\n await stagingDir.rename(runtimeDir.path);\n final marker = File(p.join(runtimeDir.path, '.bundle_ready'));\n await marker.writeAsString(\n const JsonEncoder.withIndent(' ').convert({\n 'installedAt': DateTime.now().toIso8601String(),\n 'version': release.version,\n 'platform': platformKey,\n 'sha256': digest,\n 'source': release.archiveUri.toString(),\n }),\n );\n await _repairRuntimeLaunchMetadata(\n runtimeDir: runtimeDir,\n nodePath: File(p.join(runtimeDir.path, spec.nodeExecutable)),\n platformKey: platformKey,\n cached: false,\n );\n await AppSettings.setOpenClawActiveRuntimeSpec(spec.toJson());\n await _deleteDirectoryIfExists(extractDir);\n await _deleteFileIfExists(archiveFile);\n _diag(\n 'runtime-update.installed',\n data: {\n 'platformKey': platformKey,\n 'version': release.version,\n 'runtimeDir': runtimeDir.path,\n 'size': release.size,\n },\n );\n return spec;\n }\n\n Future<String> _downloadRuntimeArchive(\n _OpenClawRuntimeRelease release,\n File destination,\n ) async {\n final client = http.Client();\n IOSink? output;\n try {\n final request = http.Request('GET', release.archiveUri)\n ..headers['Accept'] = 'application/octet-stream';\n final response = await client\n .send(request)\n .timeout(const Duration(seconds: 20));\n if (response.statusCode < 200 || response.statusCode >= 300) {\n throw OpenClawRuntimeException(\n 'OpenClaw runtime 下载失败 (${response.statusCode})',\n );\n }\n await destination.parent.create(recursive: true);\n output = destination.openWrite();\n final digestSink = AccumulatorSink<Digest>();\n final hashSink = sha256.startChunkedConversion(digestSink);\n var bytes = 0;\n await for (final chunk in response.stream.timeout(\n const Duration(minutes: 5),\n )) {\n bytes += chunk.length;\n hashSink.add(chunk);\n output.add(chunk);\n }\n await output.flush();\n await output.close();\n output = null;\n hashSink.close();\n final digest = digestSink.events.single.toString();\n _diag(\n 'runtime-update.downloaded',\n data: {\n 'archiveUri': release.archiveUri.toString(),\n 'bytes': bytes,\n 'sha256': digest,\n },\n );\n return digest;\n } finally {\n await output?.close().catchError((_) {});\n client.close();\n }\n }\n\n Future<void> _extractRuntimeArchive(\n File archiveFile,\n Directory destination,\n ) async {\n final lowerPath = archiveFile.path.toLowerCase();\n if (Platform.isMacOS && lowerPath.endsWith('.zip')) {\n try {\n final result = await Process.run('/usr/bin/ditto', [\n '-x',\n '-k',\n archiveFile.path,\n destination.path,\n ]).timeout(const Duration(minutes: 5));\n _diag(\n 'runtime-update.extract-native',\n data: {\n 'archivePath': archiveFile.path,\n 'destination': destination.path,\n 'exitCode': result.exitCode,\n if ((result.stderr as Object).toString().isNotEmpty)\n 'stderr': _previewProcessOutput(result.stderr),\n },\n );\n if (result.exitCode == 0) return;\n } catch (error, stackTrace) {\n _diag(\n 'runtime-update.extract-native-failed',\n data: {\n 'archivePath': archiveFile.path,\n 'destination': destination.path,\n 'error': error.toString(),\n },\n error: error,\n stackTrace: stackTrace,\n );\n }\n }\n\n await extractFileToDisk(archiveFile.path, destination.path);\n _diag(\n 'runtime-update.extract-dart',\n data: {'archivePath': archiveFile.path, 'destination': destination.path},\n );\n }\n\n Future<Directory?> _findExtractedRuntimeRoot({\n required Directory extractDir,\n required _OpenClawBundleSpec spec,\n }) async {\n final direct = await _runtimeRootIfComplete(extractDir, spec);\n if (direct != null) return direct;\n await for (final entity in extractDir.list(followLinks: false)) {\n if (entity is! Directory) continue;\n final nested = await _runtimeRootIfComplete(entity, spec);\n if (nested != null) return nested;\n }\n return null;\n }\n\n Future<Directory?> _runtimeRootIfComplete(\n Directory dir,\n _OpenClawBundleSpec spec,\n ) async {\n final nodePath = File(p.join(dir.path, spec.nodeExecutable));\n final cliPath = File(p.join(dir.path, spec.cliEntrypoint));\n if (await nodePath.exists() && await cliPath.exists()) return dir;\n return null;\n }\n\n Future<Directory?> _cachedRuntimeDirIfComplete(\n _OpenClawBundleSpec spec,\n String platformKey,\n ) async {\n final runtimeDir = await _runtimeDir(spec, platformKey);\n final marker = File(p.join(runtimeDir.path, '.bundle_ready'));\n final nodePath = File(p.join(runtimeDir.path, spec.nodeExecutable));\n final cliPath = File(p.join(runtimeDir.path, spec.cliEntrypoint));\n if (await marker.exists() &&\n await nodePath.exists() &&\n await cliPath.exists()) {\n return runtimeDir;\n }\n return null;\n }\n\n String _safePathPart(String value) {\n final safe = value.replaceAll(RegExp(r'[^A-Za-z0-9_.-]+'), '-');\n return safe.isEmpty ? 'runtime' : safe;\n }\n\n String _archiveExtension(Uri uri) {\n final lower = uri.path.toLowerCase();\n if (lower.endsWith('.tar.gz')) return '.tar.gz';\n if (lower.endsWith('.tgz')) return '.tgz';\n if (lower.endsWith('.tar')) return '.tar';\n return '.zip';\n }\n\n Future<void> _deleteFileIfExists(File file) async {\n try {\n if (await file.exists()) await file.delete();\n } catch (_) {}\n }\n\n Future<void> _deleteDirectoryIfExists(Directory dir) async {\n try {\n if (await dir.exists()) await dir.delete(recursive: true);\n } catch (_) {}\n }\n\n Future<Directory> _prepareBundle(\n _OpenClawBundleSpec spec,\n String platformKey,\n ) async {\n final overrideRuntimeDir = await _runtimeDirOverride(spec, platformKey);\n if (overrideRuntimeDir != null) {\n final nodePath = File(\n p.join(overrideRuntimeDir.path, spec.nodeExecutable),\n );\n await _repairRuntimeLaunchMetadata(\n runtimeDir: overrideRuntimeDir,\n nodePath: nodePath,\n platformKey: platformKey,\n cached: true,\n );\n _diag(\n 'bundle.launch.override-runtime',\n data: {\n 'platformKey': platformKey,\n 'runtimeDir': overrideRuntimeDir.path,\n },\n );\n return overrideRuntimeDir;\n }\n\n if (spec.downloaded) {\n final downloadedRuntimeDir = await _cachedRuntimeDirIfComplete(\n spec,\n platformKey,\n );\n if (downloadedRuntimeDir != null) {\n await _repairRuntimeLaunchMetadata(\n runtimeDir: downloadedRuntimeDir,\n nodePath: File(\n p.join(downloadedRuntimeDir.path, spec.nodeExecutable),\n ),\n platformKey: platformKey,\n cached: true,\n );\n _diag(\n 'bundle.launch.downloaded-runtime',\n data: {\n 'platformKey': platformKey,\n 'version': spec.version,\n 'runtimeDir': downloadedRuntimeDir.path,\n },\n );\n return downloadedRuntimeDir;\n }\n await AppSettings.clearOpenClawActiveRuntimeSpec();\n throw OpenClawRuntimeException(\n '已下载的 OpenClaw runtime 不完整,已回退到内置版本,请重新启动。',\n );\n }\n\n final bundledRuntimeDir = await _bundledRuntimeDir(spec, platformKey);\n if (bundledRuntimeDir != null) {\n _diag(\n 'bundle.launch.bundled-runtime',\n data: {\n 'platformKey': platformKey,\n 'runtimeDir': bundledRuntimeDir.path,\n },\n );\n return bundledRuntimeDir;\n }\n\n if (Platform.isMacOS) {\n throw OpenClawRuntimeException(\n '当前安装包没有可执行的 $platformKey OpenClaw runtime。请先运行 scripts/build_openclaw_desktop_bundle.sh $platformKey,再通过 flutter run 启动。',\n );\n }\n\n final runtimeDir = await _runtimeDir(spec, platformKey);\n final marker = File(p.join(runtimeDir.path, '.bundle_ready'));\n final nodePath = File(p.join(runtimeDir.path, spec.nodeExecutable));\n final cliPath = File(p.join(runtimeDir.path, spec.cliEntrypoint));\n\n if (await marker.exists() &&\n await nodePath.exists() &&\n await cliPath.exists()) {\n await _repairRuntimeLaunchMetadata(\n runtimeDir: runtimeDir,\n nodePath: nodePath,\n platformKey: platformKey,\n cached: true,\n );\n _diag(\n 'bundle.prepare.cached',\n data: {\n 'platformKey': platformKey,\n 'runtimeDir': runtimeDir.path,\n 'marker': marker.path,\n },\n );\n return runtimeDir;\n }\n\n final legacyRuntimeDir = await _legacyRuntimeDir(spec, platformKey);\n if (legacyRuntimeDir.path != runtimeDir.path &&\n await File(\n p.join(legacyRuntimeDir.path, spec.nodeExecutable),\n ).exists() &&\n await File(\n p.join(legacyRuntimeDir.path, spec.cliEntrypoint),\n ).exists()) {\n _diag(\n 'bundle.prepare.migrate-legacy',\n data: {\n 'platformKey': platformKey,\n 'from': legacyRuntimeDir.path,\n 'to': runtimeDir.path,\n },\n );\n if (await runtimeDir.exists()) {\n await runtimeDir.delete(recursive: true);\n }\n await _copyRuntimeDirectory(legacyRuntimeDir, runtimeDir);\n await _repairRuntimeLaunchMetadata(\n runtimeDir: runtimeDir,\n nodePath: nodePath,\n platformKey: platformKey,\n cached: false,\n );\n await marker.writeAsString(DateTime.now().toIso8601String());\n _diag(\n 'bundle.prepare.migrate-complete',\n data: {\n 'platformKey': platformKey,\n 'runtimeDir': runtimeDir.path,\n 'nodeExists': await nodePath.exists(),\n 'cliExists': await cliPath.exists(),\n },\n );\n return runtimeDir;\n }\n\n final prefix = 'assets/openclaw/$platformKey/';\n final assets = await _listAssets(prefix);\n final hasNodeAsset = assets.contains('$prefix${spec.nodeExecutable}');\n final hasCliAsset = assets.contains('$prefix${spec.cliEntrypoint}');\n _diag(\n 'bundle.prepare.assets',\n data: {\n 'platformKey': platformKey,\n 'prefix': prefix,\n 'assetCount': assets.length,\n 'hasNodeAsset': hasNodeAsset,\n 'hasCliAsset': hasCliAsset,\n 'nodeAsset': '$prefix${spec.nodeExecutable}',\n 'cliAsset': '$prefix${spec.cliEntrypoint}',\n 'sample': assets.take(8).toList(),\n },\n );\n if (!hasNodeAsset || !hasCliAsset) {\n throw OpenClawRuntimeException(\n '当前安装包没有内置 $platformKey 的 OpenClaw runtime。请在 release 构建中运行 scripts/build_openclaw_desktop_bundle.sh 后再打包。',\n );\n }\n\n if (await runtimeDir.exists()) {\n _diag('bundle.prepare.remove-old', data: {'runtimeDir': runtimeDir.path});\n await runtimeDir.delete(recursive: true);\n }\n await runtimeDir.create(recursive: true);\n\n for (final asset in assets) {\n final relative = asset.substring(prefix.length);\n if (relative.isEmpty || relative.endsWith('/')) continue;\n final bytes = await rootBundle.load(asset);\n final file = File(p.join(runtimeDir.path, relative));\n await file.parent.create(recursive: true);\n await file.writeAsBytes(\n bytes.buffer.asUint8List(bytes.offsetInBytes, bytes.lengthInBytes),\n flush: false,\n );\n }\n\n await _repairRuntimeLaunchMetadata(\n runtimeDir: runtimeDir,\n nodePath: nodePath,\n platformKey: platformKey,\n cached: false,\n );\n await marker.writeAsString(DateTime.now().toIso8601String());\n _diag(\n 'bundle.prepare.complete',\n data: {\n 'platformKey': platformKey,\n 'runtimeDir': runtimeDir.path,\n 'assetCount': assets.length,\n 'nodeExists': await nodePath.exists(),\n 'nodeSize': await nodePath.exists() ? await nodePath.length() : 0,\n 'cliExists': await cliPath.exists(),\n 'cliSize': await cliPath.exists() ? await cliPath.length() : 0,\n },\n );\n return runtimeDir;\n }\n\n Future<void> _copyRuntimeDirectory(\n Directory source,\n Directory destination,\n ) async {\n await destination.create(recursive: true);\n await for (final entity in source.list(\n recursive: true,\n followLinks: false,\n )) {\n final relative = p.relative(entity.path, from: source.path);\n if (relative == '.') continue;\n final target = p.join(destination.path, relative);\n if (entity is Directory) {\n await Directory(target).create(recursive: true);\n } else if (entity is File) {\n final targetFile = File(target);\n await targetFile.parent.create(recursive: true);\n await entity.openRead().pipe(targetFile.openWrite());\n }\n }\n }\n\n Future<void> _moveOrCopyRuntimeDirectory(\n Directory source,\n Directory destination,\n ) async {\n if (await destination.exists()) {\n await destination.delete(recursive: true);\n }\n await destination.parent.create(recursive: true);\n try {\n await source.rename(destination.path);\n return;\n } on FileSystemException {\n await _copyRuntimeDirectory(source, destination);\n }\n }\n\n Future<void> _repairRuntimeLaunchMetadata({\n required Directory runtimeDir,\n required File nodePath,\n required String platformKey,\n required bool cached,\n }) async {\n if (Platform.isWindows) return;\n\n final chmodResult = await Process.run('/bin/chmod', ['+x', nodePath.path]);\n ProcessResult? xattrResult;\n if (Platform.isMacOS) {\n // Files created by a quarantined app can inherit the quarantine xattr.\n // macOS then rejects Process.start with \"Operation not permitted\".\n xattrResult = await Process.run('/usr/bin/xattr', [\n '-dr',\n 'com.apple.quarantine',\n runtimeDir.path,\n ]);\n await Process.run('/usr/bin/xattr', [\n '-dr',\n 'com.apple.provenance',\n runtimeDir.path,\n ]);\n }\n\n _diag(\n 'bundle.prepare.launch-metadata',\n data: {\n 'platformKey': platformKey,\n 'runtimeDir': runtimeDir.path,\n 'nodePath': nodePath.path,\n 'cached': cached,\n 'chmodExitCode': chmodResult.exitCode,\n if ((chmodResult.stderr as Object).toString().isNotEmpty)\n 'chmodStderr': _previewProcessOutput(chmodResult.stderr),\n if (xattrResult != null) 'xattrExitCode': xattrResult.exitCode,\n if (xattrResult != null &&\n (xattrResult.stderr as Object).toString().isNotEmpty)\n 'xattrStderr': _previewProcessOutput(xattrResult.stderr),\n },\n );\n }\n\n Future<bool> _hasRequiredBundleAssets(\n String platformKey,\n _OpenClawBundleSpec spec,\n ) async {\n final prefix = 'assets/openclaw/$platformKey/';\n final assets = await _listAssets(prefix);\n final hasRequired =\n assets.contains('$prefix${spec.nodeExecutable}') &&\n assets.contains('$prefix${spec.cliEntrypoint}');\n _diag(\n 'bundle.has-required-assets',\n data: {\n 'platformKey': platformKey,\n 'assetCount': assets.length,\n 'hasRequired': hasRequired,\n },\n );\n return hasRequired;\n }\n\n Future<List<String>> _listAssets(String prefix) async {\n final assets = <String>{};\n var jsonCount = 0;\n var binCount = 0;\n var indexCount = 0;\n final failures = <String>[];\n\n try {\n final raw = await rootBundle.loadString('AssetManifest.json');\n final decoded = jsonDecode(raw) as Map<String, dynamic>;\n final items = decoded.keys\n .where((key) => key.startsWith(prefix))\n .toList();\n jsonCount = items.length;\n assets.addAll(items);\n } catch (error) {\n failures.add('AssetManifest.json: $error');\n // Newer Flutter release builds may only include AssetManifest.bin.\n }\n\n try {\n final manifest = await AssetManifest.loadFromAssetBundle(rootBundle);\n final items = manifest\n .listAssets()\n .where((key) => key.startsWith(prefix))\n .toList();\n binCount = items.length;\n assets.addAll(items);\n } catch (error) {\n failures.add('AssetManifest.bin: $error');\n // App Store archives patched after flutter build carry a JSON/index file.\n }\n\n try {\n final raw = await rootBundle.loadString(\n 'assets/openclaw/asset_index.json',\n );\n final decoded = jsonDecode(raw);\n final indexedAssets = decoded is Map\n ? decoded['assets']\n : decoded is List\n ? decoded\n : const [];\n if (indexedAssets is List) {\n final items = indexedAssets\n .map((item) => item.toString())\n .where((key) => key.startsWith(prefix))\n .toList();\n indexCount = items.length;\n assets.addAll(items);\n }\n } catch (error) {\n failures.add('asset_index.json: $error');\n // Older builds do not have an OpenClaw asset index.\n }\n\n final sortedAssets = assets.toList()..sort();\n _diag(\n 'asset.list',\n data: {\n 'prefix': prefix,\n 'total': sortedAssets.length,\n 'jsonCount': jsonCount,\n 'binCount': binCount,\n 'indexCount': indexCount,\n if (failures.isNotEmpty) 'failures': failures,\n 'sample': sortedAssets.take(8).toList(),\n },\n );\n return sortedAssets;\n }\n\n Future<bool> _probe(\n int port,\n String token, {\n String context = 'probe',\n }) async {\n try {\n final healthResponse = await http\n .get(\n Uri.parse('http://127.0.0.1:$port/health'),\n headers: {'Authorization': 'Bearer $token'},\n )\n .timeout(_probeTimeout);\n final healthPayload = _decodeJsonOrNull(healthResponse.body);\n if (healthResponse.statusCode < 200 ||\n healthResponse.statusCode >= 300 ||\n !_looksLikeHealthPayload(healthPayload)) {\n _diag(\n 'probe.health-unhealthy',\n data: {\n 'context': context,\n 'port': port,\n 'statusCode': healthResponse.statusCode,\n 'contentType': healthResponse.headers['content-type'],\n 'bodyPreview': _previewBody(healthResponse.body),\n },\n );\n return false;\n }\n\n final modelsResponse = await http\n .get(\n Uri.parse('http://127.0.0.1:$port/v1/models'),\n headers: {'Authorization': 'Bearer $token'},\n )\n .timeout(_probeTimeout);\n final modelsPayload = _decodeJsonOrNull(modelsResponse.body);\n if (modelsResponse.statusCode < 200 ||\n modelsResponse.statusCode >= 300 ||\n !_looksLikeModelsPayload(modelsPayload)) {\n _diag(\n 'probe.models-unhealthy',\n data: {\n 'context': context,\n 'port': port,\n 'statusCode': modelsResponse.statusCode,\n 'contentType': modelsResponse.headers['content-type'],\n 'bodyPreview': _previewBody(modelsResponse.body),\n },\n );\n return false;\n }\n\n // `/v1/models` can be healthy while `/v1/chat/completions` is disabled\n // by an old config. Use GET so the probe only checks route wiring and\n // never starts a real model request.\n final chatResponse = await http\n .get(\n Uri.parse('http://127.0.0.1:$port/v1/chat/completions'),\n headers: {'Authorization': 'Bearer $token'},\n )\n .timeout(_probeTimeout);\n final healthy =\n chatResponse.statusCode != 401 &&\n chatResponse.statusCode != 403 &&\n chatResponse.statusCode != 404;\n _diag(\n healthy ? 'probe.healthy' : 'probe.chat-unhealthy',\n data: {\n 'context': context,\n 'port': port,\n 'chatStatusCode': chatResponse.statusCode,\n },\n );\n return healthy;\n } catch (error) {\n if (context != 'startup') {\n _diag(\n 'probe.error',\n data: {'context': context, 'port': port},\n error: error,\n );\n }\n return false;\n }\n }\n\n Future<File> _ensureConfigFile({\n required Directory runtimeDir,\n required Directory stateRoot,\n required int port,\n required String token,\n required String backendDeepSeekModel,\n required String deepSeekProxyBaseUrl,\n required String remoteGatewayUrl,\n List<String> extraPluginLoadPaths = const [],\n }) async {\n final configDir = Directory(p.join(stateRoot.path, 'config'));\n await configDir.create(recursive: true);\n final workspace = Directory(p.join(stateRoot.path, 'workspace'));\n await workspace.create(recursive: true);\n final configPath = File(p.join(configDir.path, 'openclaw.json'));\n final bundledPluginLoadPaths = await _bundledPluginLoadPaths(runtimeDir);\n final pluginLoadPaths = _mergeStringLists(\n extraPluginLoadPaths,\n bundledPluginLoadPaths,\n );\n final hasWeChatPlugin = pluginLoadPaths.any(\n (path) => p.basename(path) == _weChatPluginId,\n );\n final remoteUrl = remoteGatewayUrl.trim();\n\n final config = _buildEmbeddedConfig(\n stateRoot: stateRoot,\n port: port,\n token: token,\n backendDeepSeekModel: backendDeepSeekModel,\n deepSeekProxyBaseUrl: deepSeekProxyBaseUrl,\n remoteGatewayUrl: remoteUrl,\n pluginLoadPaths: pluginLoadPaths,\n hasWeChatPlugin: hasWeChatPlugin,\n );\n\n // Repair older embedded configs in-place. OpenClaw 2026.6 requires\n // gateway.mode=local, and the home screen requires the OpenAI-compatible\n // chat endpoint. Preserve unrelated user edits while fixing the fields\n // needed for the app-owned embedded gateway to start reliably.\n final merged = await _mergeEmbeddedConfig(configPath, config);\n await configPath.writeAsString(\n const JsonEncoder.withIndent(' ').convert(merged),\n );\n _diag(\n 'config.written',\n data: {\n 'configPath': configPath.path,\n 'port': port,\n 'gatewayMode': _mutableMap(merged['gateway'])['mode'],\n 'backendDeepSeekModel': backendDeepSeekModel,\n 'deepSeekProxyBaseUrl': deepSeekProxyBaseUrl,\n 'remoteGatewayUrlSet': remoteUrl.isNotEmpty,\n 'pluginLoadPaths': pluginLoadPaths,\n 'hasWeChatPlugin': hasWeChatPlugin,\n },\n );\n return configPath;\n }\n\n @visibleForTesting\n Map<String, dynamic> buildEmbeddedConfigForTest({\n required Directory stateRoot,\n required int port,\n required String token,\n required String backendDeepSeekModel,\n required String deepSeekProxyBaseUrl,\n String remoteGatewayUrl = '',\n List<String> pluginLoadPaths = const [],\n bool hasWeChatPlugin = false,\n }) {\n return _buildEmbeddedConfig(\n stateRoot: stateRoot,\n port: port,\n token: token,\n backendDeepSeekModel: backendDeepSeekModel,\n deepSeekProxyBaseUrl: deepSeekProxyBaseUrl,\n remoteGatewayUrl: remoteGatewayUrl,\n pluginLoadPaths: pluginLoadPaths,\n hasWeChatPlugin: hasWeChatPlugin,\n );\n }\n\n Map<String, dynamic> _buildEmbeddedConfig({\n required Directory stateRoot,\n required int port,\n required String token,\n required String backendDeepSeekModel,\n required String deepSeekProxyBaseUrl,\n required String remoteGatewayUrl,\n required List<String> pluginLoadPaths,\n required bool hasWeChatPlugin,\n }) {\n final workspace = p.join(stateRoot.path, 'workspace');\n final remoteUrl = remoteGatewayUrl.trim();\n final desktopToolAllowList = DesktopControlPolicy.supportedTools.toList()\n ..sort();\n final canvasHostConfig = <String, dynamic>{\n 'enabled': true,\n 'root': p.join(stateRoot.path, 'canvas'),\n };\n\n return <String, dynamic>{\n 'gateway': {\n 'mode': 'local',\n 'port': port,\n 'bind': 'loopback',\n 'auth': {'mode': 'token', 'token': token},\n if (remoteUrl.isNotEmpty) 'remote': {'enabled': true, 'url': remoteUrl},\n 'http': {\n 'endpoints': {\n 'chatCompletions': {'enabled': true},\n 'responses': {'enabled': true},\n },\n },\n },\n 'models': {\n 'mode': 'merge',\n 'providers': {\n _backendDeepSeekProviderId: {\n 'baseUrl': deepSeekProxyBaseUrl,\n 'api': 'openai-completions',\n 'apiKey': 'DACHENG_OPENCLAW_PROXY_TOKEN',\n 'authHeader': true,\n 'headers': {'x-dacheng-auth-token': 'DACHENG_AUTH_TOKEN'},\n 'models': [\n {\n 'id': 'deepseek-chat',\n 'name': 'DeepSeek Chat',\n 'contextWindow': 131072,\n 'maxTokens': 8192,\n 'input': ['text'],\n 'compat': {\n 'requiresStringContent': true,\n 'strictMessageKeys': true,\n },\n },\n {\n 'id': 'deepseek-reasoner',\n 'name': 'DeepSeek Reasoner',\n 'contextWindow': 131072,\n 'maxTokens': 8192,\n 'reasoning': true,\n 'input': ['text'],\n 'compat': {\n 'requiresStringContent': true,\n 'strictMessageKeys': true,\n },\n },\n ],\n },\n },\n },\n 'browser': {\n 'enabled': true,\n 'defaultProfile': 'openclaw',\n 'headless': false,\n 'ssrfPolicy': {'dangerouslyAllowPrivateNetwork': true},\n },\n 'tools': {\n 'profile': 'full',\n 'alsoAllow': desktopToolAllowList,\n 'exec': {'host': 'gateway', 'security': 'full', 'ask': 'off'},\n },\n 'plugins': {\n 'enabled': true,\n 'deny': <String>[],\n 'load': {'paths': pluginLoadPaths},\n 'slots': {'memory': 'memory-core'},\n 'entries': <String, dynamic>{\n 'memory-core': {'enabled': true},\n 'bonjour': {'enabled': false},\n 'browser': {'enabled': true},\n 'canvas': {\n 'enabled': true,\n 'config': {'host': canvasHostConfig},\n },\n 'device-pair': {'enabled': true},\n 'file-transfer': {'enabled': true},\n 'phone-control': {'enabled': true},\n 'talk-voice': {'enabled': true},\n _desktopToolsPluginId: {'enabled': true},\n if (hasWeChatPlugin) _weChatPluginId: {'enabled': true},\n },\n },\n 'channels': {\n 'defaults': {'groupPolicy': 'allowlist'},\n if (hasWeChatPlugin)\n _weChatPluginId: {\n 'enabled': true,\n 'dmPolicy': 'pairing',\n 'allowFrom': <String>[],\n 'accounts': <String, dynamic>{},\n },\n },\n 'agents': {\n 'defaults': {\n 'workspace': workspace,\n 'model': {'primary': backendDeepSeekModel},\n },\n 'list': [\n {\n 'id': 'dacheng',\n 'default': true,\n 'workspace': workspace,\n 'model': {'primary': backendDeepSeekModel},\n },\n ],\n },\n };\n }\n\n Future<File> _ensureDesktopToolsManifest({\n required Directory stateRoot,\n required _DesktopToolsLaunch desktopTools,\n }) async {\n final configDir = Directory(p.join(stateRoot.path, 'config'));\n await configDir.create(recursive: true);\n final manifestPath = File(p.join(configDir.path, 'desktop_tools.json'));\n final tools =\n DesktopControlPolicy.supportedTools\n .map(\n (name) => {\n 'name': name,\n 'endpoint': '/v1/tools/execute',\n 'readOnly': DesktopControlPolicy.isReadOnly(name),\n 'requiresConfirmation':\n DesktopControlPolicy.requiresConfirmation(name),\n },\n )\n .toList()\n ..sort(\n (a, b) => a['name'].toString().compareTo(b['name'].toString()),\n );\n\n await manifestPath.writeAsString(\n const JsonEncoder.withIndent(' ').convert({\n 'version': 1,\n 'transport': 'http-loopback',\n 'baseUrl': desktopTools.uri?.toString(),\n 'auth': {'type': 'bearer', 'env': 'DACHENG_DESKTOP_TOOLS_TOKEN'},\n 'status': desktopTools.statusJson,\n 'tools': tools,\n }),\n );\n _diag(\n 'desktop-tools.manifest-written',\n data: {\n 'manifestPath': manifestPath.path,\n 'hasBaseUrl': desktopTools.uri != null,\n },\n );\n return manifestPath;\n }\n\n Future<File> _ensureAgentModelConfig({\n required Directory stateRoot,\n required String deepSeekProxyBaseUrl,\n }) async {\n final agentDir = Directory(\n p.join(stateRoot.path, 'state', 'agents', 'dacheng', 'agent'),\n );\n await agentDir.create(recursive: true);\n final modelsPath = File(p.join(agentDir.path, 'models.json'));\n final providerConfig = <String, dynamic>{\n 'baseUrl': deepSeekProxyBaseUrl,\n 'api': 'openai-completions',\n 'apiKey': 'DACHENG_OPENCLAW_PROXY_TOKEN',\n 'authHeader': true,\n 'headers': {'x-dacheng-auth-token': 'DACHENG_AUTH_TOKEN'},\n 'models': [\n {\n 'id': 'deepseek-chat',\n 'name': 'DeepSeek Chat',\n 'contextWindow': 131072,\n 'maxTokens': 8192,\n 'input': ['text'],\n 'compat': {'requiresStringContent': true, 'strictMessageKeys': true},\n },\n {\n 'id': 'deepseek-reasoner',\n 'name': 'DeepSeek Reasoner',\n 'contextWindow': 131072,\n 'maxTokens': 8192,\n 'reasoning': true,\n 'input': ['text'],\n 'compat': {'requiresStringContent': true, 'strictMessageKeys': true},\n },\n ],\n };\n await modelsPath.writeAsString(\n const JsonEncoder.withIndent(' ').convert({\n 'providers': {_backendDeepSeekProviderId: providerConfig},\n }),\n );\n _diag(\n 'agent-models.written',\n data: {\n 'modelsPath': modelsPath.path,\n 'provider': _backendDeepSeekProviderId,\n 'deepSeekProxyBaseUrl': deepSeekProxyBaseUrl,\n },\n );\n return modelsPath;\n }\n\n Future<_OpenClawCliLaunch> _prepareCliLaunch({\n required bool ensureGateway,\n }) async {\n if (ensureGateway) {\n await ensureStarted();\n }\n final platformKey = _platformKey;\n if (platformKey == null) {\n throw const OpenClawRuntimeException('当前平台不支持内置 OpenClaw Gateway');\n }\n final spec = await _loadSpec(platformKey, checkUpdates: false);\n final runtimeDir = await _prepareBundle(spec, platformKey);\n final stateRoot = await _stateRoot();\n final desktopToolsPluginDir = await _ensureDesktopToolsPlugin(\n stateRoot: stateRoot,\n );\n final port = await AppSettings.getOpenClawGatewayPort(\n defaultValue: spec.defaultPort,\n );\n final token = await AppSettings.getOpenClawGatewayToken();\n final backendDeepSeekModel = _backendDeepSeekModelRef(\n await AppSettings.getOpenClawDeepSeekModel(\n defaultValue: _defaultDeepSeekModelFor(spec.defaultModel),\n ),\n );\n final configPath = await _ensureConfigFile(\n runtimeDir: runtimeDir,\n stateRoot: stateRoot,\n port: port,\n token: token,\n backendDeepSeekModel: backendDeepSeekModel,\n deepSeekProxyBaseUrl: _deepSeekProxyBaseUrl(),\n remoteGatewayUrl: await AppSettings.getOpenClawRemoteGatewayUrl(),\n extraPluginLoadPaths: [desktopToolsPluginDir.path],\n );\n final nodePath = p.join(runtimeDir.path, spec.nodeExecutable);\n final cliPath = p.join(runtimeDir.path, spec.cliEntrypoint);\n if (!await File(nodePath).exists()) {\n throw OpenClawRuntimeException('OpenClaw 内置 Node 不存在: $nodePath');\n }\n if (!await File(cliPath).exists()) {\n throw OpenClawRuntimeException('OpenClaw CLI 入口不存在: $cliPath');\n }\n return _OpenClawCliLaunch(\n spec: spec,\n runtimeDir: runtimeDir,\n stateRoot: stateRoot,\n configPath: configPath,\n nodePath: nodePath,\n cliPath: cliPath,\n port: port,\n token: token,\n );\n }\n\n Map<String, String> _buildOpenClawEnvironment({\n required Directory runtimeDir,\n required Directory stateRoot,\n required File configPath,\n required int port,\n required String token,\n }) {\n final nodeBinDir = Platform.isWindows\n ? p.join(runtimeDir.path, 'node')\n : p.join(runtimeDir.path, 'node', 'bin');\n final pathKey = Platform.isWindows ? 'Path' : 'PATH';\n final currentPath =\n Platform.environment[pathKey] ?? Platform.environment['PATH'] ?? '';\n final separator = Platform.isWindows ? ';' : ':';\n final pathValue = currentPath.isEmpty\n ? nodeBinDir\n : '$nodeBinDir$separator$currentPath';\n final env = Map<String, String>.from(Platform.environment)\n ..addAll({\n 'OPENCLAW_GATEWAY_PORT': '$port',\n 'OPENCLAW_GATEWAY_BIND': 'loopback',\n 'OPENCLAW_GATEWAY_TOKEN': token,\n 'OPENCLAW_CONFIG_PATH': configPath.path,\n 'OPENCLAW_STATE_DIR': p.join(stateRoot.path, 'state'),\n 'OPENCLAW_AGENT_DIR': p.join(stateRoot.path, 'agents'),\n 'OPENCLAW_WORKSPACE': p.join(stateRoot.path, 'workspace'),\n 'OPENCLAW_DISABLE_BONJOUR': '1',\n pathKey: pathValue,\n if (pathKey != 'PATH') 'PATH': pathValue,\n });\n return env;\n }\n\n Future<List<String>> _bundledPluginLoadPaths(Directory runtimeDir) async {\n final paths = <String>[];\n final weChatPluginDir = Directory(\n p.join(runtimeDir.path, 'plugins', _weChatPluginId),\n );\n if (await File(\n p.join(weChatPluginDir.path, 'openclaw.plugin.json'),\n ).exists()) {\n paths.add(weChatPluginDir.path);\n }\n return paths;\n }\n\n Future<Directory> _ensureDesktopToolsPlugin({\n required Directory stateRoot,\n }) async {\n final dir = Directory(\n p.join(stateRoot.path, 'plugins', _desktopToolsPluginId),\n );\n await dir.create(recursive: true);\n final tools = DesktopControlPolicy.supportedTools.toList()..sort();\n await File(p.join(dir.path, 'package.json')).writeAsString(\n const JsonEncoder.withIndent(' ').convert({\n 'name': '@dacheng/openclaw-desktop-tools',\n 'version': '1.0.0',\n 'type': 'module',\n 'openclaw': {\n 'extensions': ['./index.mjs'],\n },\n }),\n );\n await File(p.join(dir.path, 'openclaw.plugin.json')).writeAsString(\n const JsonEncoder.withIndent(' ').convert({\n 'id': _desktopToolsPluginId,\n 'name': 'Dacheng Desktop Tools',\n 'description':\n 'Expose Fabushi desktop and Chrome control bridge tools to embedded OpenClaw.',\n 'activation': {'onStartup': true},\n 'contracts': {'tools': tools},\n 'configSchema': {\n 'type': 'object',\n 'additionalProperties': false,\n 'properties': <String, dynamic>{},\n },\n }),\n );\n await File(\n p.join(dir.path, 'index.mjs'),\n ).writeAsString(_desktopToolsPluginSource(tools));\n _diag(\n 'desktop-tools.plugin-written',\n data: {'pluginDir': dir.path, 'tools': tools},\n );\n return dir;\n }\n\n String _desktopToolsPluginSource(List<String> tools) {\n final toolsJson = jsonEncode(tools);\n return '''\nconst tools = $toolsJson;\n\nconst descriptions = {\n \"desktop.observe\": \"Observe the active desktop application and visible windows.\",\n \"desktop.screenshot\": \"Capture a screenshot of the local desktop.\",\n \"desktop.windows\": \"List visible desktop windows.\",\n \"desktop.click\": \"Click a local desktop coordinate after user confirmation.\",\n \"desktop.type\": \"Type text into the focused local desktop app after user confirmation.\",\n \"desktop.hotkey\": \"Send a keyboard shortcut to the local desktop after user confirmation.\",\n \"desktop.scroll\": \"Scroll the local desktop after user confirmation.\",\n \"chrome.tabs\": \"List tabs from the paired Chrome connector.\",\n \"chrome.navigate\": \"Navigate the active or selected Chrome tab after user confirmation.\",\n \"chrome.dom_snapshot\": \"Read a structured DOM snapshot from the paired Chrome connector.\",\n \"chrome.screenshot\": \"Capture a screenshot from the paired Chrome connector.\",\n \"chrome.click\": \"Click an element or point in the paired Chrome tab after user confirmation.\",\n \"chrome.type\": \"Type into an element in the paired Chrome tab after user confirmation.\"\n};\n\nconst parameterSchemas = {\n \"desktop.click\": {\n type: \"object\",\n additionalProperties: true,\n properties: {\n x: { type: \"number\" },\n y: { type: \"number\" },\n button: { type: \"string\", enum: [\"left\", \"right\"] }\n },\n required: [\"x\", \"y\"]\n },\n \"desktop.type\": {\n type: \"object\",\n additionalProperties: true,\n properties: { text: { type: \"string\" } },\n required: [\"text\"]\n },\n \"desktop.hotkey\": {\n type: \"object\",\n additionalProperties: true,\n properties: {\n key: { type: \"string\" },\n keys: { type: \"array\", items: { type: \"string\" } }\n }\n },\n \"chrome.navigate\": {\n type: \"object\",\n additionalProperties: true,\n properties: { url: { type: \"string\" }, tabId: { type: \"integer\" } },\n required: [\"url\"]\n },\n \"chrome.click\": {\n type: \"object\",\n additionalProperties: true,\n properties: {\n selector: { type: \"string\" },\n x: { type: \"number\" },\n y: { type: \"number\" },\n tabId: { type: \"integer\" }\n }\n },\n \"chrome.type\": {\n type: \"object\",\n additionalProperties: true,\n properties: {\n selector: { type: \"string\" },\n x: { type: \"number\" },\n y: { type: \"number\" },\n text: { type: \"string\" },\n tabId: { type: \"integer\" }\n },\n required: [\"text\"]\n }\n};\n\nconst defaultSchema = {\n type: \"object\",\n additionalProperties: true,\n properties: {}\n};\n\nfunction toolResultText(value) {\n try {\n return JSON.stringify(value, null, 2);\n } catch {\n return String(value);\n }\n}\n\nasync function callBridge(toolName, args) {\n if (process.env.DACHENG_DESKTOP_TOOLS_ENABLED !== \"1\") {\n return {\n isError: true,\n content: [{ type: \"text\", text: \"Fabushi desktop tools bridge is not running.\" }]\n };\n }\n const rawBaseUrl = process.env.DACHENG_DESKTOP_TOOLS_URL || \"\";\n const baseUrl = rawBaseUrl.endsWith(\"/\") ? rawBaseUrl.slice(0, -1) : rawBaseUrl;\n const token = process.env.DACHENG_DESKTOP_TOOLS_TOKEN || \"\";\n if (!baseUrl || !token) {\n return {\n isError: true,\n content: [{ type: \"text\", text: \"Fabushi desktop tools bridge URL/token is missing.\" }]\n };\n }\n const response = await fetch(`\\${baseUrl}/v1/tools/execute`, {\n method: \"POST\",\n headers: {\n \"Authorization\": `Bearer \\${token}`,\n \"Content-Type\": \"application/json\"\n },\n body: JSON.stringify({ tool: toolName, arguments: args || {} })\n });\n const payload = await response.json().catch(() => ({}));\n const ok = response.ok && payload && payload.ok !== false;\n const data = payload && Object.prototype.hasOwnProperty.call(payload, \"data\")\n ? payload.data\n : payload;\n return {\n isError: !ok,\n content: [{ type: \"text\", text: toolResultText(data) }],\n details: data\n };\n}\n\nexport default {\n id: \"dacheng-desktop-tools\",\n name: \"Dacheng Desktop Tools\",\n description: \"Expose Fabushi desktop and Chrome control tools to embedded OpenClaw.\",\n configSchema: { type: \"object\", additionalProperties: false, properties: {} },\n register(api) {\n for (const name of tools) {\n api.registerTool({\n name,\n label: name,\n description: descriptions[name] || `Run \\${name} through the Fabushi desktop bridge.`,\n parameters: parameterSchemas[name] || defaultSchema,\n async execute(_toolCallId, args) {\n return await callBridge(name, args);\n }\n });\n }\n }\n};\n''';\n }\n\n Future<Map<String, dynamic>> _mergeEmbeddedConfig(\n File configPath,\n Map<String, dynamic> defaults,\n ) async {\n Map<String, dynamic> current = <String, dynamic>{};\n if (await configPath.exists()) {\n try {\n final decoded = jsonDecode(await configPath.readAsString());\n if (decoded is Map<String, dynamic>) {\n current = decoded;\n } else if (decoded is Map) {\n current = Map<String, dynamic>.from(decoded);\n }\n } catch (_) {\n current = <String, dynamic>{};\n }\n }\n\n final gateway = _mutableMap(current['gateway']);\n final defaultGateway = _mutableMap(defaults['gateway']);\n gateway['mode'] = 'local';\n gateway['port'] = defaultGateway['port'];\n gateway['bind'] = defaultGateway['bind'];\n gateway['auth'] = Map<String, dynamic>.from(defaultGateway['auth'] as Map);\n if (defaultGateway.containsKey('remote')) {\n gateway['remote'] = {\n ..._mutableMap(gateway['remote']),\n ..._mutableMap(defaultGateway['remote']),\n };\n } else {\n final remote = _mutableMap(gateway['remote'])..remove('url');\n if (remote.isEmpty) {\n gateway.remove('remote');\n } else {\n gateway['remote'] = remote;\n }\n }\n\n final gatewayHttp = _mutableMap(gateway['http']);\n final endpoints = _mutableMap(gatewayHttp['endpoints']);\n endpoints['chatCompletions'] = {'enabled': true};\n endpoints['responses'] = {'enabled': true};\n gatewayHttp['endpoints'] = endpoints;\n gateway['http'] = gatewayHttp;\n current['gateway'] = gateway;\n\n final env = _mutableMap(current['env']);\n env.remove('DEEPSEEK_API_KEY');\n if (env.isEmpty) {\n current.remove('env');\n } else {\n current['env'] = env;\n }\n\n final defaultModels = _mutableMap(defaults['models']);\n final models = _mutableMap(current['models']);\n models['mode'] ??= defaultModels['mode'];\n final providers = _mutableMap(models['providers']);\n final defaultProviders = _mutableMap(defaultModels['providers']);\n providers.remove('deepseek');\n providers.remove('dacheng-deepseek');\n providers.remove(_backendDeepSeekProviderId);\n for (final entry in defaultProviders.entries) {\n providers[entry.key] = entry.value is Map\n ? Map<String, dynamic>.from(entry.value as Map)\n : entry.value;\n }\n models['providers'] = providers;\n current['models'] = models;\n\n final defaultBrowser = _mutableMap(defaults['browser']);\n final browser = _mutableMap(current['browser']);\n browser.addAll(defaultBrowser);\n current['browser'] = browser;\n\n // OpenClaw 2026.6 rejects root-level canvas/canvasHost. Canvas is now\n // configured through plugins.entries.canvas.config.host.\n current.remove('canvas');\n current.remove('canvasHost');\n\n final defaultTools = _mutableMap(defaults['tools']);\n final tools = _mutableMap(current['tools']);\n tools.remove('toolSearch');\n tools.remove('fs');\n final alsoAllow = _mergeStringLists(\n tools['alsoAllow'],\n defaultTools['alsoAllow'],\n );\n final execTools = _mutableMap(tools['exec']);\n execTools.remove('mode');\n if (execTools.isNotEmpty) {\n tools['exec'] = execTools;\n }\n tools.addAll(defaultTools);\n if (alsoAllow.isEmpty) {\n tools.remove('alsoAllow');\n } else {\n tools['alsoAllow'] = alsoAllow;\n }\n current['tools'] = tools;\n\n final defaultPlugins = _mutableMap(defaults['plugins']);\n final plugins = _mutableMap(current['plugins']);\n plugins['enabled'] = true;\n final defaultEntries = _mutableMap(defaultPlugins['entries']);\n final requiredPluginIds = defaultEntries.keys.toSet();\n final allowList = _stringList(plugins['allow']);\n if (allowList.isEmpty) {\n plugins.remove('allow');\n } else {\n plugins['allow'] = _mergeStringLists(allowList, requiredPluginIds);\n }\n final denyList = _stringList(\n plugins['deny'],\n ).where((item) => !requiredPluginIds.contains(item)).toList();\n if (denyList.isEmpty) {\n plugins.remove('deny');\n } else {\n plugins['deny'] = denyList;\n }\n final defaultLoad = _mutableMap(defaultPlugins['load']);\n final load = _mutableMap(plugins['load']);\n load['paths'] = _mergeStringLists(load['paths'], defaultLoad['paths']);\n plugins['load'] = load;\n final slots = _mutableMap(plugins['slots']);\n slots.addAll(_mutableMap(defaultPlugins['slots']));\n plugins['slots'] = slots;\n final entries = _mutableMap(plugins['entries']);\n for (final entry in defaultEntries.entries) {\n final currentEntry = _mutableMap(entries[entry.key]);\n currentEntry.addAll(\n entry.value is Map\n ? Map<String, dynamic>.from(entry.value as Map)\n : {'enabled': true},\n );\n entries[entry.key] = currentEntry;\n }\n plugins['entries'] = entries;\n current['plugins'] = plugins;\n\n final defaultChannels = _mutableMap(defaults['channels']);\n final channels = _mutableMap(current['channels']);\n final defaultChannelDefaults = _mutableMap(defaultChannels['defaults']);\n if (defaultChannelDefaults.isNotEmpty) {\n channels['defaults'] = {\n ..._mutableMap(channels['defaults']),\n ...defaultChannelDefaults,\n };\n }\n for (final entry in defaultChannels.entries) {\n if (entry.key == 'defaults') continue;\n final currentChannel = _mutableMap(channels[entry.key]);\n currentChannel.addAll(\n entry.value is Map\n ? Map<String, dynamic>.from(entry.value as Map)\n : <String, dynamic>{},\n );\n channels[entry.key] = currentChannel;\n }\n current['channels'] = channels;\n\n final agents = _mutableMap(current['agents']);\n final defaultAgents = _mutableMap(defaults['agents']);\n final defaultAgentDefaults = _mutableMap(defaultAgents['defaults']);\n final agentDefaults = _mutableMap(agents['defaults']);\n agentDefaults['workspace'] = defaultAgentDefaults['workspace'];\n agentDefaults['model'] = defaultAgentDefaults['model'];\n agents['defaults'] = agentDefaults;\n\n final defaultList = (defaultAgents['list'] as List? ?? const [])\n .whereType<Map>()\n .map((item) => Map<String, dynamic>.from(item))\n .toList();\n final defaultDacheng = defaultList.isNotEmpty\n ? defaultList.first\n : <String, dynamic>{\n 'id': 'dacheng',\n 'default': true,\n 'workspace': agentDefaults['workspace'],\n 'model': agentDefaults['model'],\n };\n final list = (agents['list'] as List? ?? const [])\n .map((item) => item is Map ? Map<String, dynamic>.from(item) : item)\n .toList();\n var foundDacheng = false;\n for (var i = 0; i < list.length; i++) {\n final item = list[i];\n if (item is! Map<String, dynamic>) continue;\n if (item['id'] != 'dacheng') continue;\n foundDacheng = true;\n item['default'] = true;\n item['workspace'] = defaultDacheng['workspace'];\n item['model'] = defaultDacheng['model'];\n list[i] = item;\n }\n if (!foundDacheng) {\n list.add(defaultDacheng);\n }\n agents['list'] = list.isEmpty ? defaultList : list;\n current['agents'] = agents;\n\n return current;\n }\n\n @visibleForTesting\n Future<Map<String, dynamic>> mergeEmbeddedConfigForTest(\n File configPath,\n Map<String, dynamic> defaults,\n ) {\n return _mergeEmbeddedConfig(configPath, defaults);\n }\n\n String _gatewayChatModel(String model) {\n final trimmed = model.trim();\n if (trimmed == 'openclaw' || trimmed.startsWith('openclaw/')) {\n return trimmed;\n }\n return _defaultGatewayModel;\n }\n\n String _defaultDeepSeekModelFor(String model) {\n final trimmed = model.trim();\n if (trimmed.startsWith('deepseek/')) return trimmed;\n return _defaultDeepSeekModel;\n }\n\n String _backendDeepSeekModelRef(String model) {\n final trimmed = model.trim().isEmpty ? _defaultDeepSeekModel : model.trim();\n final modelId = trimmed.contains('/')\n ? trimmed.split('/').last.trim()\n : trimmed;\n final normalizedModelId = modelId.isEmpty ? 'deepseek-chat' : modelId;\n return '$_backendDeepSeekProviderId/$normalizedModelId';\n }\n\n String _deepSeekProxyBaseUrl() {\n final baseUrl = AppConfig.currentAiBackendUrl.replaceFirst(\n RegExp(r'/+$'),\n '',\n );\n return '$baseUrl/api/openclaw/deepseek/v1';\n }\n\n Map<String, dynamic> _mutableMap(Object? value) {\n if (value is Map<String, dynamic>) return Map<String, dynamic>.from(value);\n if (value is Map) return Map<String, dynamic>.from(value);\n return <String, dynamic>{};\n }\n\n List<String> _stringList(Object? value) {\n if (value is! Iterable) return <String>[];\n return value\n .map((item) => item.toString().trim())\n .where((item) => item.isNotEmpty)\n .toList();\n }\n\n List<String> _mergeStringLists(Object? first, Object? second) {\n final merged = <String>{};\n merged.addAll(_stringList(first));\n merged.addAll(_stringList(second));\n return merged.toList();\n }\n\n void _appendBounded(StringBuffer buffer, String chunk) {\n const maxChars = 120000;\n if (chunk.isEmpty || buffer.length >= maxChars) return;\n final remaining = maxChars - buffer.length;\n buffer.write(\n chunk.length <= remaining ? chunk : chunk.substring(0, remaining),\n );\n }\n\n void _captureLogs(Process process) {\n void addLog(String line) {\n final text = line.trim();\n if (text.isEmpty) return;\n _recentLogs.insert(0, text);\n if (_recentLogs.length > 80) {\n _recentLogs.removeRange(80, _recentLogs.length);\n }\n if (kDebugMode) {\n debugPrint('[OpenClaw] $text');\n }\n _diag('process.output', data: {'line': text});\n }\n\n process.stdout\n .transform(utf8.decoder)\n .transform(const LineSplitter())\n .listen(addLog, onError: (_) {});\n process.stderr\n .transform(utf8.decoder)\n .transform(const LineSplitter())\n .listen(addLog, onError: (_) {});\n }\n\n Future<Directory> _stateRoot() async {\n final support = await getApplicationSupportDirectory();\n final dir = Directory(p.join(support.path, 'openclaw_embedded'));\n await dir.create(recursive: true);\n return dir;\n }\n\n Future<_DesktopToolsLaunch> _ensureDesktopTools() async {\n try {\n final status = await DesktopControlBridge.instance.ensureStarted();\n _diag(\n 'desktop-tools.started',\n data: {\n 'bridgeRunning': status.bridgeRunning,\n 'bridgeUri': status.bridgeUri?.toString(),\n 'message': status.message,\n },\n );\n return _DesktopToolsLaunch(\n uri: status.bridgeUri,\n token: await DesktopControlBridge.instance.bridgeToken,\n statusJson: status.toJson(),\n );\n } catch (error) {\n _diag('desktop-tools.error', error: error);\n return _DesktopToolsLaunch(\n uri: null,\n token: null,\n statusJson: {\n 'enabledByBuild': false,\n 'bridgeRunning': false,\n 'message': error.toString(),\n },\n );\n }\n }\n\n Future<Directory> _runtimeDir(\n _OpenClawBundleSpec spec,\n String platformKey,\n ) async {\n final support = await getApplicationSupportDirectory();\n final runtimeBase = Platform.isMacOS\n ? Directory(\n p.join(\n support.parent.path,\n '${p.basename(support.path)}.openclaw_runtime',\n ),\n )\n : Directory(p.join(support.path, 'openclaw_embedded'));\n return Directory(\n p.join(runtimeBase.path, 'runtime', spec.version, platformKey),\n );\n }\n\n Future<Directory?> _bundledRuntimeDir(\n _OpenClawBundleSpec spec,\n String platformKey,\n ) async {\n if (!Platform.isMacOS) return null;\n\n final contentsDir = Directory(\n p.dirname(p.dirname(Platform.resolvedExecutable)),\n );\n final candidates = <Directory>[\n Directory(\n p.join(\n contentsDir.path,\n 'Frameworks',\n 'App.framework',\n 'Resources',\n 'flutter_assets',\n 'assets',\n 'openclaw',\n platformKey,\n ),\n ),\n Directory(\n p.join(\n contentsDir.path,\n 'Frameworks',\n 'App.framework',\n 'Versions',\n 'A',\n 'Resources',\n 'flutter_assets',\n 'assets',\n 'openclaw',\n platformKey,\n ),\n ),\n Directory(\n p.join(\n contentsDir.path,\n 'Resources',\n 'flutter_assets',\n 'assets',\n 'openclaw',\n platformKey,\n ),\n ),\n ];\n\n for (final dir in candidates) {\n final nodePath = File(p.join(dir.path, spec.nodeExecutable));\n final cliPath = File(p.join(dir.path, spec.cliEntrypoint));\n if (await nodePath.exists() && await cliPath.exists()) {\n return dir;\n }\n }\n _diag(\n 'bundle.macos-bundled-runtime-missing',\n data: {\n 'platformKey': platformKey,\n 'contentsDir': contentsDir.path,\n 'candidates': candidates.map((item) => item.path).toList(),\n },\n );\n return null;\n }\n\n Future<Directory?> _runtimeDirOverride(\n _OpenClawBundleSpec spec,\n String platformKey,\n ) async {\n final raw = _configuredRuntimeDirOverride(platformKey);\n if (raw == null) return null;\n\n final dir = Directory(raw);\n final nodePath = File(p.join(dir.path, spec.nodeExecutable));\n final cliPath = File(p.join(dir.path, spec.cliEntrypoint));\n final nodeExists = await nodePath.exists();\n final cliExists = await cliPath.exists();\n _diag(\n 'bundle.override.paths',\n data: {\n 'platformKey': platformKey,\n 'runtimeDir': dir.path,\n 'nodePath': nodePath.path,\n 'nodeExists': nodeExists,\n 'cliPath': cliPath.path,\n 'cliExists': cliExists,\n },\n );\n\n if (!nodeExists || !cliExists) {\n throw OpenClawRuntimeException(\n 'DACHENG_OPENCLAW_RUNTIME_DIR 指向的 OpenClaw runtime 不完整: ${dir.path}',\n );\n }\n return dir;\n }\n\n String? _configuredRuntimeDirOverride(String platformKey) {\n final raw = _runtimeDirOverrideDefine.trim().isNotEmpty\n ? _runtimeDirOverrideDefine.trim()\n : Platform.environment['DACHENG_OPENCLAW_RUNTIME_DIR']?.trim();\n if (raw == null || raw.isEmpty) return null;\n return p.normalize(raw.replaceAll('{platform}', platformKey));\n }\n\n Future<Directory> _legacyRuntimeDir(\n _OpenClawBundleSpec spec,\n String platformKey,\n ) async {\n final support = await getApplicationSupportDirectory();\n return Directory(\n p.join(\n support.path,\n 'openclaw_embedded',\n 'runtime',\n spec.version,\n platformKey,\n ),\n );\n }\n\n OpenClawRuntimeStatus _remember(OpenClawRuntimeStatus status) {\n _lastStatus = status;\n return status;\n }\n\n String? get _platformKey {\n final abi = Abi.current().toString().toLowerCase();\n if (Platform.isMacOS) {\n return abi.contains('arm64') ? 'macos-arm64' : 'macos-x64';\n }\n if (Platform.isWindows) {\n return abi.contains('arm64') ? 'windows-arm64' : 'windows-x64';\n }\n if (Platform.isLinux) {\n return abi.contains('arm64') ? 'linux-arm64' : 'linux-x64';\n }\n return null;\n }\n\n OpenClawRuntimeStatus? get lastStatus => _lastStatus;\n\n void _diag(\n String message, {\n Map<String, Object?> data = const {},\n Object? error,\n StackTrace? stackTrace,\n }) {\n unawaited(\n DiagnosticLogService.instance.log(\n 'openclaw.runtime',\n message,\n data: data,\n error: error,\n stackTrace: stackTrace,\n ),\n );\n }\n}\n\nint? _readInt(Object? value) {\n if (value is int) return value;\n if (value is num) return value.toInt();\n if (value is String) return int.tryParse(value);\n return null;\n}\n\nObject? _decodeJsonOrNull(String body) {\n try {\n return jsonDecode(body);\n } catch (_) {\n return null;\n }\n}\n\nbool _looksLikeHealthPayload(Object? payload) {\n if (payload is! Map) return false;\n if (payload['ok'] == true) return true;\n final status = payload['status']?.toString().toLowerCase();\n return status == 'live' || status == 'ok' || status == 'healthy';\n}\n\nbool _looksLikeModelsPayload(Object? payload) {\n if (payload is List) return true;\n if (payload is! Map) return false;\n return payload['data'] is List ||\n payload['models'] is List ||\n payload['object'] == 'list';\n}\n\nString _previewBody(String body) {\n final trimmed = body.trim();\n return trimmed.length <= 80 ? trimmed : trimmed.substring(0, 80);\n}\n\nString _previewProcessOutput(Object? output) {\n final text = output?.toString().trim() ?? '';\n return text.length <= 200 ? text : text.substring(0, 200);\n}\n"}, "4238282ab85ad52d6b95289dc2069c047a3febf1": {"t": 1785757957.2768784, "n": 92117, "h": "ecf52537032a69bd", "c": "import 'dart:async';\nimport 'dart:convert';\nimport 'dart:ffi' show Abi;\nimport 'dart:io';\nimport 'package:archive/archive_io.dart';\nimport 'package:convert/convert.dart';\nimport 'package:crypto/crypto.dart';\nimport 'package:flutter/foundation.dart';\nimport 'package:flutter/services.dart';\nimport 'package:http/http.dart' as http;\nimport 'package:path/path.dart' as p;\nimport 'package:path_provider/path_provider.dart';\n\nimport '../../core/config/app_config.dart';\nimport '../app_settings.dart';\nimport '../diagnostic_log_service.dart';\nimport '../desktop_control/desktop_control_bridge.dart';\nimport '../desktop_control/desktop_control_policy.dart';\nimport 'openclaw_port_resolver.dart';\n\nenum OpenClawRuntimeState {\n unsupported,\n notBundled,\n stopped,\n starting,\n running,\n failed,\n}\n\nclass OpenClawRuntimeStatus {\n final OpenClawRuntimeState state;\n final String message;\n final int? port;\n final String? platformKey;\n final String? runtimePath;\n final Map<String, dynamic>? desktopToolsStatus;\n final DateTime checkedAt;\n\n const OpenClawRuntimeStatus({\n required this.state,\n required this.message,\n this.port,\n this.platformKey,\n this.runtimePath,\n this.desktopToolsStatus,\n required this.checkedAt,\n });\n\n bool get isHealthy => state == OpenClawRuntimeState.running;\n\n String get label {\n switch (state) {\n case OpenClawRuntimeState.unsupported:\n return '不支持';\n case OpenClawRuntimeState.notBundled:\n return '未内置';\n case OpenClawRuntimeState.stopped:\n return '未启动';\n case OpenClawRuntimeState.starting:\n return '启动中';\n case OpenClawRuntimeState.running:\n return '运行中';\n case OpenClawRuntimeState.failed:\n return '异常';\n }\n }\n}\n\nclass OpenClawGatewayTarget {\n final Uri baseUri;\n final String token;\n final String model;\n final String? modelOverride;\n final Uri? desktopToolsUri;\n final String? desktopToolsToken;\n final Map<String, dynamic>? desktopToolsStatus;\n\n const OpenClawGatewayTarget({\n required this.baseUri,\n required this.token,\n required this.model,\n this.modelOverride,\n this.desktopToolsUri,\n this.desktopToolsToken,\n this.desktopToolsStatus,\n });\n}\n\nclass OpenClawCliResult {\n final List<String> args;\n final int exitCode;\n final String stdout;\n final String stderr;\n final bool timedOut;\n\n const OpenClawCliResult({\n required this.args,\n required this.exitCode,\n required this.stdout,\n required this.stderr,\n this.timedOut = false,\n });\n\n bool get succeeded => exitCode == 0 && !timedOut;\n\n String get command => 'openclaw ${args.join(' ')}';\n\n String get combinedOutput {\n final parts = <String>[\n if (stdout.trim().isNotEmpty) stdout.trimRight(),\n if (stderr.trim().isNotEmpty) stderr.trimRight(),\n ];\n return parts.join('\\n');\n }\n\n OpenClawCliResult append(OpenClawCliResult next) {\n return OpenClawCliResult(\n args: [...args, '&&', ...next.args],\n exitCode: next.exitCode,\n stdout: [\n if (stdout.trim().isNotEmpty) stdout.trimRight(),\n if (next.stdout.trim().isNotEmpty) next.stdout.trimRight(),\n ].join('\\n'),\n stderr: [\n if (stderr.trim().isNotEmpty) stderr.trimRight(),\n if (next.stderr.trim().isNotEmpty) next.stderr.trimRight(),\n ].join('\\n'),\n timedOut: timedOut || next.timedOut,\n );\n }\n}\n\nclass _OpenClawBundleSpec {\n final String version;\n final int defaultPort;\n final String defaultModel;\n final String? defaultModelOverride;\n final String nodeExecutable;\n final String cliEntrypoint;\n final List<String> gatewayArgs;\n final bool downloaded;\n\n const _OpenClawBundleSpec({\n required this.version,\n required this.defaultPort,\n required this.defaultModel,\n this.defaultModelOverride,\n required this.nodeExecutable,\n required this.cliEntrypoint,\n required this.gatewayArgs,\n this.downloaded = false,\n });\n\n Map<String, dynamic> toJson() => {\n 'schema': 1,\n 'version': version,\n 'defaultPort': defaultPort,\n 'defaultModel': defaultModel,\n if (defaultModelOverride != null)\n 'defaultModelOverride': defaultModelOverride,\n 'nodeExecutable': nodeExecutable,\n 'cliEntrypoint': cliEntrypoint,\n 'gatewayArgs': gatewayArgs,\n 'downloaded': downloaded,\n };\n}\n\nclass _OpenClawRuntimeRelease {\n final String version;\n final Uri archiveUri;\n final String sha256Hex;\n final int size;\n final int defaultPort;\n final String defaultModel;\n final String? defaultModelOverride;\n final String nodeExecutable;\n final String cliEntrypoint;\n final List<String> gatewayArgs;\n\n const _OpenClawRuntimeRelease({\n required this.version,\n required this.archiveUri,\n required this.sha256Hex,\n required this.size,\n required this.defaultPort,\n required this.defaultModel,\n this.defaultModelOverride,\n required this.nodeExecutable,\n required this.cliEntrypoint,\n required this.gatewayArgs,\n });\n\n _OpenClawBundleSpec toBundleSpec() => _OpenClawBundleSpec(\n version: version,\n defaultPort: defaultPort,\n defaultModel: defaultModel,\n defaultModelOverride: defaultModelOverride,\n nodeExecutable: nodeExecutable,\n cliEntrypoint: cliEntrypoint,\n gatewayArgs: gatewayArgs,\n downloaded: true,\n );\n}\n\nclass _DesktopToolsLaunch {\n final Uri? uri;\n final String? token;\n final Map<String, dynamic> statusJson;\n\n const _DesktopToolsLaunch({\n required this.uri,\n required this.token,\n required this.statusJson,\n });\n}\n\nclass _OpenClawCliLaunch {\n final _OpenClawBundleSpec spec;\n final Directory runtimeDir;\n final Directory stateRoot;\n final File configPath;\n final String nodePath;\n final String cliPath;\n final int port;\n final String token;\n\n const _OpenClawCliLaunch({\n required this.spec,\n required this.runtimeDir,\n required this.stateRoot,\n required this.configPath,\n required this.nodePath,\n required this.cliPath,\n required this.port,\n required this.token,\n });\n}\n\nclass OpenClawRuntimeException implements Exception {\n final String message;\n\n const OpenClawRuntimeException(this.message);\n\n @override\n String toString() => message;\n}\n\nclass OpenClawRuntime {\n OpenClawRuntime._();\n\n static final OpenClawRuntime instance = OpenClawRuntime._();\n\n static const String _manifestAsset = 'assets/openclaw/bundle_manifest.json';\n static const String _runtimeDirOverrideDefine = String.fromEnvironment(\n 'DACHENG_OPENCLAW_RUNTIME_DIR',\n );\n static const String _defaultGatewayModel =\n AppSettings.defaultOpenClawGatewayModel;\n static const String _defaultDeepSeekModel =\n AppSettings.defaultOpenClawDeepSeekModel;\n static const String _backendDeepSeekProviderId = 'dacheng-deepseek-proxy';\n static const String _desktopToolsPluginId = 'dacheng-desktop-tools';\n static const String _weChatPluginId = 'openclaw-weixin';\n static const String _weChatPluginPackage = '@tencent-weixin/openclaw-weixin';\n static const Duration _startupTimeout = Duration(seconds: 120);\n static const Duration _probeTimeout = Duration(seconds: 3);\n static const Duration _cliDefaultTimeout = Duration(seconds: 45);\n\n Process? _process;\n Future<OpenClawGatewayTarget>? _starting;\n String _processAuthToken = '';\n OpenClawRuntimeStatus? _lastStatus;\n final List<String> _recentLogs = <String>[];\n final OpenClawPortResolver _portResolver = const OpenClawPortResolver();\n\n Future<OpenClawRuntimeStatus> getStatus({bool probe = true}) async {\n final platformKey = _platformKey;\n final port = await AppSettings.getOpenClawGatewayPort();\n _diag(\n 'status.start',\n data: {'platformKey': platformKey, 'port': port, 'probe': probe},\n );\n\n if (platformKey == null) {\n return _remember(\n OpenClawRuntimeStatus(\n state: OpenClawRuntimeState.unsupported,\n message: '当前平台不是 macOS / Windows / Linux 桌面端',\n port: port,\n checkedAt: DateTime.now(),\n ),\n );\n }\n\n try {\n final spec = await _loadSpec(platformKey, checkUpdates: false);\n final overrideRuntimeDir = await _runtimeDirOverride(spec, platformKey);\n final bundledRuntimeDir = overrideRuntimeDir == null\n ? await _bundledRuntimeDir(spec, platformKey)\n : null;\n final runtimeDir =\n overrideRuntimeDir ??\n bundledRuntimeDir ??\n await _runtimeDir(spec, platformKey);\n final nodePath = p.join(runtimeDir.path, spec.nodeExecutable);\n final cliPath = p.join(runtimeDir.path, spec.cliEntrypoint);\n final nodeExists = await File(nodePath).exists();\n final cliExists = await File(cliPath).exists();\n _diag(\n 'status.paths',\n data: {\n 'platformKey': platformKey,\n 'runtimeDir': runtimeDir.path,\n 'nodePath': nodePath,\n 'nodeExists': nodeExists,\n 'cliPath': cliPath,\n 'cliExists': cliExists,\n 'runtimeSource': overrideRuntimeDir != null\n ? 'override'\n : (bundledRuntimeDir == null ? 'cache' : 'bundle'),\n },\n );\n\n if (!nodeExists || !cliExists) {\n final hasBundleAssets = await _hasRequiredBundleAssets(\n platformKey,\n spec,\n );\n _diag(\n 'status.bundle-assets',\n data: {\n 'platformKey': platformKey,\n 'hasBundleAssets': hasBundleAssets,\n 'nodeAsset': 'assets/openclaw/$platformKey/${spec.nodeExecutable}',\n 'cliAsset': 'assets/openclaw/$platformKey/${spec.cliEntrypoint}',\n },\n );\n return _remember(\n OpenClawRuntimeStatus(\n state: hasBundleAssets\n ? OpenClawRuntimeState.stopped\n : OpenClawRuntimeState.notBundled,\n message: hasBundleAssets\n ? '内置 OpenClaw 已随 App 打包,尚未释放或启动'\n : '当前 App 包未包含 $platformKey 的 OpenClaw runtime',\n port: port,\n platformKey: platformKey,\n runtimePath: runtimeDir.path,\n checkedAt: DateTime.now(),\n ),\n );\n }\n\n if (probe) {\n final healthy = await _probe(\n port,\n await AppSettings.getOpenClawGatewayToken(),\n );\n if (healthy) {\n return _remember(\n OpenClawRuntimeStatus(\n state: OpenClawRuntimeState.running,\n message: '本机 OpenClaw Gateway 正在运行',\n port: port,\n platformKey: platformKey,\n runtimePath: runtimeDir.path,\n checkedAt: DateTime.now(),\n ),\n );\n }\n }\n\n return _remember(\n OpenClawRuntimeStatus(\n state: _process == null\n ? OpenClawRuntimeState.stopped\n : OpenClawRuntimeState.starting,\n message: _process == null ? '本机 OpenClaw 未启动' : '本机 OpenClaw 正在启动',\n port: port,\n platformKey: platformKey,\n runtimePath: runtimeDir.path,\n checkedAt: DateTime.now(),\n ),\n );\n } catch (error) {\n _diag('status.error', error: error);\n return _remember(\n OpenClawRuntimeStatus(\n state: OpenClawRuntimeState.failed,\n message: error.toString(),\n port: port,\n platformKey: platformKey,\n checkedAt: DateTime.now(),\n ),\n );\n }\n }\n\n Future<OpenClawGatewayTarget> ensureStarted({\n String? authToken,\n String? username,\n bool isMember = false,\n }) {\n _starting ??= _ensureStartedInternal(\n authToken: authToken,\n username: username,\n isMember: isMember,\n ).whenComplete(() => _starting = null);\n return _starting!;\n }\n\n Future<OpenClawRuntimeStatus> restart() async {\n await stop();\n try {\n await ensureStarted();\n return getStatus();\n } catch (error) {\n return _remember(\n OpenClawRuntimeStatus(\n state: OpenClawRuntimeState.failed,\n message: error.toString(),\n port: await AppSettings.getOpenClawGatewayPort(),\n platformKey: _platformKey,\n checkedAt: DateTime.now(),\n ),\n );\n }\n }\n\n Future<void> stop() async {\n final process = _process;\n _process = null;\n _processAuthToken = '';\n if (process == null) return;\n process.kill();\n try {\n await process.exitCode.timeout(const Duration(seconds: 5));\n } catch (_) {\n process.kill();\n }\n }\n\n Future<OpenClawCliResult> createMobilePairingCode({bool remote = true}) {\n return runCli([\n 'qr',\n if (remote) '--remote',\n '--json',\n ], timeout: const Duration(seconds: 30));\n }\n\n Future<OpenClawCliResult> loginWeChat() {\n return runCli([\n 'channels',\n 'login',\n '--channel',\n _weChatPluginId,\n ], timeout: const Duration(minutes: 3));\n }\n\n Future<OpenClawCliResult> inspectChannels() {\n return runCli([\n 'channels',\n 'status',\n '--probe',\n ], timeout: const Duration(seconds: 40));\n }\n\n Future<OpenClawCliResult> installWeChatPlugin() async {\n final install = await runCli(\n ['plugins', 'install', _weChatPluginPackage, '--force'],\n timeout: const Duration(minutes: 3),\n ensureGateway: false,\n );\n if (!install.succeeded) return install;\n final enable = await runCli(\n ['config', 'set', 'plugins.entries.$_weChatPluginId.enabled', 'true'],\n timeout: const Duration(seconds: 30),\n ensureGateway: false,\n );\n return install.append(enable);\n }\n\n Future<OpenClawCliResult> runCli(\n List<String> args, {\n Duration timeout = _cliDefaultTimeout,\n bool ensureGateway = true,\n }) async {\n if (args.isEmpty) {\n throw const OpenClawRuntimeException('OpenClaw CLI 参数不能为空');\n }\n final launch = await _prepareCliLaunch(ensureGateway: ensureGateway);\n final processArgs = <String>[launch.cliPath, ...args];\n _diag(\n 'cli.start',\n data: {\n 'args': args,\n 'runtimeDir': launch.runtimeDir.path,\n 'configPath': launch.configPath.path,\n 'timeoutSeconds': timeout.inSeconds,\n },\n );\n\n final process = await Process.start(\n launch.nodePath,\n processArgs,\n workingDirectory: launch.runtimeDir.path,\n environment: _buildOpenClawEnvironment(\n runtimeDir: launch.runtimeDir,\n stateRoot: launch.stateRoot,\n configPath: launch.configPath,\n port: launch.port,\n token: launch.token,\n ),\n mode: ProcessStartMode.normal,\n runInShell: false,\n );\n final stdoutBuffer = StringBuffer();\n final stderrBuffer = StringBuffer();\n final stdoutSub = process.stdout\n .transform(utf8.decoder)\n .listen((chunk) => _appendBounded(stdoutBuffer, chunk));\n final stderrSub = process.stderr\n .transform(utf8.decoder)\n .listen((chunk) => _appendBounded(stderrBuffer, chunk));\n\n var timedOut = false;\n int exitCode;\n try {\n exitCode = await process.exitCode.timeout(timeout);\n } on TimeoutException {\n timedOut = true;\n process.kill();\n exitCode = await process.exitCode.timeout(\n const Duration(seconds: 5),\n onTimeout: () => -1,\n );\n } finally {\n await stdoutSub.cancel();\n await stderrSub.cancel();\n }\n\n final result = OpenClawCliResult(\n args: args,\n exitCode: exitCode,\n stdout: stdoutBuffer.toString(),\n stderr: stderrBuffer.toString(),\n timedOut: timedOut,\n );\n _diag(\n result.succeeded ? 'cli.complete' : 'cli.failed',\n data: {\n 'args': args,\n 'exitCode': exitCode,\n 'timedOut': timedOut,\n 'stdoutPreview': _previewProcessOutput(result.stdout),\n 'stderrPreview': _previewProcessOutput(result.stderr),\n },\n );\n return result;\n }\n\n Future<OpenClawGatewayTarget> _ensureStartedInternal({\n String? authToken,\n String? username,\n bool isMember = false,\n }) async {\n final platformKey = _platformKey;\n _diag('ensure-start.start', data: {'platformKey': platformKey});\n if (platformKey == null) {\n throw const OpenClawRuntimeException('当前平台不支持内置 OpenClaw Gateway');\n }\n\n final spec = await _loadSpec(platformKey, checkUpdates: true);\n final requestedPort = await AppSettings.getOpenClawGatewayPort(\n defaultValue: spec.defaultPort,\n );\n final token = await AppSettings.getOpenClawGatewayToken();\n final savedModel = await AppSettings.getOpenClawModel(\n defaultValue: spec.defaultModel,\n );\n final model = _gatewayChatModel(savedModel);\n final requestedAuthToken = authToken?.trim() ?? '';\n final backendDeepSeekModel = _backendDeepSeekModelRef(\n await AppSettings.getOpenClawDeepSeekModel(\n defaultValue: _defaultDeepSeekModelFor(spec.defaultModel),\n ),\n );\n final deepSeekProxyBaseUrl = _deepSeekProxyBaseUrl();\n final modelOverride = await AppSettings.getOpenClawModelOverride(\n defaultValue: spec.defaultModelOverride ?? '',\n );\n final remoteGatewayUrl = await AppSettings.getOpenClawRemoteGatewayUrl();\n final desktopTools = await _ensureDesktopTools();\n _diag(\n 'ensure-start.config',\n data: {\n 'platformKey': platformKey,\n 'requestedPort': requestedPort,\n 'model': model,\n 'rawModel': savedModel,\n 'backendDeepSeekModel': backendDeepSeekModel,\n 'deepSeekProxyBaseUrl': deepSeekProxyBaseUrl,\n 'hasAuthToken': requestedAuthToken.isNotEmpty,\n 'modelOverrideSet': modelOverride.trim().isNotEmpty,\n 'remoteGatewayUrlSet': remoteGatewayUrl.trim().isNotEmpty,\n 'desktopToolsUri': desktopTools.uri?.toString(),\n 'desktopToolsStatus': desktopTools.statusJson,\n },\n );\n\n final canReuseStartedProcess =\n _process != null && _processAuthToken == requestedAuthToken;\n if (canReuseStartedProcess &&\n await _probe(requestedPort, token, context: 'pre-start')) {\n _diag('ensure-start.reuse-existing', data: {'port': requestedPort});\n return OpenClawGatewayTarget(\n baseUri: Uri.parse('http://127.0.0.1:$requestedPort'),\n token: token,\n model: model,\n modelOverride: modelOverride.trim().isEmpty\n ? null\n : modelOverride.trim(),\n desktopToolsUri: desktopTools.uri,\n desktopToolsToken: desktopTools.token,\n desktopToolsStatus: desktopTools.statusJson,\n );\n } else if (_process != null) {\n _diag(\n 'ensure-start.restart-auth-changed',\n data: {\n 'hasPreviousAuthToken': _processAuthToken.isNotEmpty,\n 'hasRequestedAuthToken': requestedAuthToken.isNotEmpty,\n },\n );\n await stop();\n }\n\n final runtimeDir = await _prepareBundle(spec, platformKey);\n final stateRoot = await _stateRoot();\n final desktopToolsPluginDir = await _ensureDesktopToolsPlugin(\n stateRoot: stateRoot,\n );\n if (_process != null) {\n _diag(\n 'process.stop-stale-before-start',\n data: {'requestedPort': requestedPort},\n );\n await stop();\n }\n final portCandidate = await _portResolver.resolve(requestedPort);\n final port = portCandidate.port;\n if (portCandidate.isFallback) {\n await AppSettings.setOpenClawGatewayPort(port);\n _diag(\n 'ensure-start.fallback-port',\n data: {\n 'requestedPort': requestedPort,\n 'selectedPort': port,\n if (portCandidate.reason != null) 'reason': portCandidate.reason,\n },\n );\n } else {\n _diag(\n 'ensure-start.port-bindable',\n data: {'requestedPort': requestedPort, 'selectedPort': port},\n );\n }\n final configPath = await _ensureConfigFile(\n runtimeDir: runtimeDir,\n stateRoot: stateRoot,\n port: port,\n token: token,\n backendDeepSeekModel: backendDeepSeekModel,\n deepSeekProxyBaseUrl: deepSeekProxyBaseUrl,\n remoteGatewayUrl: remoteGatewayUrl,\n extraPluginLoadPaths: [desktopToolsPluginDir.path],\n );\n await _ensureAgentModelConfig(\n stateRoot: stateRoot,\n deepSeekProxyBaseUrl: deepSeekProxyBaseUrl,\n );\n final desktopToolsManifestPath = await _ensureDesktopToolsManifest(\n stateRoot: stateRoot,\n desktopTools: desktopTools,\n );\n\n final nodePath = p.join(runtimeDir.path, spec.nodeExecutable);\n final cliPath = p.join(runtimeDir.path, spec.cliEntrypoint);\n if (!await File(nodePath).exists()) {\n _diag('ensure-start.missing-node', data: {'nodePath': nodePath});\n throw OpenClawRuntimeException('OpenClaw 内置 Node 不存在: $nodePath');\n }\n if (!await File(cliPath).exists()) {\n _diag('ensure-start.missing-cli', data: {'cliPath': cliPath});\n throw OpenClawRuntimeException('OpenClaw CLI 入口不存在: $cliPath');\n }\n\n _remember(\n OpenClawRuntimeStatus(\n state: OpenClawRuntimeState.starting,\n message: '正在启动内置 OpenClaw Gateway',\n port: port,\n platformKey: platformKey,\n runtimePath: runtimeDir.path,\n checkedAt: DateTime.now(),\n ),\n );\n\n final args = <String>[\n cliPath,\n ...spec.gatewayArgs.map((arg) => arg.replaceAll('{port}', '$port')),\n ];\n _recentLogs.clear();\n _diag(\n 'process.starting',\n data: {\n 'nodePath': nodePath,\n 'args': args,\n 'workingDirectory': runtimeDir.path,\n 'configPath': configPath.path,\n 'desktopToolsPluginPath': desktopToolsPluginDir.path,\n 'desktopToolsManifestPath': desktopToolsManifestPath.path,\n 'requestedPort': requestedPort,\n 'selectedPort': port,\n },\n );\n\n final env =\n _buildOpenClawEnvironment(\n runtimeDir: runtimeDir,\n stateRoot: stateRoot,\n configPath: configPath,\n port: port,\n token: token,\n )..addAll({\n 'DACHENG_DESKTOP_TOOLS_ENABLED': desktopTools.uri == null ? '0' : '1',\n 'DACHENG_DESKTOP_TOOLS_MANIFEST': desktopToolsManifestPath.path,\n if (desktopTools.uri != null)\n 'DACHENG_DESKTOP_TOOLS_URL': desktopTools.uri.toString(),\n if (desktopTools.token != null)\n 'DACHENG_DESKTOP_TOOLS_TOKEN': desktopTools.token!,\n 'DACHENG_APP_RUNTIME': '1',\n 'DACHENG_AUTH_TOKEN': (authToken != null && authToken.isNotEmpty)\n ? authToken\n : token,\n if (username != null && username.isNotEmpty)\n 'DACHENG_USERNAME': username,\n 'DACHENG_IS_MEMBER': isMember ? '1' : '0',\n 'DACHENG_OPENCLAW_PROXY_TOKEN': 'dacheng-openclaw-proxy',\n });\n\n _process = await Process.start(\n nodePath,\n args,\n workingDirectory: runtimeDir.path,\n environment: env,\n mode: ProcessStartMode.normal,\n runInShell: false,\n );\n _processAuthToken = requestedAuthToken;\n _captureLogs(_process!);\n _diag('process.started', data: {'pid': _process!.pid, 'port': port});\n\n final deadline = DateTime.now().add(_startupTimeout);\n while (DateTime.now().isBefore(deadline)) {\n if (await _probe(port, token, context: 'startup')) {\n _diag('process.ready', data: {'pid': _process?.pid, 'port': port});\n _remember(\n OpenClawRuntimeStatus(\n state: OpenClawRuntimeState.running,\n message: '本机 OpenClaw Gateway 已启动',\n port: port,\n platformKey: platformKey,\n runtimePath: runtimeDir.path,\n desktopToolsStatus: desktopTools.statusJson,\n checkedAt: DateTime.now(),\n ),\n );\n return OpenClawGatewayTarget(\n baseUri: Uri.parse('http://127.0.0.1:$port'),\n token: token,\n model: model,\n modelOverride: modelOverride.trim().isEmpty\n ? null\n : modelOverride.trim(),\n desktopToolsUri: desktopTools.uri,\n desktopToolsToken: desktopTools.token,\n desktopToolsStatus: desktopTools.statusJson,\n );\n }\n\n if (_process != null) {\n final exited = await _process!.exitCode.timeout(\n const Duration(milliseconds: 1),\n onTimeout: () => -999999,\n );\n if (exited != -999999) {\n final logs = _recentLogs.take(12).join('\\n');\n _diag(\n 'process.exited-early',\n data: {\n 'exitCode': exited,\n 'recentLogs': _recentLogs.take(12).toList(),\n },\n );\n throw OpenClawRuntimeException(\n 'OpenClaw Gateway 提前退出,exitCode=$exited${logs.isEmpty ? '' : '\\n$logs'}',\n );\n }\n }\n await Future<void>.delayed(const Duration(milliseconds: 450));\n }\n\n final logs = _recentLogs.take(12).join('\\n');\n _diag(\n 'process.startup-timeout',\n data: {\n 'timeoutSeconds': _startupTimeout.inSeconds,\n 'recentLogs': _recentLogs.take(12).toList(),\n },\n );\n throw OpenClawRuntimeException(\n 'OpenClaw Gateway 启动超时${logs.isEmpty ? '' : '\\n$logs'}',\n );\n }\n\n Future<_OpenClawBundleSpec> _loadSpec(\n String platformKey, {\n bool checkUpdates = false,\n }) async {\n final raw = await rootBundle.loadString(_manifestAsset);\n final decoded = jsonDecode(raw) as Map<String, dynamic>;\n final platforms = Map<String, dynamic>.from(\n decoded['platforms'] as Map? ?? const {},\n );\n final platform = Map<String, dynamic>.from(\n platforms[platformKey] as Map? ?? const {},\n );\n if (platform.isEmpty) {\n throw OpenClawRuntimeException('bundle_manifest.json 缺少 $platformKey 配置');\n }\n\n final gatewayArgs =\n (platform['gatewayArgs'] as List? ??\n decoded['gatewayArgs'] as List? ??\n const ['gateway', '--port', '{port}'])\n .map((item) => item.toString())\n .toList();\n\n final spec = _OpenClawBundleSpec(\n version: (decoded['version'] ?? 'dev').toString(),\n defaultPort: _readInt(decoded['defaultPort']) ?? 18789,\n defaultModel: (decoded['defaultModel'] ?? 'openclaw/default').toString(),\n defaultModelOverride: decoded['defaultModelOverride']?.toString(),\n nodeExecutable: (platform['nodeExecutable'] ?? 'node/bin/node')\n .toString(),\n cliEntrypoint: (platform['cliEntrypoint'] ?? 'openclaw/bin/openclaw.js')\n .toString(),\n gatewayArgs: gatewayArgs,\n );\n _diag(\n 'manifest.loaded',\n data: {\n 'platformKey': platformKey,\n 'version': spec.version,\n 'defaultPort': spec.defaultPort,\n 'defaultModel': spec.defaultModel,\n 'nodeExecutable': spec.nodeExecutable,\n 'cliEntrypoint': spec.cliEntrypoint,\n 'gatewayArgs': spec.gatewayArgs,\n },\n );\n final allowDownloadedRuntime = await _allowDownloadedRuntime(platformKey);\n var effectiveSpec = spec;\n if (allowDownloadedRuntime) {\n effectiveSpec =\n await _loadActiveDownloadedSpec(platformKey, fallbackSpec: spec) ??\n spec;\n } else {\n await _clearActiveDownloadedRuntimeSpec(\n platformKey: platformKey,\n reason: 'macos-sandbox',\n );\n }\n if (checkUpdates && allowDownloadedRuntime) {\n effectiveSpec =\n await _maybeInstallRuntimeUpdate(\n platformKey: platformKey,\n currentSpec: effectiveSpec,\n ) ??\n effectiveSpec;\n } else if (checkUpdates) {\n _diag(\n 'runtime-update.skip-disabled',\n data: {'platformKey': platformKey, 'reason': 'macos-sandbox'},\n );\n }\n return effectiveSpec;\n }\n\n Future<bool> _allowDownloadedRuntime(String platformKey) async {\n if (!Platform.isMacOS) return true;\n\n final support = await getApplicationSupportDirectory();\n final sandboxContainerId =\n Platform.environment['APP_SANDBOX_CONTAINER_ID']?.trim() ?? '';\n final pathLooksSandboxed = p\n .split(p.normalize(support.path))\n .contains('Containers');\n final sandboxed = sandboxContainerId.isNotEmpty || pathLooksSandboxed;\n if (!sandboxed) return true;\n\n // App Store/TestFlight sandboxed apps cannot reliably execute downloaded\n // Mach-O payloads. Use the bundled runtime that was signed with inherited\n // sandbox entitlements during packaging.\n _diag(\n 'runtime-update.disabled-macos-sandbox',\n data: {\n 'platformKey': platformKey,\n 'supportPath': support.path,\n 'hasSandboxContainerId': sandboxContainerId.isNotEmpty,\n },\n );\n return false;\n }\n\n Future<void> _clearActiveDownloadedRuntimeSpec({\n required String platformKey,\n required String reason,\n }) async {\n final saved = await AppSettings.getOpenClawActiveRuntimeSpec();\n if (saved == null) return;\n await AppSettings.clearOpenClawActiveRuntimeSpec();\n _diag(\n 'runtime-update.active-cleared',\n data: {\n 'platformKey': platformKey,\n 'reason': reason,\n 'version': saved['version']?.toString(),\n 'downloaded': saved['downloaded'] == true,\n },\n );\n }\n\n Future<_OpenClawBundleSpec?> _loadActiveDownloadedSpec(\n String platformKey, {\n required _OpenClawBundleSpec fallbackSpec,\n }) async {\n final saved = await AppSettings.getOpenClawActiveRuntimeSpec();\n if (saved == null) return null;\n final spec = _specFromJson(saved, downloaded: true);\n if (spec == null) {\n await AppSettings.clearOpenClawActiveRuntimeSpec();\n return null;\n }\n final runtimeDir = await _cachedRuntimeDirIfComplete(spec, platformKey);\n if (runtimeDir == null) {\n await AppSettings.clearOpenClawActiveRuntimeSpec();\n _diag(\n 'runtime-update.active-missing',\n data: {'platformKey': platformKey, 'version': spec.version},\n );\n return null;\n }\n _diag(\n 'runtime-update.active-loaded',\n data: {\n 'platformKey': platformKey,\n 'version': spec.version,\n 'runtimeDir': runtimeDir.path,\n 'bundledVersion': fallbackSpec.version,\n },\n );\n return spec;\n }\n\n _OpenClawBundleSpec? _specFromJson(\n Map<String, dynamic> json, {\n required bool downloaded,\n }) {\n final version = json['version']?.toString().trim() ?? '';\n final nodeExecutable = json['nodeExecutable']?.toString().trim() ?? '';\n final cliEntrypoint = json['cliEntrypoint']?.toString().trim() ?? '';\n if (version.isEmpty || nodeExecutable.isEmpty || cliEntrypoint.isEmpty) {\n return null;\n }\n return _OpenClawBundleSpec(\n version: version,\n defaultPort: _readInt(json['defaultPort']) ?? 18789,\n defaultModel: (json['defaultModel'] ?? _defaultGatewayModel).toString(),\n defaultModelOverride: json['defaultModelOverride']?.toString(),\n nodeExecutable: nodeExecutable,\n cliEntrypoint: cliEntrypoint,\n gatewayArgs:\n (json['gatewayArgs'] as List? ??\n const ['gateway', '--port', '{port}', '--force'])\n .map((item) => item.toString())\n .toList(),\n downloaded: downloaded,\n );\n }\n\n Future<_OpenClawBundleSpec?> _maybeInstallRuntimeUpdate({\n required String platformKey,\n required _OpenClawBundleSpec currentSpec,\n }) async {\n if (_configuredRuntimeDirOverride(platformKey) != null) {\n _diag('runtime-update.skip-override', data: {'platformKey': platformKey});\n return null;\n }\n\n try {\n final release = await _fetchRuntimeRelease(\n platformKey: platformKey,\n currentVersion: currentSpec.version,\n );\n if (release == null || release.version == currentSpec.version) {\n return null;\n }\n\n final releaseSpec = release.toBundleSpec();\n final cached = await _cachedRuntimeDirIfComplete(\n releaseSpec,\n platformKey,\n );\n if (cached != null) {\n await AppSettings.setOpenClawActiveRuntimeSpec(releaseSpec.toJson());\n _diag(\n 'runtime-update.use-cached',\n data: {\n 'platformKey': platformKey,\n 'version': release.version,\n 'runtimeDir': cached.path,\n },\n );\n return releaseSpec;\n }\n\n return await _downloadAndInstallRuntimeUpdate(\n release: release,\n platformKey: platformKey,\n );\n } catch (error, stackTrace) {\n _diag(\n 'runtime-update.failed',\n data: {\n 'platformKey': platformKey,\n 'currentVersion': currentSpec.version,\n 'error': error.toString(),\n },\n error: error,\n stackTrace: stackTrace,\n );\n return null;\n }\n }\n\n Future<_OpenClawRuntimeRelease?> _fetchRuntimeRelease({\n required String platformKey,\n required String currentVersion,\n }) async {\n final baseUrl = AppConfig.currentAiBackendUrl.replaceFirst(\n RegExp(r'/+$'),\n '',\n );\n final uri = Uri.parse('$baseUrl/api/openclaw/runtime/manifest').replace(\n queryParameters: {\n 'platform': platformKey,\n 'currentVersion': currentVersion,\n 'appVersion': AppConfig.appVersion,\n },\n );\n final response = await http\n .get(uri, headers: {'Accept': 'application/json'})\n .timeout(const Duration(seconds: 20));\n if (response.statusCode < 200 || response.statusCode >= 300) {\n _diag(\n 'runtime-update.manifest-http',\n data: {'statusCode': response.statusCode, 'uri': uri.toString()},\n );\n return null;\n }\n final decoded = _decodeJsonOrNull(response.body);\n if (decoded is! Map) return null;\n final payload = Map<String, dynamic>.from(decoded);\n if (payload['updateAvailable'] != true) {\n _diag(\n 'runtime-update.none',\n data: {\n 'platformKey': platformKey,\n 'reason': payload['reason']?.toString(),\n 'currentVersion': currentVersion,\n 'latestVersion': payload['latestVersion']?.toString(),\n },\n );\n return null;\n }\n final latest = payload['latest'];\n if (latest is! Map) return null;\n return _releaseFromJson(Map<String, dynamic>.from(latest));\n }\n\n _OpenClawRuntimeRelease? _releaseFromJson(Map<String, dynamic> json) {\n final version = json['version']?.toString().trim() ?? '';\n final archiveUrl = json['archiveUrl']?.toString().trim() ?? '';\n final sha256Hex = json['sha256']?.toString().trim().toLowerCase() ?? '';\n final archiveUri = Uri.tryParse(archiveUrl);\n final gatewayArgs =\n (json['gatewayArgs'] as List? ??\n const ['gateway', '--port', '{port}', '--force'])\n .map((item) => item.toString())\n .toList();\n if (version.isEmpty ||\n archiveUri == null ||\n !archiveUri.hasScheme ||\n !RegExp(r'^[a-f0-9]{64}$').hasMatch(sha256Hex)) {\n _diag(\n 'runtime-update.invalid-release',\n data: {\n 'version': version,\n 'archiveUrlSet': archiveUrl.isNotEmpty,\n 'sha256Length': sha256Hex.length,\n },\n );\n return null;\n }\n return _OpenClawRuntimeRelease(\n version: version,\n archiveUri: archiveUri,\n sha256Hex: sha256Hex,\n size: _readInt(json['size']) ?? 0,\n defaultPort: _readInt(json['defaultPort']) ?? 18789,\n defaultModel: (json['defaultModel'] ?? _defaultGatewayModel).toString(),\n defaultModelOverride: json['defaultModelOverride']?.toString(),\n nodeExecutable: (json['nodeExecutable'] ?? 'node/bin/node').toString(),\n cliEntrypoint: (json['cliEntrypoint'] ?? 'openclaw/openclaw.mjs')\n .toString(),\n gatewayArgs: gatewayArgs,\n );\n }\n\n Future<_OpenClawBundleSpec> _downloadAndInstallRuntimeUpdate({\n required _OpenClawRuntimeRelease release,\n required String platformKey,\n }) async {\n final spec = release.toBundleSpec();\n final runtimeDir = await _runtimeDir(spec, platformKey);\n final support = await getApplicationSupportDirectory();\n final downloadDir = Directory(p.join(support.path, 'openclaw_downloads'));\n await downloadDir.create(recursive: true);\n final archivePath = p.join(\n downloadDir.path,\n '${_safePathPart(release.version)}-$platformKey${_archiveExtension(release.archiveUri)}',\n );\n final archiveFile = File(archivePath);\n final digest = await _downloadRuntimeArchive(release, archiveFile);\n if (digest != release.sha256Hex) {\n await _deleteFileIfExists(archiveFile);\n throw OpenClawRuntimeException(\n 'OpenClaw runtime 校验失败: expected=${release.sha256Hex}, actual=$digest',\n );\n }\n\n final extractDir = Directory(\n p.join(\n downloadDir.path,\n '${_safePathPart(release.version)}-$platformKey-extract',\n ),\n );\n final stagingDir = Directory(\n p.join(\n downloadDir.path,\n '${_safePathPart(release.version)}-$platformKey-stage',\n ),\n );\n if (await extractDir.exists()) await extractDir.delete(recursive: true);\n if (await stagingDir.exists()) await stagingDir.delete(recursive: true);\n await extractDir.create(recursive: true);\n await _extractRuntimeArchive(archiveFile, extractDir);\n\n final extractedRoot = await _findExtractedRuntimeRoot(\n extractDir: extractDir,\n spec: spec,\n );\n if (extractedRoot == null) {\n throw OpenClawRuntimeException('下载的 OpenClaw runtime 不包含必要的 node/cli 文件');\n }\n\n await runtimeDir.parent.create(recursive: true);\n if (await runtimeDir.exists()) await runtimeDir.delete(recursive: true);\n await _moveOrCopyRuntimeDirectory(extractedRoot, stagingDir);\n await stagingDir.rename(runtimeDir.path);\n final marker = File(p.join(runtimeDir.path, '.bundle_ready'));\n await marker.writeAsString(\n const JsonEncoder.withIndent(' ').convert({\n 'installedAt': DateTime.now().toIso8601String(),\n 'version': release.version,\n 'platform': platformKey,\n 'sha256': digest,\n 'source': release.archiveUri.toString(),\n }),\n );\n await _repairRuntimeLaunchMetadata(\n runtimeDir: runtimeDir,\n nodePath: File(p.join(runtimeDir.path, spec.nodeExecutable)),\n platformKey: platformKey,\n cached: false,\n );\n await AppSettings.setOpenClawActiveRuntimeSpec(spec.toJson());\n await _deleteDirectoryIfExists(extractDir);\n await _deleteFileIfExists(archiveFile);\n _diag(\n 'runtime-update.installed',\n data: {\n 'platformKey': platformKey,\n 'version': release.version,\n 'runtimeDir': runtimeDir.path,\n 'size': release.size,\n },\n );\n return spec;\n }\n\n Future<String> _downloadRuntimeArchive(\n _OpenClawRuntimeRelease release,\n File destination,\n ) async {\n final client = http.Client();\n IOSink? output;\n try {\n final request = http.Request('GET', release.archiveUri)\n ..headers['Accept'] = 'application/octet-stream';\n final response = await client\n .send(request)\n .timeout(const Duration(seconds: 20));\n if (response.statusCode < 200 || response.statusCode >= 300) {\n throw OpenClawRuntimeException(\n 'OpenClaw runtime 下载失败 (${response.statusCode})',\n );\n }\n await destination.parent.create(recursive: true);\n output = destination.openWrite();\n final digestSink = AccumulatorSink<Digest>();\n final hashSink = sha256.startChunkedConversion(digestSink);\n var bytes = 0;\n await for (final chunk in response.stream.timeout(\n const Duration(minutes: 5),\n )) {\n bytes += chunk.length;\n hashSink.add(chunk);\n output.add(chunk);\n }\n await output.flush();\n await output.close();\n output = null;\n hashSink.close();\n final digest = digestSink.events.single.toString();\n _diag(\n 'runtime-update.downloaded',\n data: {\n 'archiveUri': release.archiveUri.toString(),\n 'bytes': bytes,\n 'sha256': digest,\n },\n );\n return digest;\n } finally {\n await output?.close().catchError((_) {});\n client.close();\n }\n }\n\n Future<void> _extractRuntimeArchive(\n File archiveFile,\n Directory destination,\n ) async {\n final lowerPath = archiveFile.path.toLowerCase();\n if (Platform.isMacOS && lowerPath.endsWith('.zip')) {\n try {\n final result = await Process.run('/usr/bin/ditto', [\n '-x',\n '-k',\n archiveFile.path,\n destination.path,\n ]).timeout(const Duration(minutes: 5));\n _diag(\n 'runtime-update.extract-native',\n data: {\n 'archivePath': archiveFile.path,\n 'destination': destination.path,\n 'exitCode': result.exitCode,\n if ((result.stderr as Object).toString().isNotEmpty)\n 'stderr': _previewProcessOutput(result.stderr),\n },\n );\n if (result.exitCode == 0) return;\n } catch (error, stackTrace) {\n _diag(\n 'runtime-update.extract-native-failed',\n data: {\n 'archivePath': archiveFile.path,\n 'destination': destination.path,\n 'error': error.toString(),\n },\n error: error,\n stackTrace: stackTrace,\n );\n }\n }\n\n await extractFileToDisk(archiveFile.path, destination.path);\n _diag(\n 'runtime-update.extract-dart',\n data: {'archivePath': archiveFile.path, 'destination': destination.path},\n );\n }\n\n Future<Directory?> _findExtractedRuntimeRoot({\n required Directory extractDir,\n required _OpenClawBundleSpec spec,\n }) async {\n final direct = await _runtimeRootIfComplete(extractDir, spec);\n if (direct != null) return direct;\n await for (final entity in extractDir.list(followLinks: false)) {\n if (entity is! Directory) continue;\n final nested = await _runtimeRootIfComplete(entity, spec);\n if (nested != null) return nested;\n }\n return null;\n }\n\n Future<Directory?> _runtimeRootIfComplete(\n Directory dir,\n _OpenClawBundleSpec spec,\n ) async {\n final nodePath = File(p.join(dir.path, spec.nodeExecutable));\n final cliPath = File(p.join(dir.path, spec.cliEntrypoint));\n if (await nodePath.exists() && await cliPath.exists()) return dir;\n return null;\n }\n\n Future<Directory?> _cachedRuntimeDirIfComplete(\n _OpenClawBundleSpec spec,\n String platformKey,\n ) async {\n final runtimeDir = await _runtimeDir(spec, platformKey);\n final marker = File(p.join(runtimeDir.path, '.bundle_ready'));\n final nodePath = File(p.join(runtimeDir.path, spec.nodeExecutable));\n final cliPath = File(p.join(runtimeDir.path, spec.cliEntrypoint));\n if (await marker.exists() &&\n await nodePath.exists() &&\n await cliPath.exists()) {\n return runtimeDir;\n }\n return null;\n }\n\n String _safePathPart(String value) {\n final safe = value.replaceAll(RegExp(r'[^A-Za-z0-9_.-]+'), '-');\n return safe.isEmpty ? 'runtime' : safe;\n }\n\n String _archiveExtension(Uri uri) {\n final lower = uri.path.toLowerCase();\n if (lower.endsWith('.tar.gz')) return '.tar.gz';\n if (lower.endsWith('.tgz')) return '.tgz';\n if (lower.endsWith('.tar')) return '.tar';\n return '.zip';\n }\n\n Future<void> _deleteFileIfExists(File file) async {\n try {\n if (await file.exists()) await file.delete();\n } catch (_) {}\n }\n\n Future<void> _deleteDirectoryIfExists(Directory dir) async {\n try {\n if (await dir.exists()) await dir.delete(recursive: true);\n } catch (_) {}\n }\n\n Future<Directory> _prepareBundle(\n _OpenClawBundleSpec spec,\n String platformKey,\n ) async {\n final overrideRuntimeDir = await _runtimeDirOverride(spec, platformKey);\n if (overrideRuntimeDir != null) {\n final nodePath = File(\n p.join(overrideRuntimeDir.path, spec.nodeExecutable),\n );\n await _repairRuntimeLaunchMetadata(\n runtimeDir: overrideRuntimeDir,\n nodePath: nodePath,\n platformKey: platformKey,\n cached: true,\n );\n _diag(\n 'bundle.launch.override-runtime',\n data: {\n 'platformKey': platformKey,\n 'runtimeDir': overrideRuntimeDir.path,\n },\n );\n return overrideRuntimeDir;\n }\n\n if (spec.downloaded) {\n final downloadedRuntimeDir = await _cachedRuntimeDirIfComplete(\n spec,\n platformKey,\n );\n if (downloadedRuntimeDir != null) {\n await _repairRuntimeLaunchMetadata(\n runtimeDir: downloadedRuntimeDir,\n nodePath: File(\n p.join(downloadedRuntimeDir.path, spec.nodeExecutable),\n ),\n platformKey: platformKey,\n cached: true,\n );\n _diag(\n 'bundle.launch.downloaded-runtime',\n data: {\n 'platformKey': platformKey,\n 'version': spec.version,\n 'runtimeDir': downloadedRuntimeDir.path,\n },\n );\n return downloadedRuntimeDir;\n }\n await AppSettings.clearOpenClawActiveRuntimeSpec();\n throw OpenClawRuntimeException(\n '已下载的 OpenClaw runtime 不完整,已回退到内置版本,请重新启动。',\n );\n }\n\n final bundledRuntimeDir = await _bundledRuntimeDir(spec, platformKey);\n if (bundledRuntimeDir != null) {\n _diag(\n 'bundle.launch.bundled-runtime',\n data: {\n 'platformKey': platformKey,\n 'runtimeDir': bundledRuntimeDir.path,\n },\n );\n return bundledRuntimeDir;\n }\n\n if (Platform.isMacOS) {\n throw OpenClawRuntimeException(\n '当前安装包没有可执行的 $platformKey OpenClaw runtime。请先运行 scripts/build_openclaw_desktop_bundle.sh $platformKey,再通过 flutter run 启动。',\n );\n }\n\n final runtimeDir = await _runtimeDir(spec, platformKey);\n final marker = File(p.join(runtimeDir.path, '.bundle_ready'));\n final nodePath = File(p.join(runtimeDir.path, spec.nodeExecutable));\n final cliPath = File(p.join(runtimeDir.path, spec.cliEntrypoint));\n\n if (await marker.exists() &&\n await nodePath.exists() &&\n await cliPath.exists()) {\n await _repairRuntimeLaunchMetadata(\n runtimeDir: runtimeDir,\n nodePath: nodePath,\n platformKey: platformKey,\n cached: true,\n );\n _diag(\n 'bundle.prepare.cached',\n data: {\n 'platformKey': platformKey,\n 'runtimeDir': runtimeDir.path,\n 'marker': marker.path,\n },\n );\n return runtimeDir;\n }\n\n final legacyRuntimeDir = await _legacyRuntimeDir(spec, platformKey);\n if (legacyRuntimeDir.path != runtimeDir.path &&\n await File(\n p.join(legacyRuntimeDir.path, spec.nodeExecutable),\n ).exists() &&\n await File(\n p.join(legacyRuntimeDir.path, spec.cliEntrypoint),\n ).exists()) {\n _diag(\n 'bundle.prepare.migrate-legacy',\n data: {\n 'platformKey': platformKey,\n 'from': legacyRuntimeDir.path,\n 'to': runtimeDir.path,\n },\n );\n if (await runtimeDir.exists()) {\n await runtimeDir.delete(recursive: true);\n }\n await _copyRuntimeDirectory(legacyRuntimeDir, runtimeDir);\n await _repairRuntimeLaunchMetadata(\n runtimeDir: runtimeDir,\n nodePath: nodePath,\n platformKey: platformKey,\n cached: false,\n );\n await marker.writeAsString(DateTime.now().toIso8601String());\n _diag(\n 'bundle.prepare.migrate-complete',\n data: {\n 'platformKey': platformKey,\n 'runtimeDir': runtimeDir.path,\n 'nodeExists': await nodePath.exists(),\n 'cliExists': await cliPath.exists(),\n },\n );\n return runtimeDir;\n }\n\n final prefix = 'assets/openclaw/$platformKey/';\n final assets = await _listAssets(prefix);\n final hasNodeAsset = assets.contains('$prefix${spec.nodeExecutable}');\n final hasCliAsset = assets.contains('$prefix${spec.cliEntrypoint}');\n _diag(\n 'bundle.prepare.assets',\n data: {\n 'platformKey': platformKey,\n 'prefix': prefix,\n 'assetCount': assets.length,\n 'hasNodeAsset': hasNodeAsset,\n 'hasCliAsset': hasCliAsset,\n 'nodeAsset': '$prefix${spec.nodeExecutable}',\n 'cliAsset': '$prefix${spec.cliEntrypoint}',\n 'sample': assets.take(8).toList(),\n },\n );\n if (!hasNodeAsset || !hasCliAsset) {\n throw OpenClawRuntimeException(\n '当前安装包没有内置 $platformKey 的 OpenClaw runtime。请在 release 构建中运行 scripts/build_openclaw_desktop_bundle.sh 后再打包。',\n );\n }\n\n if (await runtimeDir.exists()) {\n _diag('bundle.prepare.remove-old', data: {'runtimeDir': runtimeDir.path});\n await runtimeDir.delete(recursive: true);\n }\n await runtimeDir.create(recursive: true);\n\n for (final asset in assets) {\n final relative = asset.substring(prefix.length);\n if (relative.isEmpty || relative.endsWith('/')) continue;\n final bytes = await rootBundle.load(asset);\n final file = File(p.join(runtimeDir.path, relative));\n await file.parent.create(recursive: true);\n await file.writeAsBytes(\n bytes.buffer.asUint8List(bytes.offsetInBytes, bytes.lengthInBytes),\n flush: false,\n );\n }\n\n await _repairRuntimeLaunchMetadata(\n runtimeDir: runtimeDir,\n nodePath: nodePath,\n platformKey: platformKey,\n cached: false,\n );\n await marker.writeAsString(DateTime.now().toIso8601String());\n _diag(\n 'bundle.prepare.complete',\n data: {\n 'platformKey': platformKey,\n 'runtimeDir': runtimeDir.path,\n 'assetCount': assets.length,\n 'nodeExists': await nodePath.exists(),\n 'nodeSize': await nodePath.exists() ? await nodePath.length() : 0,\n 'cliExists': await cliPath.exists(),\n 'cliSize': await cliPath.exists() ? await cliPath.length() : 0,\n },\n );\n return runtimeDir;\n }\n\n Future<void> _copyRuntimeDirectory(\n Directory source,\n Directory destination,\n ) async {\n await destination.create(recursive: true);\n await for (final entity in source.list(\n recursive: true,\n followLinks: false,\n )) {\n final relative = p.relative(entity.path, from: source.path);\n if (relative == '.') continue;\n final target = p.join(destination.path, relative);\n if (entity is Directory) {\n await Directory(target).create(recursive: true);\n } else if (entity is File) {\n final targetFile = File(target);\n await targetFile.parent.create(recursive: true);\n await entity.openRead().pipe(targetFile.openWrite());\n }\n }\n }\n\n Future<void> _moveOrCopyRuntimeDirectory(\n Directory source,\n Directory destination,\n ) async {\n if (await destination.exists()) {\n await destination.delete(recursive: true);\n }\n await destination.parent.create(recursive: true);\n try {\n await source.rename(destination.path);\n return;\n } on FileSystemException {\n await _copyRuntimeDirectory(source, destination);\n }\n }\n\n Future<void> _repairRuntimeLaunchMetadata({\n required Directory runtimeDir,\n required File nodePath,\n required String platformKey,\n required bool cached,\n }) async {\n if (Platform.isWindows) return;\n\n final chmodResult = await Process.run('/bin/chmod', ['+x', nodePath.path]);\n ProcessResult? xattrResult;\n if (Platform.isMacOS) {\n // Files created by a quarantined app can inherit the quarantine xattr.\n // macOS then rejects Process.start with \"Operation not permitted\".\n xattrResult = await Process.run('/usr/bin/xattr', [\n '-dr',\n 'com.apple.quarantine',\n runtimeDir.path,\n ]);\n await Process.run('/usr/bin/xattr', [\n '-dr',\n 'com.apple.provenance',\n runtimeDir.path,\n ]);\n }\n\n _diag(\n 'bundle.prepare.launch-metadata',\n data: {\n 'platformKey': platformKey,\n 'runtimeDir': runtimeDir.path,\n 'nodePath': nodePath.path,\n 'cached': cached,\n 'chmodExitCode': chmodResult.exitCode,\n if ((chmodResult.stderr as Object).toString().isNotEmpty)\n 'chmodStderr': _previewProcessOutput(chmodResult.stderr),\n if (xattrResult != null) 'xattrExitCode': xattrResult.exitCode,\n if (xattrResult != null &&\n (xattrResult.stderr as Object).toString().isNotEmpty)\n 'xattrStderr': _previewProcessOutput(xattrResult.stderr),\n },\n );\n }\n\n Future<bool> _hasRequiredBundleAssets(\n String platformKey,\n _OpenClawBundleSpec spec,\n ) async {\n final prefix = 'assets/openclaw/$platformKey/';\n final assets = await _listAssets(prefix);\n final hasRequired =\n assets.contains('$prefix${spec.nodeExecutable}') &&\n assets.contains('$prefix${spec.cliEntrypoint}');\n _diag(\n 'bundle.has-required-assets',\n data: {\n 'platformKey': platformKey,\n 'assetCount': assets.length,\n 'hasRequired': hasRequired,\n },\n );\n return hasRequired;\n }\n\n Future<List<String>> _listAssets(String prefix) async {\n final assets = <String>{};\n var jsonCount = 0;\n var binCount = 0;\n var indexCount = 0;\n final failures = <String>[];\n\n try {\n final raw = await rootBundle.loadString('AssetManifest.json');\n final decoded = jsonDecode(raw) as Map<String, dynamic>;\n final items = decoded.keys\n .where((key) => key.startsWith(prefix))\n .toList();\n jsonCount = items.length;\n assets.addAll(items);\n } catch (error) {\n failures.add('AssetManifest.json: $error');\n // Newer Flutter release builds may only include AssetManifest.bin.\n }\n\n try {\n final manifest = await AssetManifest.loadFromAssetBundle(rootBundle);\n final items = manifest\n .listAssets()\n .where((key) => key.startsWith(prefix))\n .toList();\n binCount = items.length;\n assets.addAll(items);\n } catch (error) {\n failures.add('AssetManifest.bin: $error');\n // App Store archives patched after flutter build carry a JSON/index file.\n }\n\n try {\n final raw = await rootBundle.loadString(\n 'assets/openclaw/asset_index.json',\n );\n final decoded = jsonDecode(raw);\n final indexedAssets = decoded is Map\n ? decoded['assets']\n : decoded is List\n ? decoded\n : const [];\n if (indexedAssets is List) {\n final items = indexedAssets\n .map((item) => item.toString())\n .where((key) => key.startsWith(prefix))\n .toList();\n indexCount = items.length;\n assets.addAll(items);\n }\n } catch (error) {\n failures.add('asset_index.json: $error');\n // Older builds do not have an OpenClaw asset index.\n }\n\n final sortedAssets = assets.toList()..sort();\n _diag(\n 'asset.list',\n data: {\n 'prefix': prefix,\n 'total': sortedAssets.length,\n 'jsonCount': jsonCount,\n 'binCount': binCount,\n 'indexCount': indexCount,\n if (failures.isNotEmpty) 'failures': failures,\n 'sample': sortedAssets.take(8).toList(),\n },\n );\n return sortedAssets;\n }\n\n Future<bool> _probe(\n int port,\n String token, {\n String context = 'probe',\n }) async {\n try {\n final healthResponse = await http\n .get(\n Uri.parse('http://127.0.0.1:$port/health'),\n headers: {'Authorization': 'Bearer $token'},\n )\n .timeout(_probeTimeout);\n final healthPayload = _decodeJsonOrNull(healthResponse.body);\n if (healthResponse.statusCode < 200 ||\n healthResponse.statusCode >= 300 ||\n !_looksLikeHealthPayload(healthPayload)) {\n _diag(\n 'probe.health-unhealthy',\n data: {\n 'context': context,\n 'port': port,\n 'statusCode': healthResponse.statusCode,\n 'contentType': healthResponse.headers['content-type'],\n 'bodyPreview': _previewBody(healthResponse.body),\n },\n );\n return false;\n }\n\n final modelsResponse = await http\n .get(\n Uri.parse('http://127.0.0.1:$port/v1/models'),\n headers: {'Authorization': 'Bearer $token'},\n )\n .timeout(_probeTimeout);\n final modelsPayload = _decodeJsonOrNull(modelsResponse.body);\n if (modelsResponse.statusCode < 200 ||\n modelsResponse.statusCode >= 300 ||\n !_looksLikeModelsPayload(modelsPayload)) {\n _diag(\n 'probe.models-unhealthy',\n data: {\n 'context': context,\n 'port': port,\n 'statusCode': modelsResponse.statusCode,\n 'contentType': modelsResponse.headers['content-type'],\n 'bodyPreview': _previewBody(modelsResponse.body),\n },\n );\n return false;\n }\n\n // `/v1/models` can be healthy while `/v1/chat/completions` is disabled\n // by an old config. Use GET so the probe only checks route wiring and\n // never starts a real model request.\n final chatResponse = await http\n .get(\n Uri.parse('http://127.0.0.1:$port/v1/chat/completions'),\n headers: {'Authorization': 'Bearer $token'},\n )\n .timeout(_probeTimeout);\n final healthy =\n chatResponse.statusCode != 401 &&\n chatResponse.statusCode != 403 &&\n chatResponse.statusCode != 404;\n _diag(\n healthy ? 'probe.healthy' : 'probe.chat-unhealthy',\n data: {\n 'context': context,\n 'port': port,\n 'chatStatusCode': chatResponse.statusCode,\n },\n );\n return healthy;\n } catch (error) {\n if (context != 'startup') {\n _diag(\n 'probe.error',\n data: {'context': context, 'port': port},\n error: error,\n );\n }\n return false;\n }\n }\n\n Future<File> _ensureConfigFile({\n required Directory runtimeDir,\n required Directory stateRoot,\n required int port,\n required String token,\n required String backendDeepSeekModel,\n required String deepSeekProxyBaseUrl,\n required String remoteGatewayUrl,\n List<String> extraPluginLoadPaths = const [],\n }) async {\n final configDir = Directory(p.join(stateRoot.path, 'config'));\n await configDir.create(recursive: true);\n final workspace = Directory(p.join(stateRoot.path, 'workspace'));\n await workspace.create(recursive: true);\n final configPath = File(p.join(configDir.path, 'openclaw.json'));\n final bundledPluginLoadPaths = await _bundledPluginLoadPaths(runtimeDir);\n final pluginLoadPaths = _mergeStringLists(\n extraPluginLoadPaths,\n bundledPluginLoadPaths,\n );\n final hasWeChatPlugin = pluginLoadPaths.any(\n (path) => p.basename(path) == _weChatPluginId,\n );\n final remoteUrl = remoteGatewayUrl.trim();\n\n final config = <String, dynamic>{\n 'gateway': {\n 'mode': 'local',\n 'port': port,\n 'bind': 'loopback',\n 'auth': {'mode': 'token', 'token': token},\n if (remoteUrl.isNotEmpty) 'remote': {'enabled': true, 'url': remoteUrl},\n 'http': {\n 'endpoints': {\n 'chatCompletions': {'enabled': true},\n 'responses': {'enabled': true},\n },\n },\n },\n 'models': {\n 'mode': 'merge',\n 'providers': {\n _backendDeepSeekProviderId: {\n 'baseUrl': deepSeekProxyBaseUrl,\n 'api': 'openai-completions',\n 'apiKey': 'DACHENG_OPENCLAW_PROXY_TOKEN',\n 'authHeader': true,\n 'headers': {'x-dacheng-auth-token': 'DACHENG_AUTH_TOKEN'},\n 'models': [\n {\n 'id': 'deepseek-chat',\n 'name': 'DeepSeek Chat',\n 'contextWindow': 131072,\n 'maxTokens': 8192,\n 'input': ['text'],\n 'compat': {\n 'requiresStringContent': true,\n 'strictMessageKeys': true,\n },\n },\n {\n 'id': 'deepseek-reasoner',\n 'name': 'DeepSeek Reasoner',\n 'contextWindow': 131072,\n 'maxTokens': 8192,\n 'reasoning': true,\n 'input': ['text'],\n 'compat': {\n 'requiresStringContent': true,\n 'strictMessageKeys': true,\n },\n },\n ],\n },\n },\n },\n 'browser': {\n 'enabled': true,\n 'defaultProfile': 'openclaw',\n 'headless': false,\n 'ssrfPolicy': {'dangerouslyAllowPrivateNetwork': true},\n },\n 'canvas': {\n 'enabled': true,\n 'host': {'enabled': true, 'root': p.join(stateRoot.path, 'canvas')},\n },\n 'tools': {\n 'profile': 'full',\n 'exec': {'host': 'gateway', 'security': 'full', 'ask': 'off'},\n },\n 'plugins': {\n 'enabled': true,\n 'deny': <String>[],\n 'load': {'paths': pluginLoadPaths},\n 'slots': {'memory': 'memory-core'},\n 'entries': <String, dynamic>{\n 'memory-core': {'enabled': true},\n 'bonjour': {'enabled': false},\n 'browser': {'enabled': true},\n 'canvas': {'enabled': true},\n 'device-pair': {'enabled': true},\n 'file-transfer': {'enabled': true},\n 'phone-control': {'enabled': true},\n 'talk-voice': {'enabled': true},\n _desktopToolsPluginId: {'enabled': true},\n if (hasWeChatPlugin) _weChatPluginId: {'enabled': true},\n },\n },\n 'channels': {\n 'defaults': {'groupPolicy': 'allowlist'},\n if (hasWeChatPlugin)\n _weChatPluginId: {\n 'enabled': true,\n 'dmPolicy': 'pairing',\n 'allowFrom': <String>[],\n 'accounts': <String, dynamic>{},\n },\n },\n 'agents': {\n 'defaults': {\n 'workspace': workspace.path,\n 'model': {'primary': backendDeepSeekModel},\n },\n 'list': [\n {\n 'id': 'dacheng',\n 'default': true,\n 'workspace': workspace.path,\n 'model': {'primary': backendDeepSeekModel},\n },\n ],\n },\n };\n\n // Repair older embedded configs in-place. OpenClaw 2026.6 requires\n // gateway.mode=local, and the home screen requires the OpenAI-compatible\n // chat endpoint. Preserve unrelated user edits while fixing the fields\n // needed for the app-owned embedded gateway to start reliably.\n final merged = await _mergeEmbeddedConfig(configPath, config);\n await configPath.writeAsString(\n const JsonEncoder.withIndent(' ').convert(merged),\n );\n _diag(\n 'config.written',\n data: {\n 'configPath': configPath.path,\n 'port': port,\n 'gatewayMode': _mutableMap(merged['gateway'])['mode'],\n 'backendDeepSeekModel': backendDeepSeekModel,\n 'deepSeekProxyBaseUrl': deepSeekProxyBaseUrl,\n 'remoteGatewayUrlSet': remoteUrl.isNotEmpty,\n 'pluginLoadPaths': pluginLoadPaths,\n 'hasWeChatPlugin': hasWeChatPlugin,\n },\n );\n return configPath;\n }\n\n Future<File> _ensureDesktopToolsManifest({\n required Directory stateRoot,\n required _DesktopToolsLaunch desktopTools,\n }) async {\n final configDir = Directory(p.join(stateRoot.path, 'config'));\n await configDir.create(recursive: true);\n final manifestPath = File(p.join(configDir.path, 'desktop_tools.json'));\n final tools =\n DesktopControlPolicy.supportedTools\n .map(\n (name) => {\n 'name': name,\n 'endpoint': '/v1/tools/execute',\n 'readOnly': DesktopControlPolicy.isReadOnly(name),\n 'requiresConfirmation':\n DesktopControlPolicy.requiresConfirmation(name),\n },\n )\n .toList()\n ..sort(\n (a, b) => a['name'].toString().compareTo(b['name'].toString()),\n );\n\n await manifestPath.writeAsString(\n const JsonEncoder.withIndent(' ').convert({\n 'version': 1,\n 'transport': 'http-loopback',\n 'baseUrl': desktopTools.uri?.toString(),\n 'auth': {'type': 'bearer', 'env': 'DACHENG_DESKTOP_TOOLS_TOKEN'},\n 'status': desktopTools.statusJson,\n 'tools': tools,\n }),\n );\n _diag(\n 'desktop-tools.manifest-written',\n data: {\n 'manifestPath': manifestPath.path,\n 'hasBaseUrl': desktopTools.uri != null,\n },\n );\n return manifestPath;\n }\n\n Future<File> _ensureAgentModelConfig({\n required Directory stateRoot,\n required String deepSeekProxyBaseUrl,\n }) async {\n final agentDir = Directory(\n p.join(stateRoot.path, 'state', 'agents', 'dacheng', 'agent'),\n );\n await agentDir.create(recursive: true);\n final modelsPath = File(p.join(agentDir.path, 'models.json'));\n final providerConfig = <String, dynamic>{\n 'baseUrl': deepSeekProxyBaseUrl,\n 'api': 'openai-completions',\n 'apiKey': 'DACHENG_OPENCLAW_PROXY_TOKEN',\n 'authHeader': true,\n 'headers': {'x-dacheng-auth-token': 'DACHENG_AUTH_TOKEN'},\n 'models': [\n {\n 'id': 'deepseek-chat',\n 'name': 'DeepSeek Chat',\n 'contextWindow': 131072,\n 'maxTokens': 8192,\n 'input': ['text'],\n 'compat': {'requiresStringContent': true, 'strictMessageKeys': true},\n },\n {\n 'id': 'deepseek-reasoner',\n 'name': 'DeepSeek Reasoner',\n 'contextWindow': 131072,\n 'maxTokens': 8192,\n 'reasoning': true,\n 'input': ['text'],\n 'compat': {'requiresStringContent': true, 'strictMessageKeys': true},\n },\n ],\n };\n await modelsPath.writeAsString(\n const JsonEncoder.withIndent(' ').convert({\n 'providers': {_backendDeepSeekProviderId: providerConfig},\n }),\n );\n _diag(\n 'agent-models.written',\n data: {\n 'modelsPath': modelsPath.path,\n 'provider': _backendDeepSeekProviderId,\n 'deepSeekProxyBaseUrl': deepSeekProxyBaseUrl,\n },\n );\n return modelsPath;\n }\n\n Future<_OpenClawCliLaunch> _prepareCliLaunch({\n required bool ensureGateway,\n }) async {\n if (ensureGateway) {\n await ensureStarted();\n }\n final platformKey = _platformKey;\n if (platformKey == null) {\n throw const OpenClawRuntimeException('当前平台不支持内置 OpenClaw Gateway');\n }\n final spec = await _loadSpec(platformKey, checkUpdates: false);\n final runtimeDir = await _prepareBundle(spec, platformKey);\n final stateRoot = await _stateRoot();\n final desktopToolsPluginDir = await _ensureDesktopToolsPlugin(\n stateRoot: stateRoot,\n );\n final port = await AppSettings.getOpenClawGatewayPort(\n defaultValue: spec.defaultPort,\n );\n final token = await AppSettings.getOpenClawGatewayToken();\n final backendDeepSeekModel = _backendDeepSeekModelRef(\n await AppSettings.getOpenClawDeepSeekModel(\n defaultValue: _defaultDeepSeekModelFor(spec.defaultModel),\n ),\n );\n final configPath = await _ensureConfigFile(\n runtimeDir: runtimeDir,\n stateRoot: stateRoot,\n port: port,\n token: token,\n backendDeepSeekModel: backendDeepSeekModel,\n deepSeekProxyBaseUrl: _deepSeekProxyBaseUrl(),\n remoteGatewayUrl: await AppSettings.getOpenClawRemoteGatewayUrl(),\n extraPluginLoadPaths: [desktopToolsPluginDir.path],\n );\n final nodePath = p.join(runtimeDir.path, spec.nodeExecutable);\n final cliPath = p.join(runtimeDir.path, spec.cliEntrypoint);\n if (!await File(nodePath).exists()) {\n throw OpenClawRuntimeException('OpenClaw 内置 Node 不存在: $nodePath');\n }\n if (!await File(cliPath).exists()) {\n throw OpenClawRuntimeException('OpenClaw CLI 入口不存在: $cliPath');\n }\n return _OpenClawCliLaunch(\n spec: spec,\n runtimeDir: runtimeDir,\n stateRoot: stateRoot,\n configPath: configPath,\n nodePath: nodePath,\n cliPath: cliPath,\n port: port,\n token: token,\n );\n }\n\n Map<String, String> _buildOpenClawEnvironment({\n required Directory runtimeDir,\n required Directory stateRoot,\n required File configPath,\n required int port,\n required String token,\n }) {\n final nodeBinDir = Platform.isWindows\n ? p.join(runtimeDir.path, 'node')\n : p.join(runtimeDir.path, 'node', 'bin');\n final pathKey = Platform.isWindows ? 'Path' : 'PATH';\n final currentPath =\n Platform.environment[pathKey] ?? Platform.environment['PATH'] ?? '';\n final separator = Platform.isWindows ? ';' : ':';\n final pathValue = currentPath.isEmpty\n ? nodeBinDir\n : '$nodeBinDir$separator$currentPath';\n final env = Map<String, String>.from(Platform.environment)\n ..addAll({\n 'OPENCLAW_GATEWAY_PORT': '$port',\n 'OPENCLAW_GATEWAY_BIND': 'loopback',\n 'OPENCLAW_GATEWAY_TOKEN': token,\n 'OPENCLAW_CONFIG_PATH': configPath.path,\n 'OPENCLAW_STATE_DIR': p.join(stateRoot.path, 'state'),\n 'OPENCLAW_AGENT_DIR': p.join(stateRoot.path, 'agents'),\n 'OPENCLAW_WORKSPACE': p.join(stateRoot.path, 'workspace'),\n 'OPENCLAW_DISABLE_BONJOUR': '1',\n pathKey: pathValue,\n if (pathKey != 'PATH') 'PATH': pathValue,\n });\n return env;\n }\n\n Future<List<String>> _bundledPluginLoadPaths(Directory runtimeDir) async {\n final paths = <String>[];\n final weChatPluginDir = Directory(\n p.join(runtimeDir.path, 'plugins', _weChatPluginId),\n );\n if (await File(\n p.join(weChatPluginDir.path, 'openclaw.plugin.json'),\n ).exists()) {\n paths.add(weChatPluginDir.path);\n }\n return paths;\n }\n\n Future<Directory> _ensureDesktopToolsPlugin({\n required Directory stateRoot,\n }) async {\n final dir = Directory(\n p.join(stateRoot.path, 'plugins', _desktopToolsPluginId),\n );\n await dir.create(recursive: true);\n final tools = DesktopControlPolicy.supportedTools.toList()..sort();\n await File(p.join(dir.path, 'package.json')).writeAsString(\n const JsonEncoder.withIndent(' ').convert({\n 'name': '@dacheng/openclaw-desktop-tools',\n 'version': '1.0.0',\n 'type': 'module',\n 'openclaw': {\n 'extensions': ['./index.mjs'],\n },\n }),\n );\n await File(p.join(dir.path, 'openclaw.plugin.json')).writeAsString(\n const JsonEncoder.withIndent(' ').convert({\n 'id': _desktopToolsPluginId,\n 'name': 'Dacheng Desktop Tools',\n 'description':\n 'Expose Fabushi desktop and Chrome control bridge tools to embedded OpenClaw.',\n 'activation': {'onStartup': true},\n 'contracts': {'tools': tools},\n 'configSchema': {\n 'type': 'object',\n 'additionalProperties': false,\n 'properties': <String, dynamic>{},\n },\n }),\n );\n await File(\n p.join(dir.path, 'index.mjs'),\n ).writeAsString(_desktopToolsPluginSource(tools));\n _diag(\n 'desktop-tools.plugin-written',\n data: {'pluginDir': dir.path, 'tools': tools},\n );\n return dir;\n }\n\n String _desktopToolsPluginSource(List<String> tools) {\n final toolsJson = jsonEncode(tools);\n return '''\nconst tools = $toolsJson;\n\nconst descriptions = {\n \"desktop.observe\": \"Observe the active desktop application and visible windows.\",\n \"desktop.screenshot\": \"Capture a screenshot of the local desktop.\",\n \"desktop.windows\": \"List visible desktop windows.\",\n \"desktop.click\": \"Click a local desktop coordinate after user confirmation.\",\n \"desktop.type\": \"Type text into the focused local desktop app after user confirmation.\",\n \"desktop.hotkey\": \"Send a keyboard shortcut to the local desktop after user confirmation.\",\n \"desktop.scroll\": \"Scroll the local desktop after user confirmation.\",\n \"chrome.tabs\": \"List tabs from the paired Chrome connector.\",\n \"chrome.navigate\": \"Navigate the active or selected Chrome tab after user confirmation.\",\n \"chrome.dom_snapshot\": \"Read a structured DOM snapshot from the paired Chrome connector.\",\n \"chrome.screenshot\": \"Capture a screenshot from the paired Chrome connector.\",\n \"chrome.click\": \"Click an element or point in the paired Chrome tab after user confirmation.\",\n \"chrome.type\": \"Type into an element in the paired Chrome tab after user confirmation.\"\n};\n\nconst parameterSchemas = {\n \"desktop.click\": {\n type: \"object\",\n additionalProperties: true,\n properties: {\n x: { type: \"number\" },\n y: { type: \"number\" },\n button: { type: \"string\", enum: [\"left\", \"right\"] }\n },\n required: [\"x\", \"y\"]\n },\n \"desktop.type\": {\n type: \"object\",\n additionalProperties: true,\n properties: { text: { type: \"string\" } },\n required: [\"text\"]\n },\n \"desktop.hotkey\": {\n type: \"object\",\n additionalProperties: true,\n properties: {\n key: { type: \"string\" },\n keys: { type: \"array\", items: { type: \"string\" } }\n }\n },\n \"chrome.navigate\": {\n type: \"object\",\n additionalProperties: true,\n properties: { url: { type: \"string\" }, tabId: { type: \"integer\" } },\n required: [\"url\"]\n },\n \"chrome.click\": {\n type: \"object\",\n additionalProperties: true,\n properties: {\n selector: { type: \"string\" },\n x: { type: \"number\" },\n y: { type: \"number\" },\n tabId: { type: \"integer\" }\n }\n },\n \"chrome.type\": {\n type: \"object\",\n additionalProperties: true,\n properties: {\n selector: { type: \"string\" },\n x: { type: \"number\" },\n y: { type: \"number\" },\n text: { type: \"string\" },\n tabId: { type: \"integer\" }\n },\n required: [\"text\"]\n }\n};\n\nconst defaultSchema = {\n type: \"object\",\n additionalProperties: true,\n properties: {}\n};\n\nfunction toolResultText(value) {\n try {\n return JSON.stringify(value, null, 2);\n } catch {\n return String(value);\n }\n}\n\nasync function callBridge(toolName, args) {\n if (process.env.DACHENG_DESKTOP_TOOLS_ENABLED !== \"1\") {\n return {\n isError: true,\n content: [{ type: \"text\", text: \"Fabushi desktop tools bridge is not running.\" }]\n };\n }\n const rawBaseUrl = process.env.DACHENG_DESKTOP_TOOLS_URL || \"\";\n const baseUrl = rawBaseUrl.endsWith(\"/\") ? rawBaseUrl.slice(0, -1) : rawBaseUrl;\n const token = process.env.DACHENG_DESKTOP_TOOLS_TOKEN || \"\";\n if (!baseUrl || !token) {\n return {\n isError: true,\n content: [{ type: \"text\", text: \"Fabushi desktop tools bridge URL/token is missing.\" }]\n };\n }\n const response = await fetch(`\\${baseUrl}/v1/tools/execute`, {\n method: \"POST\",\n headers: {\n \"Authorization\": `Bearer \\${token}`,\n \"Content-Type\": \"application/json\"\n },\n body: JSON.stringify({ tool: toolName, arguments: args || {} })\n });\n const payload = await response.json().catch(() => ({}));\n const ok = response.ok && payload && payload.ok !== false;\n const data = payload && Object.prototype.hasOwnProperty.call(payload, \"data\")\n ? payload.data\n : payload;\n return {\n isError: !ok,\n content: [{ type: \"text\", text: toolResultText(data) }],\n details: data\n };\n}\n\nexport default {\n id: \"dacheng-desktop-tools\",\n name: \"Dacheng Desktop Tools\",\n description: \"Expose Fabushi desktop and Chrome control tools to embedded OpenClaw.\",\n configSchema: { type: \"object\", additionalProperties: false, properties: {} },\n register(api) {\n for (const name of tools) {\n api.registerTool({\n name,\n label: name,\n description: descriptions[name] || `Run \\${name} through the Fabushi desktop bridge.`,\n parameters: parameterSchemas[name] || defaultSchema,\n async execute(_toolCallId, args) {\n return await callBridge(name, args);\n }\n });\n }\n }\n};\n''';\n }\n\n Future<Map<String, dynamic>> _mergeEmbeddedConfig(\n File configPath,\n Map<String, dynamic> defaults,\n ) async {\n Map<String, dynamic> current = <String, dynamic>{};\n if (await configPath.exists()) {\n try {\n final decoded = jsonDecode(await configPath.readAsString());\n if (decoded is Map<String, dynamic>) {\n current = decoded;\n } else if (decoded is Map) {\n current = Map<String, dynamic>.from(decoded);\n }\n } catch (_) {\n current = <String, dynamic>{};\n }\n }\n\n final gateway = _mutableMap(current['gateway']);\n final defaultGateway = _mutableMap(defaults['gateway']);\n gateway['mode'] = 'local';\n gateway['port'] = defaultGateway['port'];\n gateway['bind'] = defaultGateway['bind'];\n gateway['auth'] = Map<String, dynamic>.from(defaultGateway['auth'] as Map);\n if (defaultGateway.containsKey('remote')) {\n gateway['remote'] = {\n ..._mutableMap(gateway['remote']),\n ..._mutableMap(defaultGateway['remote']),\n };\n } else {\n final remote = _mutableMap(gateway['remote'])..remove('url');\n if (remote.isEmpty) {\n gateway.remove('remote');\n } else {\n gateway['remote'] = remote;\n }\n }\n\n final gatewayHttp = _mutableMap(gateway['http']);\n final endpoints = _mutableMap(gatewayHttp['endpoints']);\n endpoints['chatCompletions'] = {'enabled': true};\n endpoints['responses'] = {'enabled': true};\n gatewayHttp['endpoints'] = endpoints;\n gateway['http'] = gatewayHttp;\n current['gateway'] = gateway;\n\n final env = _mutableMap(current['env']);\n env.remove('DEEPSEEK_API_KEY');\n if (env.isEmpty) {\n current.remove('env');\n } else {\n current['env'] = env;\n }\n\n final defaultModels = _mutableMap(defaults['models']);\n final models = _mutableMap(current['models']);\n models['mode'] ??= defaultModels['mode'];\n final providers = _mutableMap(models['providers']);\n final defaultProviders = _mutableMap(defaultModels['providers']);\n providers.remove('deepseek');\n providers.remove('dacheng-deepseek');\n providers.remove(_backendDeepSeekProviderId);\n for (final entry in defaultProviders.entries) {\n providers[entry.key] = entry.value is Map\n ? Map<String, dynamic>.from(entry.value as Map)\n : entry.value;\n }\n models['providers'] = providers;\n current['models'] = models;\n\n final defaultBrowser = _mutableMap(defaults['browser']);\n final browser = _mutableMap(current['browser']);\n browser.addAll(defaultBrowser);\n current['browser'] = browser;\n\n final defaultCanvas = _mutableMap(defaults['canvas']);\n final canvas = _mutableMap(current['canvas']);\n canvas.addAll(defaultCanvas);\n current['canvas'] = canvas;\n\n final defaultTools = _mutableMap(defaults['tools']);\n final tools = _mutableMap(current['tools']);\n tools.remove('toolSearch');\n tools.remove('fs');\n final execTools = _mutableMap(tools['exec']);\n execTools.remove('mode');\n if (execTools.isNotEmpty) {\n tools['exec'] = execTools;\n }\n tools.addAll(defaultTools);\n current['tools'] = tools;\n\n final defaultPlugins = _mutableMap(defaults['plugins']);\n final plugins = _mutableMap(current['plugins']);\n plugins['enabled'] = true;\n final defaultEntries = _mutableMap(defaultPlugins['entries']);\n final requiredPluginIds = defaultEntries.keys.toSet();\n final allowList = _stringList(plugins['allow']);\n if (allowList.isEmpty) {\n plugins.remove('allow');\n } else {\n plugins['allow'] = _mergeStringLists(allowList, requiredPluginIds);\n }\n final denyList = _stringList(\n plugins['deny'],\n ).where((item) => !requiredPluginIds.contains(item)).toList();\n if (denyList.isEmpty) {\n plugins.remove('deny');\n } else {\n plugins['deny'] = denyList;\n }\n final defaultLoad = _mutableMap(defaultPlugins['load']);\n final load = _mutableMap(plugins['load']);\n load['paths'] = _mergeStringLists(load['paths'], defaultLoad['paths']);\n plugins['load'] = load;\n final slots = _mutableMap(plugins['slots']);\n slots.addAll(_mutableMap(defaultPlugins['slots']));\n plugins['slots'] = slots;\n final entries = _mutableMap(plugins['entries']);\n for (final entry in defaultEntries.entries) {\n final currentEntry = _mutableMap(entries[entry.key]);\n currentEntry.addAll(\n entry.value is Map\n ? Map<String, dynamic>.from(entry.value as Map)\n : {'enabled': true},\n );\n entries[entry.key] = currentEntry;\n }\n plugins['entries'] = entries;\n current['plugins'] = plugins;\n\n final defaultChannels = _mutableMap(defaults['channels']);\n final channels = _mutableMap(current['channels']);\n final defaultChannelDefaults = _mutableMap(defaultChannels['defaults']);\n if (defaultChannelDefaults.isNotEmpty) {\n channels['defaults'] = {\n ..._mutableMap(channels['defaults']),\n ...defaultChannelDefaults,\n };\n }\n for (final entry in defaultChannels.entries) {\n if (entry.key == 'defaults') continue;\n final currentChannel = _mutableMap(channels[entry.key]);\n currentChannel.addAll(\n entry.value is Map\n ? Map<String, dynamic>.from(entry.value as Map)\n : <String, dynamic>{},\n );\n channels[entry.key] = currentChannel;\n }\n current['channels'] = channels;\n\n final agents = _mutableMap(current['agents']);\n final defaultAgents = _mutableMap(defaults['agents']);\n final defaultAgentDefaults = _mutableMap(defaultAgents['defaults']);\n final agentDefaults = _mutableMap(agents['defaults']);\n agentDefaults['workspace'] = defaultAgentDefaults['workspace'];\n agentDefaults['model'] = defaultAgentDefaults['model'];\n agents['defaults'] = agentDefaults;\n\n final defaultList = (defaultAgents['list'] as List? ?? const [])\n .whereType<Map>()\n .map((item) => Map<String, dynamic>.from(item))\n .toList();\n final defaultDacheng = defaultList.isNotEmpty\n ? defaultList.first\n : <String, dynamic>{\n 'id': 'dacheng',\n 'default': true,\n 'workspace': agentDefaults['workspace'],\n 'model': agentDefaults['model'],\n };\n final list = (agents['list'] as List? ?? const [])\n .map((item) => item is Map ? Map<String, dynamic>.from(item) : item)\n .toList();\n var foundDacheng = false;\n for (var i = 0; i < list.length; i++) {\n final item = list[i];\n if (item is! Map<String, dynamic>) continue;\n if (item['id'] != 'dacheng') continue;\n foundDacheng = true;\n item['default'] = true;\n item['workspace'] = defaultDacheng['workspace'];\n item['model'] = defaultDacheng['model'];\n list[i] = item;\n }\n if (!foundDacheng) {\n list.add(defaultDacheng);\n }\n agents['list'] = list.isEmpty ? defaultList : list;\n current['agents'] = agents;\n\n return current;\n }\n\n String _gatewayChatModel(String model) {\n final trimmed = model.trim();\n if (trimmed == 'openclaw' || trimmed.startsWith('openclaw/')) {\n return trimmed;\n }\n return _defaultGatewayModel;\n }\n\n String _defaultDeepSeekModelFor(String model) {\n final trimmed = model.trim();\n if (trimmed.startsWith('deepseek/')) return trimmed;\n return _defaultDeepSeekModel;\n }\n\n String _backendDeepSeekModelRef(String model) {\n final trimmed = model.trim().isEmpty ? _defaultDeepSeekModel : model.trim();\n final modelId = trimmed.contains('/')\n ? trimmed.split('/').last.trim()\n : trimmed;\n final normalizedModelId = modelId.isEmpty ? 'deepseek-chat' : modelId;\n return '$_backendDeepSeekProviderId/$normalizedModelId';\n }\n\n String _deepSeekProxyBaseUrl() {\n final baseUrl = AppConfig.currentAiBackendUrl.replaceFirst(\n RegExp(r'/+$'),\n '',\n );\n return '$baseUrl/api/openclaw/deepseek/v1';\n }\n\n Map<String, dynamic> _mutableMap(Object? value) {\n if (value is Map<String, dynamic>) return Map<String, dynamic>.from(value);\n if (value is Map) return Map<String, dynamic>.from(value);\n return <String, dynamic>{};\n }\n\n List<String> _stringList(Object? value) {\n if (value is! Iterable) return <String>[];\n return value\n .map((item) => item.toString().trim())\n .where((item) => item.isNotEmpty)\n .toList();\n }\n\n List<String> _mergeStringLists(Object? first, Object? second) {\n final merged = <String>{};\n merged.addAll(_stringList(first));\n merged.addAll(_stringList(second));\n return merged.toList();\n }\n\n void _appendBounded(StringBuffer buffer, String chunk) {\n const maxChars = 120000;\n if (chunk.isEmpty || buffer.length >= maxChars) return;\n final remaining = maxChars - buffer.length;\n buffer.write(\n chunk.length <= remaining ? chunk : chunk.substring(0, remaining),\n );\n }\n\n void _captureLogs(Process process) {\n void addLog(String line) {\n final text = line.trim();\n if (text.isEmpty) return;\n _recentLogs.insert(0, text);\n if (_recentLogs.length > 80) {\n _recentLogs.removeRange(80, _recentLogs.length);\n }\n if (kDebugMode) {\n debugPrint('[OpenClaw] $text');\n }\n _diag('process.output', data: {'line': text});\n }\n\n process.stdout\n .transform(utf8.decoder)\n .transform(const LineSplitter())\n .listen(addLog, onError: (_) {});\n process.stderr\n .transform(utf8.decoder)\n .transform(const LineSplitter())\n .listen(addLog, onError: (_) {});\n }\n\n Future<Directory> _stateRoot() async {\n final support = await getApplicationSupportDirectory();\n final dir = Directory(p.join(support.path, 'openclaw_embedded'));\n await dir.create(recursive: true);\n return dir;\n }\n\n Future<_DesktopToolsLaunch> _ensureDesktopTools() async {\n try {\n final status = await DesktopControlBridge.instance.ensureStarted();\n _diag(\n 'desktop-tools.started',\n data: {\n 'bridgeRunning': status.bridgeRunning,\n 'bridgeUri': status.bridgeUri?.toString(),\n 'message': status.message,\n },\n );\n return _DesktopToolsLaunch(\n uri: status.bridgeUri,\n token: await DesktopControlBridge.instance.bridgeToken,\n statusJson: status.toJson(),\n );\n } catch (error) {\n _diag('desktop-tools.error', error: error);\n return _DesktopToolsLaunch(\n uri: null,\n token: null,\n statusJson: {\n 'enabledByBuild': false,\n 'bridgeRunning': false,\n 'message': error.toString(),\n },\n );\n }\n }\n\n Future<Directory> _runtimeDir(\n _OpenClawBundleSpec spec,\n String platformKey,\n ) async {\n final support = await getApplicationSupportDirectory();\n final runtimeBase = Platform.isMacOS\n ? Directory(\n p.join(\n support.parent.path,\n '${p.basename(support.path)}.openclaw_runtime',\n ),\n )\n : Directory(p.join(support.path, 'openclaw_embedded'));\n return Directory(\n p.join(runtimeBase.path, 'runtime', spec.version, platformKey),\n );\n }\n\n Future<Directory?> _bundledRuntimeDir(\n _OpenClawBundleSpec spec,\n String platformKey,\n ) async {\n if (!Platform.isMacOS) return null;\n\n final contentsDir = Directory(\n p.dirname(p.dirname(Platform.resolvedExecutable)),\n );\n final candidates = <Directory>[\n Directory(\n p.join(\n contentsDir.path,\n 'Frameworks',\n 'App.framework',\n 'Resources',\n 'flutter_assets',\n 'assets',\n 'openclaw',\n platformKey,\n ),\n ),\n Directory(\n p.join(\n contentsDir.path,\n 'Frameworks',\n 'App.framework',\n 'Versions',\n 'A',\n 'Resources',\n 'flutter_assets',\n 'assets',\n 'openclaw',\n platformKey,\n ),\n ),\n Directory(\n p.join(\n contentsDir.path,\n 'Resources',\n 'flutter_assets',\n 'assets',\n 'openclaw',\n platformKey,\n ),\n ),\n ];\n\n for (final dir in candidates) {\n final nodePath = File(p.join(dir.path, spec.nodeExecutable));\n final cliPath = File(p.join(dir.path, spec.cliEntrypoint));\n if (await nodePath.exists() && await cliPath.exists()) {\n return dir;\n }\n }\n _diag(\n 'bundle.macos-bundled-runtime-missing',\n data: {\n 'platformKey': platformKey,\n 'contentsDir': contentsDir.path,\n 'candidates': candidates.map((item) => item.path).toList(),\n },\n );\n return null;\n }\n\n Future<Directory?> _runtimeDirOverride(\n _OpenClawBundleSpec spec,\n String platformKey,\n ) async {\n final raw = _configuredRuntimeDirOverride(platformKey);\n if (raw == null) return null;\n\n final dir = Directory(raw);\n final nodePath = File(p.join(dir.path, spec.nodeExecutable));\n final cliPath = File(p.join(dir.path, spec.cliEntrypoint));\n final nodeExists = await nodePath.exists();\n final cliExists = await cliPath.exists();\n _diag(\n 'bundle.override.paths',\n data: {\n 'platformKey': platformKey,\n 'runtimeDir': dir.path,\n 'nodePath': nodePath.path,\n 'nodeExists': nodeExists,\n 'cliPath': cliPath.path,\n 'cliExists': cliExists,\n },\n );\n\n if (!nodeExists || !cliExists) {\n throw OpenClawRuntimeException(\n 'DACHENG_OPENCLAW_RUNTIME_DIR 指向的 OpenClaw runtime 不完整: ${dir.path}',\n );\n }\n return dir;\n }\n\n String? _configuredRuntimeDirOverride(String platformKey) {\n final raw = _runtimeDirOverrideDefine.trim().isNotEmpty\n ? _runtimeDirOverrideDefine.trim()\n : Platform.environment['DACHENG_OPENCLAW_RUNTIME_DIR']?.trim();\n if (raw == null || raw.isEmpty) return null;\n return p.normalize(raw.replaceAll('{platform}', platformKey));\n }\n\n Future<Directory> _legacyRuntimeDir(\n _OpenClawBundleSpec spec,\n String platformKey,\n ) async {\n final support = await getApplicationSupportDirectory();\n return Directory(\n p.join(\n support.path,\n 'openclaw_embedded',\n 'runtime',\n spec.version,\n platformKey,\n ),\n );\n }\n\n OpenClawRuntimeStatus _remember(OpenClawRuntimeStatus status) {\n _lastStatus = status;\n return status;\n }\n\n String? get _platformKey {\n final abi = Abi.current().toString().toLowerCase();\n if (Platform.isMacOS) {\n return abi.contains('arm64') ? 'macos-arm64' : 'macos-x64';\n }\n if (Platform.isWindows) {\n return abi.contains('arm64') ? 'windows-arm64' : 'windows-x64';\n }\n if (Platform.isLinux) {\n return abi.contains('arm64') ? 'linux-arm64' : 'linux-x64';\n }\n return null;\n }\n\n OpenClawRuntimeStatus? get lastStatus => _lastStatus;\n\n void _diag(\n String message, {\n Map<String, Object?> data = const {},\n Object? error,\n StackTrace? stackTrace,\n }) {\n unawaited(\n DiagnosticLogService.instance.log(\n 'openclaw.runtime',\n message,\n data: data,\n error: error,\n stackTrace: stackTrace,\n ),\n );\n }\n}\n\nint? _readInt(Object? value) {\n if (value is int) return value;\n if (value is num) return value.toInt();\n if (value is String) return int.tryParse(value);\n return null;\n}\n\nObject? _decodeJsonOrNull(String body) {\n try {\n return jsonDecode(body);\n } catch (_) {\n return null;\n }\n}\n\nbool _looksLikeHealthPayload(Object? payload) {\n if (payload is! Map) return false;\n if (payload['ok'] == true) return true;\n final status = payload['status']?.toString().toLowerCase();\n return status == 'live' || status == 'ok' || status == 'healthy';\n}\n\nbool _looksLikeModelsPayload(Object? payload) {\n if (payload is List) return true;\n if (payload is! Map) return false;\n return payload['data'] is List ||\n payload['models'] is List ||\n payload['object'] == 'list';\n}\n\nString _previewBody(String body) {\n final trimmed = body.trim();\n return trimmed.length <= 80 ? trimmed : trimmed.substring(0, 80);\n}\n\nString _previewProcessOutput(Object? output) {\n final text = output?.toString().trim() ?? '';\n return text.length <= 200 ? text : text.substring(0, 200);\n}\n"}, "15d299ace35e37138c1dfcd36c1f182f2e26a4ab": {"t": 1785757958.7873464, "n": 72156, "h": "1d42f36e3c04e033", "c": "import 'dart:async';\nimport 'dart:convert';\nimport 'dart:ffi' show Abi;\nimport 'dart:io';\nimport 'package:archive/archive_io.dart';\nimport 'package:convert/convert.dart';\nimport 'package:crypto/crypto.dart';\nimport 'package:flutter/foundation.dart';\nimport 'package:flutter/services.dart';\nimport 'package:http/http.dart' as http;\nimport 'package:path/path.dart' as p;\nimport 'package:path_provider/path_provider.dart';\n\nimport '../../core/config/app_config.dart';\nimport '../app_settings.dart';\nimport '../diagnostic_log_service.dart';\nimport '../desktop_control/desktop_control_bridge.dart';\nimport '../desktop_control/desktop_control_policy.dart';\nimport 'openclaw_port_resolver.dart';\n\nenum OpenClawRuntimeState {\n unsupported,\n notBundled,\n stopped,\n starting,\n running,\n failed,\n}\n\nclass OpenClawRuntimeStatus {\n final OpenClawRuntimeState state;\n final String message;\n final int? port;\n final String? platformKey;\n final String? runtimePath;\n final Map<String, dynamic>? desktopToolsStatus;\n final DateTime checkedAt;\n\n const OpenClawRuntimeStatus({\n required this.state,\n required this.message,\n this.port,\n this.platformKey,\n this.runtimePath,\n this.desktopToolsStatus,\n required this.checkedAt,\n });\n\n bool get isHealthy => state == OpenClawRuntimeState.running;\n\n String get label {\n switch (state) {\n case OpenClawRuntimeState.unsupported:\n return '不支持';\n case OpenClawRuntimeState.notBundled:\n return '未内置';\n case OpenClawRuntimeState.stopped:\n return '未启动';\n case OpenClawRuntimeState.starting:\n return '启动中';\n case OpenClawRuntimeState.running:\n return '运行中';\n case OpenClawRuntimeState.failed:\n return '异常';\n }\n }\n}\n\nclass OpenClawGatewayTarget {\n final Uri baseUri;\n final String token;\n final String model;\n final String? modelOverride;\n final Uri? desktopToolsUri;\n final String? desktopToolsToken;\n final Map<String, dynamic>? desktopToolsStatus;\n\n const OpenClawGatewayTarget({\n required this.baseUri,\n required this.token,\n required this.model,\n this.modelOverride,\n this.desktopToolsUri,\n this.desktopToolsToken,\n this.desktopToolsStatus,\n });\n}\n\nclass _OpenClawBundleSpec {\n final String version;\n final int defaultPort;\n final String defaultModel;\n final String? defaultModelOverride;\n final String nodeExecutable;\n final String cliEntrypoint;\n final List<String> gatewayArgs;\n final bool downloaded;\n\n const _OpenClawBundleSpec({\n required this.version,\n required this.defaultPort,\n required this.defaultModel,\n this.defaultModelOverride,\n required this.nodeExecutable,\n required this.cliEntrypoint,\n required this.gatewayArgs,\n this.downloaded = false,\n });\n\n Map<String, dynamic> toJson() => {\n 'schema': 1,\n 'version': version,\n 'defaultPort': defaultPort,\n 'defaultModel': defaultModel,\n if (defaultModelOverride != null)\n 'defaultModelOverride': defaultModelOverride,\n 'nodeExecutable': nodeExecutable,\n 'cliEntrypoint': cliEntrypoint,\n 'gatewayArgs': gatewayArgs,\n 'downloaded': downloaded,\n };\n}\n\nclass _OpenClawRuntimeRelease {\n final String version;\n final Uri archiveUri;\n final String sha256Hex;\n final int size;\n final int defaultPort;\n final String defaultModel;\n final String? defaultModelOverride;\n final String nodeExecutable;\n final String cliEntrypoint;\n final List<String> gatewayArgs;\n\n const _OpenClawRuntimeRelease({\n required this.version,\n required this.archiveUri,\n required this.sha256Hex,\n required this.size,\n required this.defaultPort,\n required this.defaultModel,\n this.defaultModelOverride,\n required this.nodeExecutable,\n required this.cliEntrypoint,\n required this.gatewayArgs,\n });\n\n _OpenClawBundleSpec toBundleSpec() => _OpenClawBundleSpec(\n version: version,\n defaultPort: defaultPort,\n defaultModel: defaultModel,\n defaultModelOverride: defaultModelOverride,\n nodeExecutable: nodeExecutable,\n cliEntrypoint: cliEntrypoint,\n gatewayArgs: gatewayArgs,\n downloaded: true,\n );\n}\n\nclass _DesktopToolsLaunch {\n final Uri? uri;\n final String? token;\n final Map<String, dynamic> statusJson;\n\n const _DesktopToolsLaunch({\n required this.uri,\n required this.token,\n required this.statusJson,\n });\n}\n\nclass OpenClawRuntimeException implements Exception {\n final String message;\n\n const OpenClawRuntimeException(this.message);\n\n @override\n String toString() => message;\n}\n\nclass OpenClawRuntime {\n OpenClawRuntime._();\n\n static final OpenClawRuntime instance = OpenClawRuntime._();\n\n static const String _manifestAsset = 'assets/openclaw/bundle_manifest.json';\n static const String _runtimeDirOverrideDefine = String.fromEnvironment(\n 'DACHENG_OPENCLAW_RUNTIME_DIR',\n );\n static const String _defaultGatewayModel =\n AppSettings.defaultOpenClawGatewayModel;\n static const String _defaultDeepSeekModel =\n AppSettings.defaultOpenClawDeepSeekModel;\n static const String _backendDeepSeekProviderId = 'dacheng-deepseek-proxy';\n static const Duration _startupTimeout = Duration(seconds: 120);\n static const Duration _probeTimeout = Duration(seconds: 3);\n\n Process? _process;\n Future<OpenClawGatewayTarget>? _starting;\n String _processAuthToken = '';\n OpenClawRuntimeStatus? _lastStatus;\n final List<String> _recentLogs = <String>[];\n final OpenClawPortResolver _portResolver = const OpenClawPortResolver();\n\n Future<OpenClawRuntimeStatus> getStatus({bool probe = true}) async {\n final platformKey = _platformKey;\n final port = await AppSettings.getOpenClawGatewayPort();\n _diag(\n 'status.start',\n data: {'platformKey': platformKey, 'port': port, 'probe': probe},\n );\n\n if (platformKey == null) {\n return _remember(\n OpenClawRuntimeStatus(\n state: OpenClawRuntimeState.unsupported,\n message: '当前平台不是 macOS / Windows / Linux 桌面端',\n port: port,\n checkedAt: DateTime.now(),\n ),\n );\n }\n\n try {\n final spec = await _loadSpec(platformKey, checkUpdates: false);\n final overrideRuntimeDir = await _runtimeDirOverride(spec, platformKey);\n final bundledRuntimeDir = overrideRuntimeDir == null\n ? await _bundledRuntimeDir(spec, platformKey)\n : null;\n final runtimeDir =\n overrideRuntimeDir ??\n bundledRuntimeDir ??\n await _runtimeDir(spec, platformKey);\n final nodePath = p.join(runtimeDir.path, spec.nodeExecutable);\n final cliPath = p.join(runtimeDir.path, spec.cliEntrypoint);\n final nodeExists = await File(nodePath).exists();\n final cliExists = await File(cliPath).exists();\n _diag(\n 'status.paths',\n data: {\n 'platformKey': platformKey,\n 'runtimeDir': runtimeDir.path,\n 'nodePath': nodePath,\n 'nodeExists': nodeExists,\n 'cliPath': cliPath,\n 'cliExists': cliExists,\n 'runtimeSource': overrideRuntimeDir != null\n ? 'override'\n : (bundledRuntimeDir == null ? 'cache' : 'bundle'),\n },\n );\n\n if (!nodeExists || !cliExists) {\n final hasBundleAssets = await _hasRequiredBundleAssets(\n platformKey,\n spec,\n );\n _diag(\n 'status.bundle-assets',\n data: {\n 'platformKey': platformKey,\n 'hasBundleAssets': hasBundleAssets,\n 'nodeAsset': 'assets/openclaw/$platformKey/${spec.nodeExecutable}',\n 'cliAsset': 'assets/openclaw/$platformKey/${spec.cliEntrypoint}',\n },\n );\n return _remember(\n OpenClawRuntimeStatus(\n state: hasBundleAssets\n ? OpenClawRuntimeState.stopped\n : OpenClawRuntimeState.notBundled,\n message: hasBundleAssets\n ? '内置 OpenClaw 已随 App 打包,尚未释放或启动'\n : '当前 App 包未包含 $platformKey 的 OpenClaw runtime',\n port: port,\n platformKey: platformKey,\n runtimePath: runtimeDir.path,\n checkedAt: DateTime.now(),\n ),\n );\n }\n\n if (probe) {\n final healthy = await _probe(\n port,\n await AppSettings.getOpenClawGatewayToken(),\n );\n if (healthy) {\n return _remember(\n OpenClawRuntimeStatus(\n state: OpenClawRuntimeState.running,\n message: '本机 OpenClaw Gateway 正在运行',\n port: port,\n platformKey: platformKey,\n runtimePath: runtimeDir.path,\n checkedAt: DateTime.now(),\n ),\n );\n }\n }\n\n return _remember(\n OpenClawRuntimeStatus(\n state: _process == null\n ? OpenClawRuntimeState.stopped\n : OpenClawRuntimeState.starting,\n message: _process == null ? '本机 OpenClaw 未启动' : '本机 OpenClaw 正在启动',\n port: port,\n platformKey: platformKey,\n runtimePath: runtimeDir.path,\n checkedAt: DateTime.now(),\n ),\n );\n } catch (error) {\n _diag('status.error', error: error);\n return _remember(\n OpenClawRuntimeStatus(\n state: OpenClawRuntimeState.failed,\n message: error.toString(),\n port: port,\n platformKey: platformKey,\n checkedAt: DateTime.now(),\n ),\n );\n }\n }\n\n Future<OpenClawGatewayTarget> ensureStarted({\n String? authToken,\n String? username,\n bool isMember = false,\n }) {\n _starting ??= _ensureStartedInternal(\n authToken: authToken,\n username: username,\n isMember: isMember,\n ).whenComplete(() => _starting = null);\n return _starting!;\n }\n\n Future<OpenClawRuntimeStatus> restart() async {\n await stop();\n try {\n await ensureStarted();\n return getStatus();\n } catch (error) {\n return _remember(\n OpenClawRuntimeStatus(\n state: OpenClawRuntimeState.failed,\n message: error.toString(),\n port: await AppSettings.getOpenClawGatewayPort(),\n platformKey: _platformKey,\n checkedAt: DateTime.now(),\n ),\n );\n }\n }\n\n Future<void> stop() async {\n final process = _process;\n _process = null;\n _processAuthToken = '';\n if (process == null) return;\n process.kill();\n try {\n await process.exitCode.timeout(const Duration(seconds: 5));\n } catch (_) {\n process.kill();\n }\n }\n\n Future<OpenClawGatewayTarget> _ensureStartedInternal({\n String? authToken,\n String? username,\n bool isMember = false,\n }) async {\n final platformKey = _platformKey;\n _diag('ensure-start.start', data: {'platformKey': platformKey});\n if (platformKey == null) {\n throw const OpenClawRuntimeException('当前平台不支持内置 OpenClaw Gateway');\n }\n\n final spec = await _loadSpec(platformKey, checkUpdates: true);\n final requestedPort = await AppSettings.getOpenClawGatewayPort(\n defaultValue: spec.defaultPort,\n );\n final token = await AppSettings.getOpenClawGatewayToken();\n final savedModel = await AppSettings.getOpenClawModel(\n defaultValue: spec.defaultModel,\n );\n final model = _gatewayChatModel(savedModel);\n final requestedAuthToken = authToken?.trim() ?? '';\n final backendDeepSeekModel = _backendDeepSeekModelRef(\n await AppSettings.getOpenClawDeepSeekModel(\n defaultValue: _defaultDeepSeekModelFor(spec.defaultModel),\n ),\n );\n final deepSeekProxyBaseUrl = _deepSeekProxyBaseUrl();\n final modelOverride = await AppSettings.getOpenClawModelOverride(\n defaultValue: spec.defaultModelOverride ?? '',\n );\n final desktopTools = await _ensureDesktopTools();\n _diag(\n 'ensure-start.config',\n data: {\n 'platformKey': platformKey,\n 'requestedPort': requestedPort,\n 'model': model,\n 'rawModel': savedModel,\n 'backendDeepSeekModel': backendDeepSeekModel,\n 'deepSeekProxyBaseUrl': deepSeekProxyBaseUrl,\n 'hasAuthToken': requestedAuthToken.isNotEmpty,\n 'modelOverrideSet': modelOverride.trim().isNotEmpty,\n 'desktopToolsUri': desktopTools.uri?.toString(),\n 'desktopToolsStatus': desktopTools.statusJson,\n },\n );\n\n final canReuseStartedProcess =\n _process != null && _processAuthToken == requestedAuthToken;\n if (canReuseStartedProcess &&\n await _probe(requestedPort, token, context: 'pre-start')) {\n _diag('ensure-start.reuse-existing', data: {'port': requestedPort});\n return OpenClawGatewayTarget(\n baseUri: Uri.parse('http://127.0.0.1:$requestedPort'),\n token: token,\n model: model,\n modelOverride: modelOverride.trim().isEmpty\n ? null\n : modelOverride.trim(),\n desktopToolsUri: desktopTools.uri,\n desktopToolsToken: desktopTools.token,\n desktopToolsStatus: desktopTools.statusJson,\n );\n } else if (_process != null) {\n _diag(\n 'ensure-start.restart-auth-changed',\n data: {\n 'hasPreviousAuthToken': _processAuthToken.isNotEmpty,\n 'hasRequestedAuthToken': requestedAuthToken.isNotEmpty,\n },\n );\n await stop();\n }\n\n final runtimeDir = await _prepareBundle(spec, platformKey);\n final stateRoot = await _stateRoot();\n if (_process != null) {\n _diag(\n 'process.stop-stale-before-start',\n data: {'requestedPort': requestedPort},\n );\n await stop();\n }\n final portCandidate = await _portResolver.resolve(requestedPort);\n final port = portCandidate.port;\n if (portCandidate.isFallback) {\n await AppSettings.setOpenClawGatewayPort(port);\n _diag(\n 'ensure-start.fallback-port',\n data: {\n 'requestedPort': requestedPort,\n 'selectedPort': port,\n if (portCandidate.reason != null) 'reason': portCandidate.reason,\n },\n );\n } else {\n _diag(\n 'ensure-start.port-bindable',\n data: {'requestedPort': requestedPort, 'selectedPort': port},\n );\n }\n final configPath = await _ensureConfigFile(\n stateRoot: stateRoot,\n port: port,\n token: token,\n backendDeepSeekModel: backendDeepSeekModel,\n deepSeekProxyBaseUrl: deepSeekProxyBaseUrl,\n );\n await _ensureAgentModelConfig(\n stateRoot: stateRoot,\n deepSeekProxyBaseUrl: deepSeekProxyBaseUrl,\n );\n final desktopToolsManifestPath = await _ensureDesktopToolsManifest(\n stateRoot: stateRoot,\n desktopTools: desktopTools,\n );\n\n final nodePath = p.join(runtimeDir.path, spec.nodeExecutable);\n final cliPath = p.join(runtimeDir.path, spec.cliEntrypoint);\n if (!await File(nodePath).exists()) {\n _diag('ensure-start.missing-node', data: {'nodePath': nodePath});\n throw OpenClawRuntimeException('OpenClaw 内置 Node 不存在: $nodePath');\n }\n if (!await File(cliPath).exists()) {\n _diag('ensure-start.missing-cli', data: {'cliPath': cliPath});\n throw OpenClawRuntimeException('OpenClaw CLI 入口不存在: $cliPath');\n }\n\n _remember(\n OpenClawRuntimeStatus(\n state: OpenClawRuntimeState.starting,\n message: '正在启动内置 OpenClaw Gateway',\n port: port,\n platformKey: platformKey,\n runtimePath: runtimeDir.path,\n checkedAt: DateTime.now(),\n ),\n );\n\n final args = <String>[\n cliPath,\n ...spec.gatewayArgs.map((arg) => arg.replaceAll('{port}', '$port')),\n ];\n _recentLogs.clear();\n _diag(\n 'process.starting',\n data: {\n 'nodePath': nodePath,\n 'args': args,\n 'workingDirectory': runtimeDir.path,\n 'configPath': configPath.path,\n 'desktopToolsManifestPath': desktopToolsManifestPath.path,\n 'requestedPort': requestedPort,\n 'selectedPort': port,\n },\n );\n\n final env = Map<String, String>.from(Platform.environment)\n ..addAll({\n 'OPENCLAW_GATEWAY_PORT': '$port',\n 'OPENCLAW_GATEWAY_BIND': 'loopback',\n 'OPENCLAW_GATEWAY_TOKEN': token,\n 'OPENCLAW_CONFIG_PATH': configPath.path,\n 'OPENCLAW_STATE_DIR': p.join(stateRoot.path, 'state'),\n 'OPENCLAW_AGENT_DIR': p.join(stateRoot.path, 'agents'),\n 'OPENCLAW_WORKSPACE': p.join(stateRoot.path, 'workspace'),\n 'DACHENG_DESKTOP_TOOLS_ENABLED': desktopTools.uri == null ? '0' : '1',\n 'DACHENG_DESKTOP_TOOLS_MANIFEST': desktopToolsManifestPath.path,\n if (desktopTools.uri != null)\n 'DACHENG_DESKTOP_TOOLS_URL': desktopTools.uri.toString(),\n if (desktopTools.token != null)\n 'DACHENG_DESKTOP_TOOLS_TOKEN': desktopTools.token!,\n 'DACHENG_APP_RUNTIME': '1',\n 'DACHENG_AUTH_TOKEN': (authToken != null && authToken.isNotEmpty)\n ? authToken\n : token,\n if (username != null && username.isNotEmpty)\n 'DACHENG_USERNAME': username,\n 'DACHENG_IS_MEMBER': isMember ? '1' : '0',\n 'DACHENG_OPENCLAW_PROXY_TOKEN': 'dacheng-openclaw-proxy',\n 'OPENCLAW_DISABLE_BONJOUR': '1',\n });\n\n _process = await Process.start(\n nodePath,\n args,\n workingDirectory: runtimeDir.path,\n environment: env,\n mode: ProcessStartMode.normal,\n runInShell: false,\n );\n _processAuthToken = requestedAuthToken;\n _captureLogs(_process!);\n _diag('process.started', data: {'pid': _process!.pid, 'port': port});\n\n final deadline = DateTime.now().add(_startupTimeout);\n while (DateTime.now().isBefore(deadline)) {\n if (await _probe(port, token, context: 'startup')) {\n _diag('process.ready', data: {'pid': _process?.pid, 'port': port});\n _remember(\n OpenClawRuntimeStatus(\n state: OpenClawRuntimeState.running,\n message: '本机 OpenClaw Gateway 已启动',\n port: port,\n platformKey: platformKey,\n runtimePath: runtimeDir.path,\n desktopToolsStatus: desktopTools.statusJson,\n checkedAt: DateTime.now(),\n ),\n );\n return OpenClawGatewayTarget(\n baseUri: Uri.parse('http://127.0.0.1:$port'),\n token: token,\n model: model,\n modelOverride: modelOverride.trim().isEmpty\n ? null\n : modelOverride.trim(),\n desktopToolsUri: desktopTools.uri,\n desktopToolsToken: desktopTools.token,\n desktopToolsStatus: desktopTools.statusJson,\n );\n }\n\n if (_process != null) {\n final exited = await _process!.exitCode.timeout(\n const Duration(milliseconds: 1),\n onTimeout: () => -999999,\n );\n if (exited != -999999) {\n final logs = _recentLogs.take(12).join('\\n');\n _diag(\n 'process.exited-early',\n data: {\n 'exitCode': exited,\n 'recentLogs': _recentLogs.take(12).toList(),\n },\n );\n throw OpenClawRuntimeException(\n 'OpenClaw Gateway 提前退出,exitCode=$exited${logs.isEmpty ? '' : '\\n$logs'}',\n );\n }\n }\n await Future<void>.delayed(const Duration(milliseconds: 450));\n }\n\n final logs = _recentLogs.take(12).join('\\n');\n _diag(\n 'process.startup-timeout',\n data: {\n 'timeoutSeconds': _startupTimeout.inSeconds,\n 'recentLogs': _recentLogs.take(12).toList(),\n },\n );\n throw OpenClawRuntimeException(\n 'OpenClaw Gateway 启动超时${logs.isEmpty ? '' : '\\n$logs'}',\n );\n }\n\n Future<_OpenClawBundleSpec> _loadSpec(\n String platformKey, {\n bool checkUpdates = false,\n }) async {\n final raw = await rootBundle.loadString(_manifestAsset);\n final decoded = jsonDecode(raw) as Map<String, dynamic>;\n final platforms = Map<String, dynamic>.from(\n decoded['platforms'] as Map? ?? const {},\n );\n final platform = Map<String, dynamic>.from(\n platforms[platformKey] as Map? ?? const {},\n );\n if (platform.isEmpty) {\n throw OpenClawRuntimeException('bundle_manifest.json 缺少 $platformKey 配置');\n }\n\n final gatewayArgs =\n (platform['gatewayArgs'] as List? ??\n decoded['gatewayArgs'] as List? ??\n const ['gateway', '--port', '{port}'])\n .map((item) => item.toString())\n .toList();\n\n final spec = _OpenClawBundleSpec(\n version: (decoded['version'] ?? 'dev').toString(),\n defaultPort: _readInt(decoded['defaultPort']) ?? 18789,\n defaultModel: (decoded['defaultModel'] ?? 'openclaw/default').toString(),\n defaultModelOverride: decoded['defaultModelOverride']?.toString(),\n nodeExecutable: (platform['nodeExecutable'] ?? 'node/bin/node')\n .toString(),\n cliEntrypoint: (platform['cliEntrypoint'] ?? 'openclaw/bin/openclaw.js')\n .toString(),\n gatewayArgs: gatewayArgs,\n );\n _diag(\n 'manifest.loaded',\n data: {\n 'platformKey': platformKey,\n 'version': spec.version,\n 'defaultPort': spec.defaultPort,\n 'defaultModel': spec.defaultModel,\n 'nodeExecutable': spec.nodeExecutable,\n 'cliEntrypoint': spec.cliEntrypoint,\n 'gatewayArgs': spec.gatewayArgs,\n },\n );\n final allowDownloadedRuntime = await _allowDownloadedRuntime(platformKey);\n var effectiveSpec = spec;\n if (allowDownloadedRuntime) {\n effectiveSpec =\n await _loadActiveDownloadedSpec(platformKey, fallbackSpec: spec) ??\n spec;\n } else {\n await _clearActiveDownloadedRuntimeSpec(\n platformKey: platformKey,\n reason: 'macos-sandbox',\n );\n }\n if (checkUpdates && allowDownloadedRuntime) {\n effectiveSpec =\n await _maybeInstallRuntimeUpdate(\n platformKey: platformKey,\n currentSpec: effectiveSpec,\n ) ??\n effectiveSpec;\n } else if (checkUpdates) {\n _diag(\n 'runtime-update.skip-disabled',\n data: {'platformKey': platformKey, 'reason': 'macos-sandbox'},\n );\n }\n return effectiveSpec;\n }\n\n Future<bool> _allowDownloadedRuntime(String platformKey) async {\n if (!Platform.isMacOS) return true;\n\n final support = await getApplicationSupportDirectory();\n final sandboxContainerId =\n Platform.environment['APP_SANDBOX_CONTAINER_ID']?.trim() ?? '';\n final pathLooksSandboxed = p\n .split(p.normalize(support.path))\n .contains('Containers');\n final sandboxed = sandboxContainerId.isNotEmpty || pathLooksSandboxed;\n if (!sandboxed) return true;\n\n // App Store/TestFlight sandboxed apps cannot reliably execute downloaded\n // Mach-O payloads. Use the bundled runtime that was signed with inherited\n // sandbox entitlements during packaging.\n _diag(\n 'runtime-update.disabled-macos-sandbox',\n data: {\n 'platformKey': platformKey,\n 'supportPath': support.path,\n 'hasSandboxContainerId': sandboxContainerId.isNotEmpty,\n },\n );\n return false;\n }\n\n Future<void> _clearActiveDownloadedRuntimeSpec({\n required String platformKey,\n required String reason,\n }) async {\n final saved = await AppSettings.getOpenClawActiveRuntimeSpec();\n if (saved == null) return;\n await AppSettings.clearOpenClawActiveRuntimeSpec();\n _diag(\n 'runtime-update.active-cleared',\n data: {\n 'platformKey': platformKey,\n 'reason': reason,\n 'version': saved['version']?.toString(),\n 'downloaded': saved['downloaded'] == true,\n },\n );\n }\n\n Future<_OpenClawBundleSpec?> _loadActiveDownloadedSpec(\n String platformKey, {\n required _OpenClawBundleSpec fallbackSpec,\n }) async {\n final saved = await AppSettings.getOpenClawActiveRuntimeSpec();\n if (saved == null) return null;\n final spec = _specFromJson(saved, downloaded: true);\n if (spec == null) {\n await AppSettings.clearOpenClawActiveRuntimeSpec();\n return null;\n }\n final runtimeDir = await _cachedRuntimeDirIfComplete(spec, platformKey);\n if (runtimeDir == null) {\n await AppSettings.clearOpenClawActiveRuntimeSpec();\n _diag(\n 'runtime-update.active-missing',\n data: {'platformKey': platformKey, 'version': spec.version},\n );\n return null;\n }\n _diag(\n 'runtime-update.active-loaded',\n data: {\n 'platformKey': platformKey,\n 'version': spec.version,\n 'runtimeDir': runtimeDir.path,\n 'bundledVersion': fallbackSpec.version,\n },\n );\n return spec;\n }\n\n _OpenClawBundleSpec? _specFromJson(\n Map<String, dynamic> json, {\n required bool downloaded,\n }) {\n final version = json['version']?.toString().trim() ?? '';\n final nodeExecutable = json['nodeExecutable']?.toString().trim() ?? '';\n final cliEntrypoint = json['cliEntrypoint']?.toString().trim() ?? '';\n if (version.isEmpty || nodeExecutable.isEmpty || cliEntrypoint.isEmpty) {\n return null;\n }\n return _OpenClawBundleSpec(\n version: version,\n defaultPort: _readInt(json['defaultPort']) ?? 18789,\n defaultModel: (json['defaultModel'] ?? _defaultGatewayModel).toString(),\n defaultModelOverride: json['defaultModelOverride']?.toString(),\n nodeExecutable: nodeExecutable,\n cliEntrypoint: cliEntrypoint,\n gatewayArgs:\n (json['gatewayArgs'] as List? ??\n const ['gateway', '--port', '{port}', '--force'])\n .map((item) => item.toString())\n .toList(),\n downloaded: downloaded,\n );\n }\n\n Future<_OpenClawBundleSpec?> _maybeInstallRuntimeUpdate({\n required String platformKey,\n required _OpenClawBundleSpec currentSpec,\n }) async {\n if (_configuredRuntimeDirOverride(platformKey) != null) {\n _diag('runtime-update.skip-override', data: {'platformKey': platformKey});\n return null;\n }\n\n try {\n final release = await _fetchRuntimeRelease(\n platformKey: platformKey,\n currentVersion: currentSpec.version,\n );\n if (release == null || release.version == currentSpec.version) {\n return null;\n }\n\n final releaseSpec = release.toBundleSpec();\n final cached = await _cachedRuntimeDirIfComplete(\n releaseSpec,\n platformKey,\n );\n if (cached != null) {\n await AppSettings.setOpenClawActiveRuntimeSpec(releaseSpec.toJson());\n _diag(\n 'runtime-update.use-cached',\n data: {\n 'platformKey': platformKey,\n 'version': release.version,\n 'runtimeDir': cached.path,\n },\n );\n return releaseSpec;\n }\n\n return await _downloadAndInstallRuntimeUpdate(\n release: release,\n platformKey: platformKey,\n );\n } catch (error, stackTrace) {\n _diag(\n 'runtime-update.failed',\n data: {\n 'platformKey': platformKey,\n 'currentVersion': currentSpec.version,\n 'error': error.toString(),\n },\n error: error,\n stackTrace: stackTrace,\n );\n return null;\n }\n }\n\n Future<_OpenClawRuntimeRelease?> _fetchRuntimeRelease({\n required String platformKey,\n required String currentVersion,\n }) async {\n final baseUrl = AppConfig.currentAiBackendUrl.replaceFirst(\n RegExp(r'/+$'),\n '',\n );\n final uri = Uri.parse('$baseUrl/api/openclaw/runtime/manifest').replace(\n queryParameters: {\n 'platform': platformKey,\n 'currentVersion': currentVersion,\n 'appVersion': AppConfig.appVersion,\n },\n );\n final response = await http\n .get(uri, headers: {'Accept': 'application/json'})\n .timeout(const Duration(seconds: 20));\n if (response.statusCode < 200 || response.statusCode >= 300) {\n _diag(\n 'runtime-update.manifest-http',\n data: {'statusCode': response.statusCode, 'uri': uri.toString()},\n );\n return null;\n }\n final decoded = _decodeJsonOrNull(response.body);\n if (decoded is! Map) return null;\n final payload = Map<String, dynamic>.from(decoded);\n if (payload['updateAvailable'] != true) {\n _diag(\n 'runtime-update.none',\n data: {\n 'platformKey': platformKey,\n 'reason': payload['reason']?.toString(),\n 'currentVersion': currentVersion,\n 'latestVersion': payload['latestVersion']?.toString(),\n },\n );\n return null;\n }\n final latest = payload['latest'];\n if (latest is! Map) return null;\n return _releaseFromJson(Map<String, dynamic>.from(latest));\n }\n\n _OpenClawRuntimeRelease? _releaseFromJson(Map<String, dynamic> json) {\n final version = json['version']?.toString().trim() ?? '';\n final archiveUrl = json['archiveUrl']?.toString().trim() ?? '';\n final sha256Hex = json['sha256']?.toString().trim().toLowerCase() ?? '';\n final archiveUri = Uri.tryParse(archiveUrl);\n final gatewayArgs =\n (json['gatewayArgs'] as List? ??\n const ['gateway', '--port', '{port}', '--force'])\n .map((item) => item.toString())\n .toList();\n if (version.isEmpty ||\n archiveUri == null ||\n !archiveUri.hasScheme ||\n !RegExp(r'^[a-f0-9]{64}$').hasMatch(sha256Hex)) {\n _diag(\n 'runtime-update.invalid-release',\n data: {\n 'version': version,\n 'archiveUrlSet': archiveUrl.isNotEmpty,\n 'sha256Length': sha256Hex.length,\n },\n );\n return null;\n }\n return _OpenClawRuntimeRelease(\n version: version,\n archiveUri: archiveUri,\n sha256Hex: sha256Hex,\n size: _readInt(json['size']) ?? 0,\n defaultPort: _readInt(json['defaultPort']) ?? 18789,\n defaultModel: (json['defaultModel'] ?? _defaultGatewayModel).toString(),\n defaultModelOverride: json['defaultModelOverride']?.toString(),\n nodeExecutable: (json['nodeExecutable'] ?? 'node/bin/node').toString(),\n cliEntrypoint: (json['cliEntrypoint'] ?? 'openclaw/openclaw.mjs')\n .toString(),\n gatewayArgs: gatewayArgs,\n );\n }\n\n Future<_OpenClawBundleSpec> _downloadAndInstallRuntimeUpdate({\n required _OpenClawRuntimeRelease release,\n required String platformKey,\n }) async {\n final spec = release.toBundleSpec();\n final runtimeDir = await _runtimeDir(spec, platformKey);\n final support = await getApplicationSupportDirectory();\n final downloadDir = Directory(p.join(support.path, 'openclaw_downloads'));\n await downloadDir.create(recursive: true);\n final archivePath = p.join(\n downloadDir.path,\n '${_safePathPart(release.version)}-$platformKey${_archiveExtension(release.archiveUri)}',\n );\n final archiveFile = File(archivePath);\n final digest = await _downloadRuntimeArchive(release, archiveFile);\n if (digest != release.sha256Hex) {\n await _deleteFileIfExists(archiveFile);\n throw OpenClawRuntimeException(\n 'OpenClaw runtime 校验失败: expected=${release.sha256Hex}, actual=$digest',\n );\n }\n\n final extractDir = Directory(\n p.join(\n downloadDir.path,\n '${_safePathPart(release.version)}-$platformKey-extract',\n ),\n );\n final stagingDir = Directory(\n p.join(\n downloadDir.path,\n '${_safePathPart(release.version)}-$platformKey-stage',\n ),\n );\n if (await extractDir.exists()) await extractDir.delete(recursive: true);\n if (await stagingDir.exists()) await stagingDir.delete(recursive: true);\n await extractDir.create(recursive: true);\n await _extractRuntimeArchive(archiveFile, extractDir);\n\n final extractedRoot = await _findExtractedRuntimeRoot(\n extractDir: extractDir,\n spec: spec,\n );\n if (extractedRoot == null) {\n throw OpenClawRuntimeException('下载的 OpenClaw runtime 不包含必要的 node/cli 文件');\n }\n\n await runtimeDir.parent.create(recursive: true);\n if (await runtimeDir.exists()) await runtimeDir.delete(recursive: true);\n await _moveOrCopyRuntimeDirectory(extractedRoot, stagingDir);\n await stagingDir.rename(runtimeDir.path);\n final marker = File(p.join(runtimeDir.path, '.bundle_ready'));\n await marker.writeAsString(\n const JsonEncoder.withIndent(' ').convert({\n 'installedAt': DateTime.now().toIso8601String(),\n 'version': release.version,\n 'platform': platformKey,\n 'sha256': digest,\n 'source': release.archiveUri.toString(),\n }),\n );\n await _repairRuntimeLaunchMetadata(\n runtimeDir: runtimeDir,\n nodePath: File(p.join(runtimeDir.path, spec.nodeExecutable)),\n platformKey: platformKey,\n cached: false,\n );\n await AppSettings.setOpenClawActiveRuntimeSpec(spec.toJson());\n await _deleteDirectoryIfExists(extractDir);\n await _deleteFileIfExists(archiveFile);\n _diag(\n 'runtime-update.installed',\n data: {\n 'platformKey': platformKey,\n 'version': release.version,\n 'runtimeDir': runtimeDir.path,\n 'size': release.size,\n },\n );\n return spec;\n }\n\n Future<String> _downloadRuntimeArchive(\n _OpenClawRuntimeRelease release,\n File destination,\n ) async {\n final client = http.Client();\n IOSink? output;\n try {\n final request = http.Request('GET', release.archiveUri)\n ..headers['Accept'] = 'application/octet-stream';\n final response = await client\n .send(request)\n .timeout(const Duration(seconds: 20));\n if (response.statusCode < 200 || response.statusCode >= 300) {\n throw OpenClawRuntimeException(\n 'OpenClaw runtime 下载失败 (${response.statusCode})',\n );\n }\n await destination.parent.create(recursive: true);\n output = destination.openWrite();\n final digestSink = AccumulatorSink<Digest>();\n final hashSink = sha256.startChunkedConversion(digestSink);\n var bytes = 0;\n await for (final chunk in response.stream.timeout(\n const Duration(minutes: 5),\n )) {\n bytes += chunk.length;\n hashSink.add(chunk);\n output.add(chunk);\n }\n await output.flush();\n await output.close();\n output = null;\n hashSink.close();\n final digest = digestSink.events.single.toString();\n _diag(\n 'runtime-update.downloaded',\n data: {\n 'archiveUri': release.archiveUri.toString(),\n 'bytes': bytes,\n 'sha256': digest,\n },\n );\n return digest;\n } finally {\n await output?.close().catchError((_) {});\n client.close();\n }\n }\n\n Future<void> _extractRuntimeArchive(\n File archiveFile,\n Directory destination,\n ) async {\n final lowerPath = archiveFile.path.toLowerCase();\n if (Platform.isMacOS && lowerPath.endsWith('.zip')) {\n try {\n final result = await Process.run('/usr/bin/ditto', [\n '-x',\n '-k',\n archiveFile.path,\n destination.path,\n ]).timeout(const Duration(minutes: 5));\n _diag(\n 'runtime-update.extract-native',\n data: {\n 'archivePath': archiveFile.path,\n 'destination': destination.path,\n 'exitCode': result.exitCode,\n if ((result.stderr as Object).toString().isNotEmpty)\n 'stderr': _previewProcessOutput(result.stderr),\n },\n );\n if (result.exitCode == 0) return;\n } catch (error, stackTrace) {\n _diag(\n 'runtime-update.extract-native-failed',\n data: {\n 'archivePath': archiveFile.path,\n 'destination': destination.path,\n 'error': error.toString(),\n },\n error: error,\n stackTrace: stackTrace,\n );\n }\n }\n\n await extractFileToDisk(archiveFile.path, destination.path);\n _diag(\n 'runtime-update.extract-dart',\n data: {'archivePath': archiveFile.path, 'destination': destination.path},\n );\n }\n\n Future<Directory?> _findExtractedRuntimeRoot({\n required Directory extractDir,\n required _OpenClawBundleSpec spec,\n }) async {\n final direct = await _runtimeRootIfComplete(extractDir, spec);\n if (direct != null) return direct;\n await for (final entity in extractDir.list(followLinks: false)) {\n if (entity is! Directory) continue;\n final nested = await _runtimeRootIfComplete(entity, spec);\n if (nested != null) return nested;\n }\n return null;\n }\n\n Future<Directory?> _runtimeRootIfComplete(\n Directory dir,\n _OpenClawBundleSpec spec,\n ) async {\n final nodePath = File(p.join(dir.path, spec.nodeExecutable));\n final cliPath = File(p.join(dir.path, spec.cliEntrypoint));\n if (await nodePath.exists() && await cliPath.exists()) return dir;\n return null;\n }\n\n Future<Directory?> _cachedRuntimeDirIfComplete(\n _OpenClawBundleSpec spec,\n String platformKey,\n ) async {\n final runtimeDir = await _runtimeDir(spec, platformKey);\n final marker = File(p.join(runtimeDir.path, '.bundle_ready'));\n final nodePath = File(p.join(runtimeDir.path, spec.nodeExecutable));\n final cliPath = File(p.join(runtimeDir.path, spec.cliEntrypoint));\n if (await marker.exists() &&\n await nodePath.exists() &&\n await cliPath.exists()) {\n return runtimeDir;\n }\n return null;\n }\n\n String _safePathPart(String value) {\n final safe = value.replaceAll(RegExp(r'[^A-Za-z0-9_.-]+'), '-');\n return safe.isEmpty ? 'runtime' : safe;\n }\n\n String _archiveExtension(Uri uri) {\n final lower = uri.path.toLowerCase();\n if (lower.endsWith('.tar.gz')) return '.tar.gz';\n if (lower.endsWith('.tgz')) return '.tgz';\n if (lower.endsWith('.tar')) return '.tar';\n return '.zip';\n }\n\n Future<void> _deleteFileIfExists(File file) async {\n try {\n if (await file.exists()) await file.delete();\n } catch (_) {}\n }\n\n Future<void> _deleteDirectoryIfExists(Directory dir) async {\n try {\n if (await dir.exists()) await dir.delete(recursive: true);\n } catch (_) {}\n }\n\n Future<Directory> _prepareBundle(\n _OpenClawBundleSpec spec,\n String platformKey,\n ) async {\n final overrideRuntimeDir = await _runtimeDirOverride(spec, platformKey);\n if (overrideRuntimeDir != null) {\n final nodePath = File(\n p.join(overrideRuntimeDir.path, spec.nodeExecutable),\n );\n await _repairRuntimeLaunchMetadata(\n runtimeDir: overrideRuntimeDir,\n nodePath: nodePath,\n platformKey: platformKey,\n cached: true,\n );\n _diag(\n 'bundle.launch.override-runtime',\n data: {\n 'platformKey': platformKey,\n 'runtimeDir': overrideRuntimeDir.path,\n },\n );\n return overrideRuntimeDir;\n }\n\n if (spec.downloaded) {\n final downloadedRuntimeDir = await _cachedRuntimeDirIfComplete(\n spec,\n platformKey,\n );\n if (downloadedRuntimeDir != null) {\n await _repairRuntimeLaunchMetadata(\n runtimeDir: downloadedRuntimeDir,\n nodePath: File(\n p.join(downloadedRuntimeDir.path, spec.nodeExecutable),\n ),\n platformKey: platformKey,\n cached: true,\n );\n _diag(\n 'bundle.launch.downloaded-runtime',\n data: {\n 'platformKey': platformKey,\n 'version': spec.version,\n 'runtimeDir': downloadedRuntimeDir.path,\n },\n );\n return downloadedRuntimeDir;\n }\n await AppSettings.clearOpenClawActiveRuntimeSpec();\n throw OpenClawRuntimeException(\n '已下载的 OpenClaw runtime 不完整,已回退到内置版本,请重新启动。',\n );\n }\n\n final bundledRuntimeDir = await _bundledRuntimeDir(spec, platformKey);\n if (bundledRuntimeDir != null) {\n _diag(\n 'bundle.launch.bundled-runtime',\n data: {\n 'platformKey': platformKey,\n 'runtimeDir': bundledRuntimeDir.path,\n },\n );\n return bundledRuntimeDir;\n }\n\n if (Platform.isMacOS) {\n throw OpenClawRuntimeException(\n '当前安装包没有可执行的 $platformKey OpenClaw runtime。请先运行 scripts/build_openclaw_desktop_bundle.sh $platformKey,再通过 flutter run 启动。',\n );\n }\n\n final runtimeDir = await _runtimeDir(spec, platformKey);\n final marker = File(p.join(runtimeDir.path, '.bundle_ready'));\n final nodePath = File(p.join(runtimeDir.path, spec.nodeExecutable));\n final cliPath = File(p.join(runtimeDir.path, spec.cliEntrypoint));\n\n if (await marker.exists() &&\n await nodePath.exists() &&\n await cliPath.exists()) {\n await _repairRuntimeLaunchMetadata(\n runtimeDir: runtimeDir,\n nodePath: nodePath,\n platformKey: platformKey,\n cached: true,\n );\n _diag(\n 'bundle.prepare.cached',\n data: {\n 'platformKey': platformKey,\n 'runtimeDir': runtimeDir.path,\n 'marker': marker.path,\n },\n );\n return runtimeDir;\n }\n\n final legacyRuntimeDir = await _legacyRuntimeDir(spec, platformKey);\n if (legacyRuntimeDir.path != runtimeDir.path &&\n await File(\n p.join(legacyRuntimeDir.path, spec.nodeExecutable),\n ).exists() &&\n await File(\n p.join(legacyRuntimeDir.path, spec.cliEntrypoint),\n ).exists()) {\n _diag(\n 'bundle.prepare.migrate-legacy',\n data: {\n 'platformKey': platformKey,\n 'from': legacyRuntimeDir.path,\n 'to': runtimeDir.path,\n },\n );\n if (await runtimeDir.exists()) {\n await runtimeDir.delete(recursive: true);\n }\n await _copyRuntimeDirectory(legacyRuntimeDir, runtimeDir);\n await _repairRuntimeLaunchMetadata(\n runtimeDir: runtimeDir,\n nodePath: nodePath,\n platformKey: platformKey,\n cached: false,\n );\n await marker.writeAsString(DateTime.now().toIso8601String());\n _diag(\n 'bundle.prepare.migrate-complete',\n data: {\n 'platformKey': platformKey,\n 'runtimeDir': runtimeDir.path,\n 'nodeExists': await nodePath.exists(),\n 'cliExists': await cliPath.exists(),\n },\n );\n return runtimeDir;\n }\n\n final prefix = 'assets/openclaw/$platformKey/';\n final assets = await _listAssets(prefix);\n final hasNodeAsset = assets.contains('$prefix${spec.nodeExecutable}');\n final hasCliAsset = assets.contains('$prefix${spec.cliEntrypoint}');\n _diag(\n 'bundle.prepare.assets',\n data: {\n 'platformKey': platformKey,\n 'prefix': prefix,\n 'assetCount': assets.length,\n 'hasNodeAsset': hasNodeAsset,\n 'hasCliAsset': hasCliAsset,\n 'nodeAsset': '$prefix${spec.nodeExecutable}',\n 'cliAsset': '$prefix${spec.cliEntrypoint}',\n 'sample': assets.take(8).toList(),\n },\n );\n if (!hasNodeAsset || !hasCliAsset) {\n throw OpenClawRuntimeException(\n '当前安装包没有内置 $platformKey 的 OpenClaw runtime。请在 release 构建中运行 scripts/build_openclaw_desktop_bundle.sh 后再打包。',\n );\n }\n\n if (await runtimeDir.exists()) {\n _diag('bundle.prepare.remove-old', data: {'runtimeDir': runtimeDir.path});\n await runtimeDir.delete(recursive: true);\n }\n await runtimeDir.create(recursive: true);\n\n for (final asset in assets) {\n final relative = asset.substring(prefix.length);\n if (relative.isEmpty || relative.endsWith('/')) continue;\n final bytes = await rootBundle.load(asset);\n final file = File(p.join(runtimeDir.path, relative));\n await file.parent.create(recursive: true);\n await file.writeAsBytes(\n bytes.buffer.asUint8List(bytes.offsetInBytes, bytes.lengthInBytes),\n flush: false,\n );\n }\n\n await _repairRuntimeLaunchMetadata(\n runtimeDir: runtimeDir,\n nodePath: nodePath,\n platformKey: platformKey,\n cached: false,\n );\n await marker.writeAsString(DateTime.now().toIso8601String());\n _diag(\n 'bundle.prepare.complete',\n data: {\n 'platformKey': platformKey,\n 'runtimeDir': runtimeDir.path,\n 'assetCount': assets.length,\n 'nodeExists': await nodePath.exists(),\n 'nodeSize': await nodePath.exists() ? await nodePath.length() : 0,\n 'cliExists': await cliPath.exists(),\n 'cliSize': await cliPath.exists() ? await cliPath.length() : 0,\n },\n );\n return runtimeDir;\n }\n\n Future<void> _copyRuntimeDirectory(\n Directory source,\n Directory destination,\n ) async {\n await destination.create(recursive: true);\n await for (final entity in source.list(\n recursive: true,\n followLinks: false,\n )) {\n final relative = p.relative(entity.path, from: source.path);\n if (relative == '.') continue;\n final target = p.join(destination.path, relative);\n if (entity is Directory) {\n await Directory(target).create(recursive: true);\n } else if (entity is File) {\n final targetFile = File(target);\n await targetFile.parent.create(recursive: true);\n await entity.openRead().pipe(targetFile.openWrite());\n }\n }\n }\n\n Future<void> _moveOrCopyRuntimeDirectory(\n Directory source,\n Directory destination,\n ) async {\n if (await destination.exists()) {\n await destination.delete(recursive: true);\n }\n await destination.parent.create(recursive: true);\n try {\n await source.rename(destination.path);\n return;\n } on FileSystemException {\n await _copyRuntimeDirectory(source, destination);\n }\n }\n\n Future<void> _repairRuntimeLaunchMetadata({\n required Directory runtimeDir,\n required File nodePath,\n required String platformKey,\n required bool cached,\n }) async {\n if (Platform.isWindows) return;\n\n final chmodResult = await Process.run('/bin/chmod', ['+x', nodePath.path]);\n ProcessResult? xattrResult;\n if (Platform.isMacOS) {\n // Files created by a quarantined app can inherit the quarantine xattr.\n // macOS then rejects Process.start with \"Operation not permitted\".\n xattrResult = await Process.run('/usr/bin/xattr', [\n '-dr',\n 'com.apple.quarantine',\n runtimeDir.path,\n ]);\n await Process.run('/usr/bin/xattr', [\n '-dr',\n 'com.apple.provenance',\n runtimeDir.path,\n ]);\n }\n\n _diag(\n 'bundle.prepare.launch-metadata',\n data: {\n 'platformKey': platformKey,\n 'runtimeDir': runtimeDir.path,\n 'nodePath': nodePath.path,\n 'cached': cached,\n 'chmodExitCode': chmodResult.exitCode,\n if ((chmodResult.stderr as Object).toString().isNotEmpty)\n 'chmodStderr': _previewProcessOutput(chmodResult.stderr),\n if (xattrResult != null) 'xattrExitCode': xattrResult.exitCode,\n if (xattrResult != null &&\n (xattrResult.stderr as Object).toString().isNotEmpty)\n 'xattrStderr': _previewProcessOutput(xattrResult.stderr),\n },\n );\n }\n\n Future<bool> _hasRequiredBundleAssets(\n String platformKey,\n _OpenClawBundleSpec spec,\n ) async {\n final prefix = 'assets/openclaw/$platformKey/';\n final assets = await _listAssets(prefix);\n final hasRequired =\n assets.contains('$prefix${spec.nodeExecutable}') &&\n assets.contains('$prefix${spec.cliEntrypoint}');\n _diag(\n 'bundle.has-required-assets',\n data: {\n 'platformKey': platformKey,\n 'assetCount': assets.length,\n 'hasRequired': hasRequired,\n },\n );\n return hasRequired;\n }\n\n Future<List<String>> _listAssets(String prefix) async {\n final assets = <String>{};\n var jsonCount = 0;\n var binCount = 0;\n var indexCount = 0;\n final failures = <String>[];\n\n try {\n final raw = await rootBundle.loadString('AssetManifest.json');\n final decoded = jsonDecode(raw) as Map<String, dynamic>;\n final items = decoded.keys\n .where((key) => key.startsWith(prefix))\n .toList();\n jsonCount = items.length;\n assets.addAll(items);\n } catch (error) {\n failures.add('AssetManifest.json: $error');\n // Newer Flutter release builds may only include AssetManifest.bin.\n }\n\n try {\n final manifest = await AssetManifest.loadFromAssetBundle(rootBundle);\n final items = manifest\n .listAssets()\n .where((key) => key.startsWith(prefix))\n .toList();\n binCount = items.length;\n assets.addAll(items);\n } catch (error) {\n failures.add('AssetManifest.bin: $error');\n // App Store archives patched after flutter build carry a JSON/index file.\n }\n\n try {\n final raw = await rootBundle.loadString(\n 'assets/openclaw/asset_index.json',\n );\n final decoded = jsonDecode(raw);\n final indexedAssets = decoded is Map\n ? decoded['assets']\n : decoded is List\n ? decoded\n : const [];\n if (indexedAssets is List) {\n final items = indexedAssets\n .map((item) => item.toString())\n .where((key) => key.startsWith(prefix))\n .toList();\n indexCount = items.length;\n assets.addAll(items);\n }\n } catch (error) {\n failures.add('asset_index.json: $error');\n // Older builds do not have an OpenClaw asset index.\n }\n\n final sortedAssets = assets.toList()..sort();\n _diag(\n 'asset.list',\n data: {\n 'prefix': prefix,\n 'total': sortedAssets.length,\n 'jsonCount': jsonCount,\n 'binCount': binCount,\n 'indexCount': indexCount,\n if (failures.isNotEmpty) 'failures': failures,\n 'sample': sortedAssets.take(8).toList(),\n },\n );\n return sortedAssets;\n }\n\n Future<bool> _probe(\n int port,\n String token, {\n String context = 'probe',\n }) async {\n try {\n final healthResponse = await http\n .get(\n Uri.parse('http://127.0.0.1:$port/health'),\n headers: {'Authorization': 'Bearer $token'},\n )\n .timeout(_probeTimeout);\n final healthPayload = _decodeJsonOrNull(healthResponse.body);\n if (healthResponse.statusCode < 200 ||\n healthResponse.statusCode >= 300 ||\n !_looksLikeHealthPayload(healthPayload)) {\n _diag(\n 'probe.health-unhealthy',\n data: {\n 'context': context,\n 'port': port,\n 'statusCode': healthResponse.statusCode,\n 'contentType': healthResponse.headers['content-type'],\n 'bodyPreview': _previewBody(healthResponse.body),\n },\n );\n return false;\n }\n\n final modelsResponse = await http\n .get(\n Uri.parse('http://127.0.0.1:$port/v1/models'),\n headers: {'Authorization': 'Bearer $token'},\n )\n .timeout(_probeTimeout);\n final modelsPayload = _decodeJsonOrNull(modelsResponse.body);\n if (modelsResponse.statusCode < 200 ||\n modelsResponse.statusCode >= 300 ||\n !_looksLikeModelsPayload(modelsPayload)) {\n _diag(\n 'probe.models-unhealthy',\n data: {\n 'context': context,\n 'port': port,\n 'statusCode': modelsResponse.statusCode,\n 'contentType': modelsResponse.headers['content-type'],\n 'bodyPreview': _previewBody(modelsResponse.body),\n },\n );\n return false;\n }\n\n // `/v1/models` can be healthy while `/v1/chat/completions` is disabled\n // by an old config. Use GET so the probe only checks route wiring and\n // never starts a real model request.\n final chatResponse = await http\n .get(\n Uri.parse('http://127.0.0.1:$port/v1/chat/completions'),\n headers: {'Authorization': 'Bearer $token'},\n )\n .timeout(_probeTimeout);\n final healthy =\n chatResponse.statusCode != 401 &&\n chatResponse.statusCode != 403 &&\n chatResponse.statusCode != 404;\n _diag(\n healthy ? 'probe.healthy' : 'probe.chat-unhealthy',\n data: {\n 'context': context,\n 'port': port,\n 'chatStatusCode': chatResponse.statusCode,\n },\n );\n return healthy;\n } catch (error) {\n if (context != 'startup') {\n _diag(\n 'probe.error',\n data: {'context': context, 'port': port},\n error: error,\n );\n }\n return false;\n }\n }\n\n Future<File> _ensureConfigFile({\n required Directory stateRoot,\n required int port,\n required String token,\n required String backendDeepSeekModel,\n required String deepSeekProxyBaseUrl,\n }) async {\n final configDir = Directory(p.join(stateRoot.path, 'config'));\n await configDir.create(recursive: true);\n final workspace = Directory(p.join(stateRoot.path, 'workspace'));\n await workspace.create(recursive: true);\n final configPath = File(p.join(configDir.path, 'openclaw.json'));\n\n final config = <String, dynamic>{\n 'gateway': {\n 'mode': 'local',\n 'port': port,\n 'bind': 'loopback',\n 'auth': {'mode': 'token', 'token': token},\n 'http': {\n 'endpoints': {\n 'chatCompletions': {'enabled': true},\n 'responses': {'enabled': true},\n },\n },\n },\n 'models': {\n 'mode': 'merge',\n 'providers': {\n _backendDeepSeekProviderId: {\n 'baseUrl': deepSeekProxyBaseUrl,\n 'api': 'openai-completions',\n 'apiKey': 'DACHENG_OPENCLAW_PROXY_TOKEN',\n 'authHeader': true,\n 'headers': {'x-dacheng-auth-token': 'DACHENG_AUTH_TOKEN'},\n 'models': [\n {\n 'id': 'deepseek-chat',\n 'name': 'DeepSeek Chat',\n 'contextWindow': 131072,\n 'maxTokens': 8192,\n 'input': ['text'],\n 'compat': {\n 'requiresStringContent': true,\n 'strictMessageKeys': true,\n },\n },\n {\n 'id': 'deepseek-reasoner',\n 'name': 'DeepSeek Reasoner',\n 'contextWindow': 131072,\n 'maxTokens': 8192,\n 'reasoning': true,\n 'input': ['text'],\n 'compat': {\n 'requiresStringContent': true,\n 'strictMessageKeys': true,\n },\n },\n ],\n },\n },\n },\n 'plugins': {\n 'enabled': false,\n 'allow': <String>[],\n 'deny': <String>[],\n 'load': {'paths': <String>[]},\n 'slots': {'memory': 'none'},\n 'entries': <String, dynamic>{\n 'memory-core': {'enabled': false},\n 'bonjour': {'enabled': false},\n 'browser': {'enabled': false},\n 'canvas': {'enabled': false},\n 'device-pair': {'enabled': false},\n 'file-transfer': {'enabled': false},\n 'phone-control': {'enabled': false},\n 'talk-voice': {'enabled': false},\n },\n },\n 'agents': {\n 'defaults': {\n 'workspace': workspace.path,\n 'model': {'primary': backendDeepSeekModel},\n },\n 'list': [\n {\n 'id': 'dacheng',\n 'default': true,\n 'workspace': workspace.path,\n 'model': {'primary': backendDeepSeekModel},\n },\n ],\n },\n };\n\n // Repair older embedded configs in-place. OpenClaw 2026.6 requires\n // gateway.mode=local, and the home screen requires the OpenAI-compatible\n // chat endpoint. Preserve unrelated user edits while fixing the fields\n // needed for the app-owned embedded gateway to start reliably.\n final merged = await _mergeEmbeddedConfig(configPath, config);\n await configPath.writeAsString(\n const JsonEncoder.withIndent(' ').convert(merged),\n );\n _diag(\n 'config.written',\n data: {\n 'configPath': configPath.path,\n 'port': port,\n 'gatewayMode': _mutableMap(merged['gateway'])['mode'],\n 'backendDeepSeekModel': backendDeepSeekModel,\n 'deepSeekProxyBaseUrl': deepSeekProxyBaseUrl,\n },\n );\n return configPath;\n }\n\n Future<File> _ensureDesktopToolsManifest({\n required Directory stateRoot,\n required _DesktopToolsLaunch desktopTools,\n }) async {\n final configDir = Directory(p.join(stateRoot.path, 'config'));\n await configDir.create(recursive: true);\n final manifestPath = File(p.join(configDir.path, 'desktop_tools.json'));\n final tools =\n DesktopControlPolicy.supportedTools\n .map(\n (name) => {\n 'name': name,\n 'endpoint': '/v1/tools/execute',\n 'readOnly': DesktopControlPolicy.isReadOnly(name),\n 'requiresConfirmation':\n DesktopControlPolicy.requiresConfirmation(name),\n },\n )\n .toList()\n ..sort(\n (a, b) => a['name'].toString().compareTo(b['name'].toString()),\n );\n\n await manifestPath.writeAsString(\n const JsonEncoder.withIndent(' ').convert({\n 'version': 1,\n 'transport': 'http-loopback',\n 'baseUrl': desktopTools.uri?.toString(),\n 'auth': {'type': 'bearer', 'env': 'DACHENG_DESKTOP_TOOLS_TOKEN'},\n 'status': desktopTools.statusJson,\n 'tools': tools,\n }),\n );\n _diag(\n 'desktop-tools.manifest-written',\n data: {\n 'manifestPath': manifestPath.path,\n 'hasBaseUrl': desktopTools.uri != null,\n },\n );\n return manifestPath;\n }\n\n Future<File> _ensureAgentModelConfig({\n required Directory stateRoot,\n required String deepSeekProxyBaseUrl,\n }) async {\n final agentDir = Directory(\n p.join(stateRoot.path, 'state', 'agents', 'dacheng', 'agent'),\n );\n await agentDir.create(recursive: true);\n final modelsPath = File(p.join(agentDir.path, 'models.json'));\n final providerConfig = <String, dynamic>{\n 'baseUrl': deepSeekProxyBaseUrl,\n 'api': 'openai-completions',\n 'apiKey': 'DACHENG_OPENCLAW_PROXY_TOKEN',\n 'authHeader': true,\n 'headers': {'x-dacheng-auth-token': 'DACHENG_AUTH_TOKEN'},\n 'models': [\n {\n 'id': 'deepseek-chat',\n 'name': 'DeepSeek Chat',\n 'contextWindow': 131072,\n 'maxTokens': 8192,\n 'input': ['text'],\n 'compat': {'requiresStringContent': true, 'strictMessageKeys': true},\n },\n {\n 'id': 'deepseek-reasoner',\n 'name': 'DeepSeek Reasoner',\n 'contextWindow': 131072,\n 'maxTokens': 8192,\n 'reasoning': true,\n 'input': ['text'],\n 'compat': {'requiresStringContent': true, 'strictMessageKeys': true},\n },\n ],\n };\n await modelsPath.writeAsString(\n const JsonEncoder.withIndent(' ').convert({\n 'providers': {_backendDeepSeekProviderId: providerConfig},\n }),\n );\n _diag(\n 'agent-models.written',\n data: {\n 'modelsPath': modelsPath.path,\n 'provider': _backendDeepSeekProviderId,\n 'deepSeekProxyBaseUrl': deepSeekProxyBaseUrl,\n },\n );\n return modelsPath;\n }\n\n Future<Map<String, dynamic>> _mergeEmbeddedConfig(\n File configPath,\n Map<String, dynamic> defaults,\n ) async {\n Map<String, dynamic> current = <String, dynamic>{};\n if (await configPath.exists()) {\n try {\n final decoded = jsonDecode(await configPath.readAsString());\n if (decoded is Map<String, dynamic>) {\n current = decoded;\n } else if (decoded is Map) {\n current = Map<String, dynamic>.from(decoded);\n }\n } catch (_) {\n current = <String, dynamic>{};\n }\n }\n\n final gateway = _mutableMap(current['gateway']);\n final defaultGateway = _mutableMap(defaults['gateway']);\n gateway['mode'] = 'local';\n gateway['port'] = defaultGateway['port'];\n gateway['bind'] = defaultGateway['bind'];\n gateway['auth'] = Map<String, dynamic>.from(defaultGateway['auth'] as Map);\n\n final gatewayHttp = _mutableMap(gateway['http']);\n final endpoints = _mutableMap(gatewayHttp['endpoints']);\n endpoints['chatCompletions'] = {'enabled': true};\n endpoints['responses'] = {'enabled': true};\n gatewayHttp['endpoints'] = endpoints;\n gateway['http'] = gatewayHttp;\n current['gateway'] = gateway;\n\n final env = _mutableMap(current['env']);\n env.remove('DEEPSEEK_API_KEY');\n if (env.isEmpty) {\n current.remove('env');\n } else {\n current['env'] = env;\n }\n\n final defaultModels = _mutableMap(defaults['models']);\n final models = _mutableMap(current['models']);\n models['mode'] ??= defaultModels['mode'];\n final providers = _mutableMap(models['providers']);\n final defaultProviders = _mutableMap(defaultModels['providers']);\n providers.remove('deepseek');\n providers.remove('dacheng-deepseek');\n providers.remove(_backendDeepSeekProviderId);\n for (final entry in defaultProviders.entries) {\n providers[entry.key] = entry.value is Map\n ? Map<String, dynamic>.from(entry.value as Map)\n : entry.value;\n }\n models['providers'] = providers;\n current['models'] = models;\n\n final defaultPlugins = _mutableMap(defaults['plugins']);\n final plugins = _mutableMap(current['plugins']);\n plugins['enabled'] = false;\n plugins['allow'] = <String>[];\n plugins['deny'] = <String>[];\n plugins['load'] = Map<String, dynamic>.from(\n _mutableMap(defaultPlugins['load']),\n );\n plugins['slots'] = Map<String, dynamic>.from(\n _mutableMap(defaultPlugins['slots']),\n );\n final entries = _mutableMap(plugins['entries']);\n final defaultEntries = _mutableMap(defaultPlugins['entries']);\n for (final entry in defaultEntries.entries) {\n entries[entry.key] = entry.value is Map\n ? Map<String, dynamic>.from(entry.value as Map)\n : entry.value;\n }\n plugins['entries'] = entries;\n current['plugins'] = plugins;\n\n final agents = _mutableMap(current['agents']);\n final defaultAgents = _mutableMap(defaults['agents']);\n final defaultAgentDefaults = _mutableMap(defaultAgents['defaults']);\n final agentDefaults = _mutableMap(agents['defaults']);\n agentDefaults['workspace'] = defaultAgentDefaults['workspace'];\n agentDefaults['model'] = defaultAgentDefaults['model'];\n agents['defaults'] = agentDefaults;\n\n final defaultList = (defaultAgents['list'] as List? ?? const [])\n .whereType<Map>()\n .map((item) => Map<String, dynamic>.from(item))\n .toList();\n final defaultDacheng = defaultList.isNotEmpty\n ? defaultList.first\n : <String, dynamic>{\n 'id': 'dacheng',\n 'default': true,\n 'workspace': agentDefaults['workspace'],\n 'model': agentDefaults['model'],\n };\n final list = (agents['list'] as List? ?? const [])\n .map((item) => item is Map ? Map<String, dynamic>.from(item) : item)\n .toList();\n var foundDacheng = false;\n for (var i = 0; i < list.length; i++) {\n final item = list[i];\n if (item is! Map<String, dynamic>) continue;\n if (item['id'] != 'dacheng') continue;\n foundDacheng = true;\n item['default'] = true;\n item['workspace'] = defaultDacheng['workspace'];\n item['model'] = defaultDacheng['model'];\n list[i] = item;\n }\n if (!foundDacheng) {\n list.add(defaultDacheng);\n }\n agents['list'] = list.isEmpty ? defaultList : list;\n current['agents'] = agents;\n\n return current;\n }\n\n String _gatewayChatModel(String model) {\n final trimmed = model.trim();\n if (trimmed == 'openclaw' || trimmed.startsWith('openclaw/')) {\n return trimmed;\n }\n return _defaultGatewayModel;\n }\n\n String _defaultDeepSeekModelFor(String model) {\n final trimmed = model.trim();\n if (trimmed.startsWith('deepseek/')) return trimmed;\n return _defaultDeepSeekModel;\n }\n\n String _backendDeepSeekModelRef(String model) {\n final trimmed = model.trim().isEmpty ? _defaultDeepSeekModel : model.trim();\n final modelId = trimmed.contains('/')\n ? trimmed.split('/').last.trim()\n : trimmed;\n final normalizedModelId = modelId.isEmpty ? 'deepseek-chat' : modelId;\n return '$_backendDeepSeekProviderId/$normalizedModelId';\n }\n\n String _deepSeekProxyBaseUrl() {\n final baseUrl = AppConfig.currentAiBackendUrl.replaceFirst(\n RegExp(r'/+$'),\n '',\n );\n return '$baseUrl/api/openclaw/deepseek/v1';\n }\n\n Map<String, dynamic> _mutableMap(Object? value) {\n if (value is Map<String, dynamic>) return Map<String, dynamic>.from(value);\n if (value is Map) return Map<String, dynamic>.from(value);\n return <String, dynamic>{};\n }\n\n void _captureLogs(Process process) {\n void addLog(String line) {\n final text = line.trim();\n if (text.isEmpty) return;\n _recentLogs.insert(0, text);\n if (_recentLogs.length > 80) {\n _recentLogs.removeRange(80, _recentLogs.length);\n }\n if (kDebugMode) {\n debugPrint('[OpenClaw] $text');\n }\n _diag('process.output', data: {'line': text});\n }\n\n process.stdout\n .transform(utf8.decoder)\n .transform(const LineSplitter())\n .listen(addLog, onError: (_) {});\n process.stderr\n .transform(utf8.decoder)\n .transform(const LineSplitter())\n .listen(addLog, onError: (_) {});\n }\n\n Future<Directory> _stateRoot() async {\n final support = await getApplicationSupportDirectory();\n final dir = Directory(p.join(support.path, 'openclaw_embedded'));\n await dir.create(recursive: true);\n return dir;\n }\n\n Future<_DesktopToolsLaunch> _ensureDesktopTools() async {\n try {\n final status = await DesktopControlBridge.instance.ensureStarted();\n _diag(\n 'desktop-tools.started',\n data: {\n 'bridgeRunning': status.bridgeRunning,\n 'bridgeUri': status.bridgeUri?.toString(),\n 'message': status.message,\n },\n );\n return _DesktopToolsLaunch(\n uri: status.bridgeUri,\n token: await DesktopControlBridge.instance.bridgeToken,\n statusJson: status.toJson(),\n );\n } catch (error) {\n _diag('desktop-tools.error', error: error);\n return _DesktopToolsLaunch(\n uri: null,\n token: null,\n statusJson: {\n 'enabledByBuild': false,\n 'bridgeRunning': false,\n 'message': error.toString(),\n },\n );\n }\n }\n\n Future<Directory> _runtimeDir(\n _OpenClawBundleSpec spec,\n String platformKey,\n ) async {\n final support = await getApplicationSupportDirectory();\n final runtimeBase = Platform.isMacOS\n ? Directory(\n p.join(\n support.parent.path,\n '${p.basename(support.path)}.openclaw_runtime',\n ),\n )\n : Directory(p.join(support.path, 'openclaw_embedded'));\n return Directory(\n p.join(runtimeBase.path, 'runtime', spec.version, platformKey),\n );\n }\n\n Future<Directory?> _bundledRuntimeDir(\n _OpenClawBundleSpec spec,\n String platformKey,\n ) async {\n if (!Platform.isMacOS) return null;\n\n final contentsDir = Directory(\n p.dirname(p.dirname(Platform.resolvedExecutable)),\n );\n final candidates = <Directory>[\n Directory(\n p.join(\n contentsDir.path,\n 'Frameworks',\n 'App.framework',\n 'Resources',\n 'flutter_assets',\n 'assets',\n 'openclaw',\n platformKey,\n ),\n ),\n Directory(\n p.join(\n contentsDir.path,\n 'Frameworks',\n 'App.framework',\n 'Versions',\n 'A',\n 'Resources',\n 'flutter_assets',\n 'assets',\n 'openclaw',\n platformKey,\n ),\n ),\n Directory(\n p.join(\n contentsDir.path,\n 'Resources',\n 'flutter_assets',\n 'assets',\n 'openclaw',\n platformKey,\n ),\n ),\n ];\n\n for (final dir in candidates) {\n final nodePath = File(p.join(dir.path, spec.nodeExecutable));\n final cliPath = File(p.join(dir.path, spec.cliEntrypoint));\n if (await nodePath.exists() && await cliPath.exists()) {\n return dir;\n }\n }\n _diag(\n 'bundle.macos-bundled-runtime-missing',\n data: {\n 'platformKey': platformKey,\n 'contentsDir': contentsDir.path,\n 'candidates': candidates.map((item) => item.path).toList(),\n },\n );\n return null;\n }\n\n Future<Directory?> _runtimeDirOverride(\n _OpenClawBundleSpec spec,\n String platformKey,\n ) async {\n final raw = _configuredRuntimeDirOverride(platformKey);\n if (raw == null) return null;\n\n final dir = Directory(raw);\n final nodePath = File(p.join(dir.path, spec.nodeExecutable));\n final cliPath = File(p.join(dir.path, spec.cliEntrypoint));\n final nodeExists = await nodePath.exists();\n final cliExists = await cliPath.exists();\n _diag(\n 'bundle.override.paths',\n data: {\n 'platformKey': platformKey,\n 'runtimeDir': dir.path,\n 'nodePath': nodePath.path,\n 'nodeExists': nodeExists,\n 'cliPath': cliPath.path,\n 'cliExists': cliExists,\n },\n );\n\n if (!nodeExists || !cliExists) {\n throw OpenClawRuntimeException(\n 'DACHENG_OPENCLAW_RUNTIME_DIR 指向的 OpenClaw runtime 不完整: ${dir.path}',\n );\n }\n return dir;\n }\n\n String? _configuredRuntimeDirOverride(String platformKey) {\n final raw = _runtimeDirOverrideDefine.trim().isNotEmpty\n ? _runtimeDirOverrideDefine.trim()\n : Platform.environment['DACHENG_OPENCLAW_RUNTIME_DIR']?.trim();\n if (raw == null || raw.isEmpty) return null;\n return p.normalize(raw.replaceAll('{platform}', platformKey));\n }\n\n Future<Directory> _legacyRuntimeDir(\n _OpenClawBundleSpec spec,\n String platformKey,\n ) async {\n final support = await getApplicationSupportDirectory();\n return Directory(\n p.join(\n support.path,\n 'openclaw_embedded',\n 'runtime',\n spec.version,\n platformKey,\n ),\n );\n }\n\n OpenClawRuntimeStatus _remember(OpenClawRuntimeStatus status) {\n _lastStatus = status;\n return status;\n }\n\n String? get _platformKey {\n final abi = Abi.current().toString().toLowerCase();\n if (Platform.isMacOS) {\n return abi.contains('arm64') ? 'macos-arm64' : 'macos-x64';\n }\n if (Platform.isWindows) {\n return abi.contains('arm64') ? 'windows-arm64' : 'windows-x64';\n }\n if (Platform.isLinux) {\n return abi.contains('arm64') ? 'linux-arm64' : 'linux-x64';\n }\n return null;\n }\n\n OpenClawRuntimeStatus? get lastStatus => _lastStatus;\n\n void _diag(\n String message, {\n Map<String, Object?> data = const {},\n Object? error,\n StackTrace? stackTrace,\n }) {\n unawaited(\n DiagnosticLogService.instance.log(\n 'openclaw.runtime',\n message,\n data: data,\n error: error,\n stackTrace: stackTrace,\n ),\n );\n }\n}\n\nint? _readInt(Object? value) {\n if (value is int) return value;\n if (value is num) return value.toInt();\n if (value is String) return int.tryParse(value);\n return null;\n}\n\nObject? _decodeJsonOrNull(String body) {\n try {\n return jsonDecode(body);\n } catch (_) {\n return null;\n }\n}\n\nbool _looksLikeHealthPayload(Object? payload) {\n if (payload is! Map) return false;\n if (payload['ok'] == true) return true;\n final status = payload['status']?.toString().toLowerCase();\n return status == 'live' || status == 'ok' || status == 'healthy';\n}\n\nbool _looksLikeModelsPayload(Object? payload) {\n if (payload is List) return true;\n if (payload is! Map) return false;\n return payload['data'] is List ||\n payload['models'] is List ||\n payload['object'] == 'list';\n}\n\nString _previewBody(String body) {\n final trimmed = body.trim();\n return trimmed.length <= 80 ? trimmed : trimmed.substring(0, 80);\n}\n\nString _previewProcessOutput(Object? output) {\n final text = output?.toString().trim() ?? '';\n return text.length <= 200 ? text : text.substring(0, 200);\n}\n"}, "2029d40b14d05d3b2991bdbb82e9751f6fe0a100": {"t": 1785757963.962858, "n": 70087, "h": "0b38bbef9e56289f", "c": "import 'dart:async';\nimport 'dart:convert';\nimport 'dart:ffi' show Abi;\nimport 'dart:io';\nimport 'package:archive/archive_io.dart';\nimport 'package:convert/convert.dart';\nimport 'package:crypto/crypto.dart';\nimport 'package:flutter/foundation.dart';\nimport 'package:flutter/services.dart';\nimport 'package:http/http.dart' as http;\nimport 'package:path/path.dart' as p;\nimport 'package:path_provider/path_provider.dart';\n\nimport '../../core/config/app_config.dart';\nimport '../app_settings.dart';\nimport '../diagnostic_log_service.dart';\nimport '../desktop_control/desktop_control_bridge.dart';\nimport '../desktop_control/desktop_control_policy.dart';\nimport 'openclaw_port_resolver.dart';\n\nenum OpenClawRuntimeState {\n unsupported,\n notBundled,\n stopped,\n starting,\n running,\n failed,\n}\n\nclass OpenClawRuntimeStatus {\n final OpenClawRuntimeState state;\n final String message;\n final int? port;\n final String? platformKey;\n final String? runtimePath;\n final Map<String, dynamic>? desktopToolsStatus;\n final DateTime checkedAt;\n\n const OpenClawRuntimeStatus({\n required this.state,\n required this.message,\n this.port,\n this.platformKey,\n this.runtimePath,\n this.desktopToolsStatus,\n required this.checkedAt,\n });\n\n bool get isHealthy => state == OpenClawRuntimeState.running;\n\n String get label {\n switch (state) {\n case OpenClawRuntimeState.unsupported:\n return '不支持';\n case OpenClawRuntimeState.notBundled:\n return '未内置';\n case OpenClawRuntimeState.stopped:\n return '未启动';\n case OpenClawRuntimeState.starting:\n return '启动中';\n case OpenClawRuntimeState.running:\n return '运行中';\n case OpenClawRuntimeState.failed:\n return '异常';\n }\n }\n}\n\nclass OpenClawGatewayTarget {\n final Uri baseUri;\n final String token;\n final String model;\n final String? modelOverride;\n final Uri? desktopToolsUri;\n final String? desktopToolsToken;\n final Map<String, dynamic>? desktopToolsStatus;\n\n const OpenClawGatewayTarget({\n required this.baseUri,\n required this.token,\n required this.model,\n this.modelOverride,\n this.desktopToolsUri,\n this.desktopToolsToken,\n this.desktopToolsStatus,\n });\n}\n\nclass _OpenClawBundleSpec {\n final String version;\n final int defaultPort;\n final String defaultModel;\n final String? defaultModelOverride;\n final String nodeExecutable;\n final String cliEntrypoint;\n final List<String> gatewayArgs;\n final bool downloaded;\n\n const _OpenClawBundleSpec({\n required this.version,\n required this.defaultPort,\n required this.defaultModel,\n this.defaultModelOverride,\n required this.nodeExecutable,\n required this.cliEntrypoint,\n required this.gatewayArgs,\n this.downloaded = false,\n });\n\n Map<String, dynamic> toJson() => {\n 'schema': 1,\n 'version': version,\n 'defaultPort': defaultPort,\n 'defaultModel': defaultModel,\n if (defaultModelOverride != null)\n 'defaultModelOverride': defaultModelOverride,\n 'nodeExecutable': nodeExecutable,\n 'cliEntrypoint': cliEntrypoint,\n 'gatewayArgs': gatewayArgs,\n 'downloaded': downloaded,\n };\n}\n\nclass _OpenClawRuntimeRelease {\n final String version;\n final Uri archiveUri;\n final String sha256Hex;\n final int size;\n final int defaultPort;\n final String defaultModel;\n final String? defaultModelOverride;\n final String nodeExecutable;\n final String cliEntrypoint;\n final List<String> gatewayArgs;\n\n const _OpenClawRuntimeRelease({\n required this.version,\n required this.archiveUri,\n required this.sha256Hex,\n required this.size,\n required this.defaultPort,\n required this.defaultModel,\n this.defaultModelOverride,\n required this.nodeExecutable,\n required this.cliEntrypoint,\n required this.gatewayArgs,\n });\n\n _OpenClawBundleSpec toBundleSpec() => _OpenClawBundleSpec(\n version: version,\n defaultPort: defaultPort,\n defaultModel: defaultModel,\n defaultModelOverride: defaultModelOverride,\n nodeExecutable: nodeExecutable,\n cliEntrypoint: cliEntrypoint,\n gatewayArgs: gatewayArgs,\n downloaded: true,\n );\n}\n\nclass _DesktopToolsLaunch {\n final Uri? uri;\n final String? token;\n final Map<String, dynamic> statusJson;\n\n const _DesktopToolsLaunch({\n required this.uri,\n required this.token,\n required this.statusJson,\n });\n}\n\nclass OpenClawRuntimeException implements Exception {\n final String message;\n\n const OpenClawRuntimeException(this.message);\n\n @override\n String toString() => message;\n}\n\nclass OpenClawRuntime {\n OpenClawRuntime._();\n\n static final OpenClawRuntime instance = OpenClawRuntime._();\n\n static const String _manifestAsset = 'assets/openclaw/bundle_manifest.json';\n static const String _runtimeDirOverrideDefine = String.fromEnvironment(\n 'DACHENG_OPENCLAW_RUNTIME_DIR',\n );\n static const String _defaultGatewayModel =\n AppSettings.defaultOpenClawGatewayModel;\n static const String _defaultDeepSeekModel =\n AppSettings.defaultOpenClawDeepSeekModel;\n static const String _backendDeepSeekProviderId = 'dacheng-deepseek-proxy';\n static const Duration _startupTimeout = Duration(seconds: 120);\n static const Duration _probeTimeout = Duration(seconds: 3);\n\n Process? _process;\n Future<OpenClawGatewayTarget>? _starting;\n String _processAuthToken = '';\n OpenClawRuntimeStatus? _lastStatus;\n final List<String> _recentLogs = <String>[];\n final OpenClawPortResolver _portResolver = const OpenClawPortResolver();\n\n Future<OpenClawRuntimeStatus> getStatus({bool probe = true}) async {\n final platformKey = _platformKey;\n final port = await AppSettings.getOpenClawGatewayPort();\n _diag(\n 'status.start',\n data: {'platformKey': platformKey, 'port': port, 'probe': probe},\n );\n\n if (platformKey == null) {\n return _remember(\n OpenClawRuntimeStatus(\n state: OpenClawRuntimeState.unsupported,\n message: '当前平台不是 macOS / Windows / Linux 桌面端',\n port: port,\n checkedAt: DateTime.now(),\n ),\n );\n }\n\n try {\n final spec = await _loadSpec(platformKey, checkUpdates: false);\n final overrideRuntimeDir = await _runtimeDirOverride(spec, platformKey);\n final bundledRuntimeDir = overrideRuntimeDir == null\n ? await _bundledRuntimeDir(spec, platformKey)\n : null;\n final runtimeDir =\n overrideRuntimeDir ??\n bundledRuntimeDir ??\n await _runtimeDir(spec, platformKey);\n final nodePath = p.join(runtimeDir.path, spec.nodeExecutable);\n final cliPath = p.join(runtimeDir.path, spec.cliEntrypoint);\n final nodeExists = await File(nodePath).exists();\n final cliExists = await File(cliPath).exists();\n _diag(\n 'status.paths',\n data: {\n 'platformKey': platformKey,\n 'runtimeDir': runtimeDir.path,\n 'nodePath': nodePath,\n 'nodeExists': nodeExists,\n 'cliPath': cliPath,\n 'cliExists': cliExists,\n 'runtimeSource': overrideRuntimeDir != null\n ? 'override'\n : (bundledRuntimeDir == null ? 'cache' : 'bundle'),\n },\n );\n\n if (!nodeExists || !cliExists) {\n final hasBundleAssets = await _hasRequiredBundleAssets(\n platformKey,\n spec,\n );\n _diag(\n 'status.bundle-assets',\n data: {\n 'platformKey': platformKey,\n 'hasBundleAssets': hasBundleAssets,\n 'nodeAsset': 'assets/openclaw/$platformKey/${spec.nodeExecutable}',\n 'cliAsset': 'assets/openclaw/$platformKey/${spec.cliEntrypoint}',\n },\n );\n return _remember(\n OpenClawRuntimeStatus(\n state: hasBundleAssets\n ? OpenClawRuntimeState.stopped\n : OpenClawRuntimeState.notBundled,\n message: hasBundleAssets\n ? '内置 OpenClaw 已随 App 打包,尚未释放或启动'\n : '当前 App 包未包含 $platformKey 的 OpenClaw runtime',\n port: port,\n platformKey: platformKey,\n runtimePath: runtimeDir.path,\n checkedAt: DateTime.now(),\n ),\n );\n }\n\n if (probe) {\n final healthy = await _probe(\n port,\n await AppSettings.getOpenClawGatewayToken(),\n );\n if (healthy) {\n return _remember(\n OpenClawRuntimeStatus(\n state: OpenClawRuntimeState.running,\n message: '本机 OpenClaw Gateway 正在运行',\n port: port,\n platformKey: platformKey,\n runtimePath: runtimeDir.path,\n checkedAt: DateTime.now(),\n ),\n );\n }\n }\n\n return _remember(\n OpenClawRuntimeStatus(\n state: _process == null\n ? OpenClawRuntimeState.stopped\n : OpenClawRuntimeState.starting,\n message: _process == null ? '本机 OpenClaw 未启动' : '本机 OpenClaw 正在启动',\n port: port,\n platformKey: platformKey,\n runtimePath: runtimeDir.path,\n checkedAt: DateTime.now(),\n ),\n );\n } catch (error) {\n _diag('status.error', error: error);\n return _remember(\n OpenClawRuntimeStatus(\n state: OpenClawRuntimeState.failed,\n message: error.toString(),\n port: port,\n platformKey: platformKey,\n checkedAt: DateTime.now(),\n ),\n );\n }\n }\n\n Future<OpenClawGatewayTarget> ensureStarted({\n String? authToken,\n String? username,\n bool isMember = false,\n }) {\n _starting ??= _ensureStartedInternal(\n authToken: authToken,\n username: username,\n isMember: isMember,\n ).whenComplete(() => _starting = null);\n return _starting!;\n }\n\n Future<OpenClawRuntimeStatus> restart() async {\n await stop();\n try {\n await ensureStarted();\n return getStatus();\n } catch (error) {\n return _remember(\n OpenClawRuntimeStatus(\n state: OpenClawRuntimeState.failed,\n message: error.toString(),\n port: await AppSettings.getOpenClawGatewayPort(),\n platformKey: _platformKey,\n checkedAt: DateTime.now(),\n ),\n );\n }\n }\n\n Future<void> stop() async {\n final process = _process;\n _process = null;\n _processAuthToken = '';\n if (process == null) return;\n process.kill();\n try {\n await process.exitCode.timeout(const Duration(seconds: 5));\n } catch (_) {\n process.kill();\n }\n }\n\n Future<OpenClawGatewayTarget> _ensureStartedInternal({\n String? authToken,\n String? username,\n bool isMember = false,\n }) async {\n final platformKey = _platformKey;\n _diag('ensure-start.start', data: {'platformKey': platformKey});\n if (platformKey == null) {\n throw const OpenClawRuntimeException('当前平台不支持内置 OpenClaw Gateway');\n }\n\n final spec = await _loadSpec(platformKey, checkUpdates: true);\n final requestedPort = await AppSettings.getOpenClawGatewayPort(\n defaultValue: spec.defaultPort,\n );\n final token = await AppSettings.getOpenClawGatewayToken();\n final savedModel = await AppSettings.getOpenClawModel(\n defaultValue: spec.defaultModel,\n );\n final model = _gatewayChatModel(savedModel);\n final requestedAuthToken = authToken?.trim() ?? '';\n final backendDeepSeekModel = _backendDeepSeekModelRef(\n await AppSettings.getOpenClawDeepSeekModel(\n defaultValue: _defaultDeepSeekModelFor(spec.defaultModel),\n ),\n );\n final deepSeekProxyBaseUrl = _deepSeekProxyBaseUrl();\n final modelOverride = await AppSettings.getOpenClawModelOverride(\n defaultValue: spec.defaultModelOverride ?? '',\n );\n final desktopTools = await _ensureDesktopTools();\n _diag(\n 'ensure-start.config',\n data: {\n 'platformKey': platformKey,\n 'requestedPort': requestedPort,\n 'model': model,\n 'rawModel': savedModel,\n 'backendDeepSeekModel': backendDeepSeekModel,\n 'deepSeekProxyBaseUrl': deepSeekProxyBaseUrl,\n 'hasAuthToken': requestedAuthToken.isNotEmpty,\n 'modelOverrideSet': modelOverride.trim().isNotEmpty,\n 'desktopToolsUri': desktopTools.uri?.toString(),\n 'desktopToolsStatus': desktopTools.statusJson,\n },\n );\n\n final canReuseStartedProcess =\n _process != null && _processAuthToken == requestedAuthToken;\n if (canReuseStartedProcess &&\n await _probe(requestedPort, token, context: 'pre-start')) {\n _diag('ensure-start.reuse-existing', data: {'port': requestedPort});\n return OpenClawGatewayTarget(\n baseUri: Uri.parse('http://127.0.0.1:$requestedPort'),\n token: token,\n model: model,\n modelOverride: modelOverride.trim().isEmpty\n ? null\n : modelOverride.trim(),\n desktopToolsUri: desktopTools.uri,\n desktopToolsToken: desktopTools.token,\n desktopToolsStatus: desktopTools.statusJson,\n );\n } else if (_process != null) {\n _diag(\n 'ensure-start.restart-auth-changed',\n data: {\n 'hasPreviousAuthToken': _processAuthToken.isNotEmpty,\n 'hasRequestedAuthToken': requestedAuthToken.isNotEmpty,\n },\n );\n await stop();\n }\n\n final runtimeDir = await _prepareBundle(spec, platformKey);\n final stateRoot = await _stateRoot();\n if (_process != null) {\n _diag(\n 'process.stop-stale-before-start',\n data: {'requestedPort': requestedPort},\n );\n await stop();\n }\n final portCandidate = await _portResolver.resolve(requestedPort);\n final port = portCandidate.port;\n if (portCandidate.isFallback) {\n await AppSettings.setOpenClawGatewayPort(port);\n _diag(\n 'ensure-start.fallback-port',\n data: {\n 'requestedPort': requestedPort,\n 'selectedPort': port,\n if (portCandidate.reason != null) 'reason': portCandidate.reason,\n },\n );\n } else {\n _diag(\n 'ensure-start.port-bindable',\n data: {'requestedPort': requestedPort, 'selectedPort': port},\n );\n }\n final configPath = await _ensureConfigFile(\n stateRoot: stateRoot,\n port: port,\n token: token,\n backendDeepSeekModel: backendDeepSeekModel,\n deepSeekProxyBaseUrl: deepSeekProxyBaseUrl,\n );\n await _ensureAgentModelConfig(\n stateRoot: stateRoot,\n deepSeekProxyBaseUrl: deepSeekProxyBaseUrl,\n );\n final desktopToolsManifestPath = await _ensureDesktopToolsManifest(\n stateRoot: stateRoot,\n desktopTools: desktopTools,\n );\n\n final nodePath = p.join(runtimeDir.path, spec.nodeExecutable);\n final cliPath = p.join(runtimeDir.path, spec.cliEntrypoint);\n if (!await File(nodePath).exists()) {\n _diag('ensure-start.missing-node', data: {'nodePath': nodePath});\n throw OpenClawRuntimeException('OpenClaw 内置 Node 不存在: $nodePath');\n }\n if (!await File(cliPath).exists()) {\n _diag('ensure-start.missing-cli', data: {'cliPath': cliPath});\n throw OpenClawRuntimeException('OpenClaw CLI 入口不存在: $cliPath');\n }\n\n _remember(\n OpenClawRuntimeStatus(\n state: OpenClawRuntimeState.starting,\n message: '正在启动内置 OpenClaw Gateway',\n port: port,\n platformKey: platformKey,\n runtimePath: runtimeDir.path,\n checkedAt: DateTime.now(),\n ),\n );\n\n final args = <String>[\n cliPath,\n ...spec.gatewayArgs.map((arg) => arg.replaceAll('{port}', '$port')),\n ];\n _recentLogs.clear();\n _diag(\n 'process.starting',\n data: {\n 'nodePath': nodePath,\n 'args': args,\n 'workingDirectory': runtimeDir.path,\n 'configPath': configPath.path,\n 'desktopToolsManifestPath': desktopToolsManifestPath.path,\n 'requestedPort': requestedPort,\n 'selectedPort': port,\n },\n );\n\n final env = Map<String, String>.from(Platform.environment)\n ..addAll({\n 'OPENCLAW_GATEWAY_PORT': '$port',\n 'OPENCLAW_GATEWAY_BIND': 'loopback',\n 'OPENCLAW_GATEWAY_TOKEN': token,\n 'OPENCLAW_CONFIG_PATH': configPath.path,\n 'OPENCLAW_STATE_DIR': p.join(stateRoot.path, 'state'),\n 'OPENCLAW_AGENT_DIR': p.join(stateRoot.path, 'agents'),\n 'OPENCLAW_WORKSPACE': p.join(stateRoot.path, 'workspace'),\n 'DACHENG_DESKTOP_TOOLS_ENABLED': desktopTools.uri == null ? '0' : '1',\n 'DACHENG_DESKTOP_TOOLS_MANIFEST': desktopToolsManifestPath.path,\n if (desktopTools.uri != null)\n 'DACHENG_DESKTOP_TOOLS_URL': desktopTools.uri.toString(),\n if (desktopTools.token != null)\n 'DACHENG_DESKTOP_TOOLS_TOKEN': desktopTools.token!,\n 'DACHENG_APP_RUNTIME': '1',\n 'DACHENG_AUTH_TOKEN': (authToken != null && authToken.isNotEmpty)\n ? authToken\n : token,\n if (username != null && username.isNotEmpty)\n 'DACHENG_USERNAME': username,\n 'DACHENG_IS_MEMBER': isMember ? '1' : '0',\n 'DACHENG_OPENCLAW_PROXY_TOKEN': 'dacheng-openclaw-proxy',\n 'OPENCLAW_DISABLE_BONJOUR': '1',\n });\n\n _process = await Process.start(\n nodePath,\n args,\n workingDirectory: runtimeDir.path,\n environment: env,\n mode: ProcessStartMode.normal,\n runInShell: false,\n );\n _processAuthToken = requestedAuthToken;\n _captureLogs(_process!);\n _diag('process.started', data: {'pid': _process!.pid, 'port': port});\n\n final deadline = DateTime.now().add(_startupTimeout);\n while (DateTime.now().isBefore(deadline)) {\n if (await _probe(port, token, context: 'startup')) {\n _diag('process.ready', data: {'pid': _process?.pid, 'port': port});\n _remember(\n OpenClawRuntimeStatus(\n state: OpenClawRuntimeState.running,\n message: '本机 OpenClaw Gateway 已启动',\n port: port,\n platformKey: platformKey,\n runtimePath: runtimeDir.path,\n desktopToolsStatus: desktopTools.statusJson,\n checkedAt: DateTime.now(),\n ),\n );\n return OpenClawGatewayTarget(\n baseUri: Uri.parse('http://127.0.0.1:$port'),\n token: token,\n model: model,\n modelOverride: modelOverride.trim().isEmpty\n ? null\n : modelOverride.trim(),\n desktopToolsUri: desktopTools.uri,\n desktopToolsToken: desktopTools.token,\n desktopToolsStatus: desktopTools.statusJson,\n );\n }\n\n if (_process != null) {\n final exited = await _process!.exitCode.timeout(\n const Duration(milliseconds: 1),\n onTimeout: () => -999999,\n );\n if (exited != -999999) {\n final logs = _recentLogs.take(12).join('\\n');\n _diag(\n 'process.exited-early',\n data: {\n 'exitCode': exited,\n 'recentLogs': _recentLogs.take(12).toList(),\n },\n );\n throw OpenClawRuntimeException(\n 'OpenClaw Gateway 提前退出,exitCode=$exited${logs.isEmpty ? '' : '\\n$logs'}',\n );\n }\n }\n await Future<void>.delayed(const Duration(milliseconds: 450));\n }\n\n final logs = _recentLogs.take(12).join('\\n');\n _diag(\n 'process.startup-timeout',\n data: {\n 'timeoutSeconds': _startupTimeout.inSeconds,\n 'recentLogs': _recentLogs.take(12).toList(),\n },\n );\n throw OpenClawRuntimeException(\n 'OpenClaw Gateway 启动超时${logs.isEmpty ? '' : '\\n$logs'}',\n );\n }\n\n Future<_OpenClawBundleSpec> _loadSpec(\n String platformKey, {\n bool checkUpdates = false,\n }) async {\n final raw = await rootBundle.loadString(_manifestAsset);\n final decoded = jsonDecode(raw) as Map<String, dynamic>;\n final platforms = Map<String, dynamic>.from(\n decoded['platforms'] as Map? ?? const {},\n );\n final platform = Map<String, dynamic>.from(\n platforms[platformKey] as Map? ?? const {},\n );\n if (platform.isEmpty) {\n throw OpenClawRuntimeException('bundle_manifest.json 缺少 $platformKey 配置');\n }\n\n final gatewayArgs =\n (platform['gatewayArgs'] as List? ??\n decoded['gatewayArgs'] as List? ??\n const ['gateway', '--port', '{port}'])\n .map((item) => item.toString())\n .toList();\n\n final spec = _OpenClawBundleSpec(\n version: (decoded['version'] ?? 'dev').toString(),\n defaultPort: _readInt(decoded['defaultPort']) ?? 18789,\n defaultModel: (decoded['defaultModel'] ?? 'openclaw/default').toString(),\n defaultModelOverride: decoded['defaultModelOverride']?.toString(),\n nodeExecutable: (platform['nodeExecutable'] ?? 'node/bin/node')\n .toString(),\n cliEntrypoint: (platform['cliEntrypoint'] ?? 'openclaw/bin/openclaw.js')\n .toString(),\n gatewayArgs: gatewayArgs,\n );\n _diag(\n 'manifest.loaded',\n data: {\n 'platformKey': platformKey,\n 'version': spec.version,\n 'defaultPort': spec.defaultPort,\n 'defaultModel': spec.defaultModel,\n 'nodeExecutable': spec.nodeExecutable,\n 'cliEntrypoint': spec.cliEntrypoint,\n 'gatewayArgs': spec.gatewayArgs,\n },\n );\n var effectiveSpec =\n await _loadActiveDownloadedSpec(platformKey, fallbackSpec: spec) ??\n spec;\n if (checkUpdates) {\n effectiveSpec =\n await _maybeInstallRuntimeUpdate(\n platformKey: platformKey,\n currentSpec: effectiveSpec,\n ) ??\n effectiveSpec;\n }\n return effectiveSpec;\n }\n\n Future<_OpenClawBundleSpec?> _loadActiveDownloadedSpec(\n String platformKey, {\n required _OpenClawBundleSpec fallbackSpec,\n }) async {\n final saved = await AppSettings.getOpenClawActiveRuntimeSpec();\n if (saved == null) return null;\n final spec = _specFromJson(saved, downloaded: true);\n if (spec == null) {\n await AppSettings.clearOpenClawActiveRuntimeSpec();\n return null;\n }\n final runtimeDir = await _cachedRuntimeDirIfComplete(spec, platformKey);\n if (runtimeDir == null) {\n await AppSettings.clearOpenClawActiveRuntimeSpec();\n _diag(\n 'runtime-update.active-missing',\n data: {'platformKey': platformKey, 'version': spec.version},\n );\n return null;\n }\n _diag(\n 'runtime-update.active-loaded',\n data: {\n 'platformKey': platformKey,\n 'version': spec.version,\n 'runtimeDir': runtimeDir.path,\n 'bundledVersion': fallbackSpec.version,\n },\n );\n return spec;\n }\n\n _OpenClawBundleSpec? _specFromJson(\n Map<String, dynamic> json, {\n required bool downloaded,\n }) {\n final version = json['version']?.toString().trim() ?? '';\n final nodeExecutable = json['nodeExecutable']?.toString().trim() ?? '';\n final cliEntrypoint = json['cliEntrypoint']?.toString().trim() ?? '';\n if (version.isEmpty || nodeExecutable.isEmpty || cliEntrypoint.isEmpty) {\n return null;\n }\n return _OpenClawBundleSpec(\n version: version,\n defaultPort: _readInt(json['defaultPort']) ?? 18789,\n defaultModel: (json['defaultModel'] ?? _defaultGatewayModel).toString(),\n defaultModelOverride: json['defaultModelOverride']?.toString(),\n nodeExecutable: nodeExecutable,\n cliEntrypoint: cliEntrypoint,\n gatewayArgs:\n (json['gatewayArgs'] as List? ??\n const ['gateway', '--port', '{port}', '--force'])\n .map((item) => item.toString())\n .toList(),\n downloaded: downloaded,\n );\n }\n\n Future<_OpenClawBundleSpec?> _maybeInstallRuntimeUpdate({\n required String platformKey,\n required _OpenClawBundleSpec currentSpec,\n }) async {\n if (_configuredRuntimeDirOverride(platformKey) != null) {\n _diag('runtime-update.skip-override', data: {'platformKey': platformKey});\n return null;\n }\n\n try {\n final release = await _fetchRuntimeRelease(\n platformKey: platformKey,\n currentVersion: currentSpec.version,\n );\n if (release == null || release.version == currentSpec.version) {\n return null;\n }\n\n final releaseSpec = release.toBundleSpec();\n final cached = await _cachedRuntimeDirIfComplete(\n releaseSpec,\n platformKey,\n );\n if (cached != null) {\n await AppSettings.setOpenClawActiveRuntimeSpec(releaseSpec.toJson());\n _diag(\n 'runtime-update.use-cached',\n data: {\n 'platformKey': platformKey,\n 'version': release.version,\n 'runtimeDir': cached.path,\n },\n );\n return releaseSpec;\n }\n\n return await _downloadAndInstallRuntimeUpdate(\n release: release,\n platformKey: platformKey,\n );\n } catch (error, stackTrace) {\n _diag(\n 'runtime-update.failed',\n data: {\n 'platformKey': platformKey,\n 'currentVersion': currentSpec.version,\n 'error': error.toString(),\n },\n error: error,\n stackTrace: stackTrace,\n );\n return null;\n }\n }\n\n Future<_OpenClawRuntimeRelease?> _fetchRuntimeRelease({\n required String platformKey,\n required String currentVersion,\n }) async {\n final baseUrl = AppConfig.currentAiBackendUrl.replaceFirst(\n RegExp(r'/+$'),\n '',\n );\n final uri = Uri.parse('$baseUrl/api/openclaw/runtime/manifest').replace(\n queryParameters: {\n 'platform': platformKey,\n 'currentVersion': currentVersion,\n 'appVersion': AppConfig.appVersion,\n },\n );\n final response = await http\n .get(uri, headers: {'Accept': 'application/json'})\n .timeout(const Duration(seconds: 20));\n if (response.statusCode < 200 || response.statusCode >= 300) {\n _diag(\n 'runtime-update.manifest-http',\n data: {'statusCode': response.statusCode, 'uri': uri.toString()},\n );\n return null;\n }\n final decoded = _decodeJsonOrNull(response.body);\n if (decoded is! Map) return null;\n final payload = Map<String, dynamic>.from(decoded);\n if (payload['updateAvailable'] != true) {\n _diag(\n 'runtime-update.none',\n data: {\n 'platformKey': platformKey,\n 'reason': payload['reason']?.toString(),\n 'currentVersion': currentVersion,\n 'latestVersion': payload['latestVersion']?.toString(),\n },\n );\n return null;\n }\n final latest = payload['latest'];\n if (latest is! Map) return null;\n return _releaseFromJson(Map<String, dynamic>.from(latest));\n }\n\n _OpenClawRuntimeRelease? _releaseFromJson(Map<String, dynamic> json) {\n final version = json['version']?.toString().trim() ?? '';\n final archiveUrl = json['archiveUrl']?.toString().trim() ?? '';\n final sha256Hex = json['sha256']?.toString().trim().toLowerCase() ?? '';\n final archiveUri = Uri.tryParse(archiveUrl);\n final gatewayArgs =\n (json['gatewayArgs'] as List? ??\n const ['gateway', '--port', '{port}', '--force'])\n .map((item) => item.toString())\n .toList();\n if (version.isEmpty ||\n archiveUri == null ||\n !archiveUri.hasScheme ||\n !RegExp(r'^[a-f0-9]{64}$').hasMatch(sha256Hex)) {\n _diag(\n 'runtime-update.invalid-release',\n data: {\n 'version': version,\n 'archiveUrlSet': archiveUrl.isNotEmpty,\n 'sha256Length': sha256Hex.length,\n },\n );\n return null;\n }\n return _OpenClawRuntimeRelease(\n version: version,\n archiveUri: archiveUri,\n sha256Hex: sha256Hex,\n size: _readInt(json['size']) ?? 0,\n defaultPort: _readInt(json['defaultPort']) ?? 18789,\n defaultModel: (json['defaultModel'] ?? _defaultGatewayModel).toString(),\n defaultModelOverride: json['defaultModelOverride']?.toString(),\n nodeExecutable: (json['nodeExecutable'] ?? 'node/bin/node').toString(),\n cliEntrypoint: (json['cliEntrypoint'] ?? 'openclaw/openclaw.mjs')\n .toString(),\n gatewayArgs: gatewayArgs,\n );\n }\n\n Future<_OpenClawBundleSpec> _downloadAndInstallRuntimeUpdate({\n required _OpenClawRuntimeRelease release,\n required String platformKey,\n }) async {\n final spec = release.toBundleSpec();\n final runtimeDir = await _runtimeDir(spec, platformKey);\n final support = await getApplicationSupportDirectory();\n final downloadDir = Directory(p.join(support.path, 'openclaw_downloads'));\n await downloadDir.create(recursive: true);\n final archivePath = p.join(\n downloadDir.path,\n '${_safePathPart(release.version)}-$platformKey${_archiveExtension(release.archiveUri)}',\n );\n final archiveFile = File(archivePath);\n final digest = await _downloadRuntimeArchive(release, archiveFile);\n if (digest != release.sha256Hex) {\n await _deleteFileIfExists(archiveFile);\n throw OpenClawRuntimeException(\n 'OpenClaw runtime 校验失败: expected=${release.sha256Hex}, actual=$digest',\n );\n }\n\n final extractDir = Directory(\n p.join(\n downloadDir.path,\n '${_safePathPart(release.version)}-$platformKey-extract',\n ),\n );\n final stagingDir = Directory(\n p.join(\n downloadDir.path,\n '${_safePathPart(release.version)}-$platformKey-stage',\n ),\n );\n if (await extractDir.exists()) await extractDir.delete(recursive: true);\n if (await stagingDir.exists()) await stagingDir.delete(recursive: true);\n await extractDir.create(recursive: true);\n await _extractRuntimeArchive(archiveFile, extractDir);\n\n final extractedRoot = await _findExtractedRuntimeRoot(\n extractDir: extractDir,\n spec: spec,\n );\n if (extractedRoot == null) {\n throw OpenClawRuntimeException('下载的 OpenClaw runtime 不包含必要的 node/cli 文件');\n }\n\n await runtimeDir.parent.create(recursive: true);\n if (await runtimeDir.exists()) await runtimeDir.delete(recursive: true);\n await _moveOrCopyRuntimeDirectory(extractedRoot, stagingDir);\n await stagingDir.rename(runtimeDir.path);\n final marker = File(p.join(runtimeDir.path, '.bundle_ready'));\n await marker.writeAsString(\n const JsonEncoder.withIndent(' ').convert({\n 'installedAt': DateTime.now().toIso8601String(),\n 'version': release.version,\n 'platform': platformKey,\n 'sha256': digest,\n 'source': release.archiveUri.toString(),\n }),\n );\n await _repairRuntimeLaunchMetadata(\n runtimeDir: runtimeDir,\n nodePath: File(p.join(runtimeDir.path, spec.nodeExecutable)),\n platformKey: platformKey,\n cached: false,\n );\n await AppSettings.setOpenClawActiveRuntimeSpec(spec.toJson());\n await _deleteDirectoryIfExists(extractDir);\n await _deleteFileIfExists(archiveFile);\n _diag(\n 'runtime-update.installed',\n data: {\n 'platformKey': platformKey,\n 'version': release.version,\n 'runtimeDir': runtimeDir.path,\n 'size': release.size,\n },\n );\n return spec;\n }\n\n Future<String> _downloadRuntimeArchive(\n _OpenClawRuntimeRelease release,\n File destination,\n ) async {\n final client = http.Client();\n IOSink? output;\n try {\n final request = http.Request('GET', release.archiveUri)\n ..headers['Accept'] = 'application/octet-stream';\n final response = await client\n .send(request)\n .timeout(const Duration(seconds: 20));\n if (response.statusCode < 200 || response.statusCode >= 300) {\n throw OpenClawRuntimeException(\n 'OpenClaw runtime 下载失败 (${response.statusCode})',\n );\n }\n await destination.parent.create(recursive: true);\n output = destination.openWrite();\n final digestSink = AccumulatorSink<Digest>();\n final hashSink = sha256.startChunkedConversion(digestSink);\n var bytes = 0;\n await for (final chunk in response.stream.timeout(\n const Duration(minutes: 5),\n )) {\n bytes += chunk.length;\n hashSink.add(chunk);\n output.add(chunk);\n }\n await output.flush();\n await output.close();\n output = null;\n hashSink.close();\n final digest = digestSink.events.single.toString();\n _diag(\n 'runtime-update.downloaded',\n data: {\n 'archiveUri': release.archiveUri.toString(),\n 'bytes': bytes,\n 'sha256': digest,\n },\n );\n return digest;\n } finally {\n await output?.close().catchError((_) {});\n client.close();\n }\n }\n\n Future<void> _extractRuntimeArchive(\n File archiveFile,\n Directory destination,\n ) async {\n final lowerPath = archiveFile.path.toLowerCase();\n if (Platform.isMacOS && lowerPath.endsWith('.zip')) {\n try {\n final result = await Process.run('/usr/bin/ditto', [\n '-x',\n '-k',\n archiveFile.path,\n destination.path,\n ]).timeout(const Duration(minutes: 5));\n _diag(\n 'runtime-update.extract-native',\n data: {\n 'archivePath': archiveFile.path,\n 'destination': destination.path,\n 'exitCode': result.exitCode,\n if ((result.stderr as Object).toString().isNotEmpty)\n 'stderr': _previewProcessOutput(result.stderr),\n },\n );\n if (result.exitCode == 0) return;\n } catch (error, stackTrace) {\n _diag(\n 'runtime-update.extract-native-failed',\n data: {\n 'archivePath': archiveFile.path,\n 'destination': destination.path,\n 'error': error.toString(),\n },\n error: error,\n stackTrace: stackTrace,\n );\n }\n }\n\n await extractFileToDisk(archiveFile.path, destination.path);\n _diag(\n 'runtime-update.extract-dart',\n data: {'archivePath': archiveFile.path, 'destination': destination.path},\n );\n }\n\n Future<Directory?> _findExtractedRuntimeRoot({\n required Directory extractDir,\n required _OpenClawBundleSpec spec,\n }) async {\n final direct = await _runtimeRootIfComplete(extractDir, spec);\n if (direct != null) return direct;\n await for (final entity in extractDir.list(followLinks: false)) {\n if (entity is! Directory) continue;\n final nested = await _runtimeRootIfComplete(entity, spec);\n if (nested != null) return nested;\n }\n return null;\n }\n\n Future<Directory?> _runtimeRootIfComplete(\n Directory dir,\n _OpenClawBundleSpec spec,\n ) async {\n final nodePath = File(p.join(dir.path, spec.nodeExecutable));\n final cliPath = File(p.join(dir.path, spec.cliEntrypoint));\n if (await nodePath.exists() && await cliPath.exists()) return dir;\n return null;\n }\n\n Future<Directory?> _cachedRuntimeDirIfComplete(\n _OpenClawBundleSpec spec,\n String platformKey,\n ) async {\n final runtimeDir = await _runtimeDir(spec, platformKey);\n final marker = File(p.join(runtimeDir.path, '.bundle_ready'));\n final nodePath = File(p.join(runtimeDir.path, spec.nodeExecutable));\n final cliPath = File(p.join(runtimeDir.path, spec.cliEntrypoint));\n if (await marker.exists() &&\n await nodePath.exists() &&\n await cliPath.exists()) {\n return runtimeDir;\n }\n return null;\n }\n\n String _safePathPart(String value) {\n final safe = value.replaceAll(RegExp(r'[^A-Za-z0-9_.-]+'), '-');\n return safe.isEmpty ? 'runtime' : safe;\n }\n\n String _archiveExtension(Uri uri) {\n final lower = uri.path.toLowerCase();\n if (lower.endsWith('.tar.gz')) return '.tar.gz';\n if (lower.endsWith('.tgz')) return '.tgz';\n if (lower.endsWith('.tar')) return '.tar';\n return '.zip';\n }\n\n Future<void> _deleteFileIfExists(File file) async {\n try {\n if (await file.exists()) await file.delete();\n } catch (_) {}\n }\n\n Future<void> _deleteDirectoryIfExists(Directory dir) async {\n try {\n if (await dir.exists()) await dir.delete(recursive: true);\n } catch (_) {}\n }\n\n Future<Directory> _prepareBundle(\n _OpenClawBundleSpec spec,\n String platformKey,\n ) async {\n final overrideRuntimeDir = await _runtimeDirOverride(spec, platformKey);\n if (overrideRuntimeDir != null) {\n final nodePath = File(\n p.join(overrideRuntimeDir.path, spec.nodeExecutable),\n );\n await _repairRuntimeLaunchMetadata(\n runtimeDir: overrideRuntimeDir,\n nodePath: nodePath,\n platformKey: platformKey,\n cached: true,\n );\n _diag(\n 'bundle.launch.override-runtime',\n data: {\n 'platformKey': platformKey,\n 'runtimeDir': overrideRuntimeDir.path,\n },\n );\n return overrideRuntimeDir;\n }\n\n if (spec.downloaded) {\n final downloadedRuntimeDir = await _cachedRuntimeDirIfComplete(\n spec,\n platformKey,\n );\n if (downloadedRuntimeDir != null) {\n await _repairRuntimeLaunchMetadata(\n runtimeDir: downloadedRuntimeDir,\n nodePath: File(\n p.join(downloadedRuntimeDir.path, spec.nodeExecutable),\n ),\n platformKey: platformKey,\n cached: true,\n );\n _diag(\n 'bundle.launch.downloaded-runtime',\n data: {\n 'platformKey': platformKey,\n 'version': spec.version,\n 'runtimeDir': downloadedRuntimeDir.path,\n },\n );\n return downloadedRuntimeDir;\n }\n await AppSettings.clearOpenClawActiveRuntimeSpec();\n throw OpenClawRuntimeException(\n '已下载的 OpenClaw runtime 不完整,已回退到内置版本,请重新启动。',\n );\n }\n\n final bundledRuntimeDir = await _bundledRuntimeDir(spec, platformKey);\n if (bundledRuntimeDir != null) {\n _diag(\n 'bundle.launch.bundled-runtime',\n data: {\n 'platformKey': platformKey,\n 'runtimeDir': bundledRuntimeDir.path,\n },\n );\n return bundledRuntimeDir;\n }\n\n if (Platform.isMacOS) {\n throw OpenClawRuntimeException(\n '当前安装包没有可执行的 $platformKey OpenClaw runtime。请先运行 scripts/build_openclaw_desktop_bundle.sh $platformKey,再通过 flutter run 启动。',\n );\n }\n\n final runtimeDir = await _runtimeDir(spec, platformKey);\n final marker = File(p.join(runtimeDir.path, '.bundle_ready'));\n final nodePath = File(p.join(runtimeDir.path, spec.nodeExecutable));\n final cliPath = File(p.join(runtimeDir.path, spec.cliEntrypoint));\n\n if (await marker.exists() &&\n await nodePath.exists() &&\n await cliPath.exists()) {\n await _repairRuntimeLaunchMetadata(\n runtimeDir: runtimeDir,\n nodePath: nodePath,\n platformKey: platformKey,\n cached: true,\n );\n _diag(\n 'bundle.prepare.cached',\n data: {\n 'platformKey': platformKey,\n 'runtimeDir': runtimeDir.path,\n 'marker': marker.path,\n },\n );\n return runtimeDir;\n }\n\n final legacyRuntimeDir = await _legacyRuntimeDir(spec, platformKey);\n if (legacyRuntimeDir.path != runtimeDir.path &&\n await File(\n p.join(legacyRuntimeDir.path, spec.nodeExecutable),\n ).exists() &&\n await File(\n p.join(legacyRuntimeDir.path, spec.cliEntrypoint),\n ).exists()) {\n _diag(\n 'bundle.prepare.migrate-legacy',\n data: {\n 'platformKey': platformKey,\n 'from': legacyRuntimeDir.path,\n 'to': runtimeDir.path,\n },\n );\n if (await runtimeDir.exists()) {\n await runtimeDir.delete(recursive: true);\n }\n await _copyRuntimeDirectory(legacyRuntimeDir, runtimeDir);\n await _repairRuntimeLaunchMetadata(\n runtimeDir: runtimeDir,\n nodePath: nodePath,\n platformKey: platformKey,\n cached: false,\n );\n await marker.writeAsString(DateTime.now().toIso8601String());\n _diag(\n 'bundle.prepare.migrate-complete',\n data: {\n 'platformKey': platformKey,\n 'runtimeDir': runtimeDir.path,\n 'nodeExists': await nodePath.exists(),\n 'cliExists': await cliPath.exists(),\n },\n );\n return runtimeDir;\n }\n\n final prefix = 'assets/openclaw/$platformKey/';\n final assets = await _listAssets(prefix);\n final hasNodeAsset = assets.contains('$prefix${spec.nodeExecutable}');\n final hasCliAsset = assets.contains('$prefix${spec.cliEntrypoint}');\n _diag(\n 'bundle.prepare.assets',\n data: {\n 'platformKey': platformKey,\n 'prefix': prefix,\n 'assetCount': assets.length,\n 'hasNodeAsset': hasNodeAsset,\n 'hasCliAsset': hasCliAsset,\n 'nodeAsset': '$prefix${spec.nodeExecutable}',\n 'cliAsset': '$prefix${spec.cliEntrypoint}',\n 'sample': assets.take(8).toList(),\n },\n );\n if (!hasNodeAsset || !hasCliAsset) {\n throw OpenClawRuntimeException(\n '当前安装包没有内置 $platformKey 的 OpenClaw runtime。请在 release 构建中运行 scripts/build_openclaw_desktop_bundle.sh 后再打包。',\n );\n }\n\n if (await runtimeDir.exists()) {\n _diag('bundle.prepare.remove-old', data: {'runtimeDir': runtimeDir.path});\n await runtimeDir.delete(recursive: true);\n }\n await runtimeDir.create(recursive: true);\n\n for (final asset in assets) {\n final relative = asset.substring(prefix.length);\n if (relative.isEmpty || relative.endsWith('/')) continue;\n final bytes = await rootBundle.load(asset);\n final file = File(p.join(runtimeDir.path, relative));\n await file.parent.create(recursive: true);\n await file.writeAsBytes(\n bytes.buffer.asUint8List(bytes.offsetInBytes, bytes.lengthInBytes),\n flush: false,\n );\n }\n\n await _repairRuntimeLaunchMetadata(\n runtimeDir: runtimeDir,\n nodePath: nodePath,\n platformKey: platformKey,\n cached: false,\n );\n await marker.writeAsString(DateTime.now().toIso8601String());\n _diag(\n 'bundle.prepare.complete',\n data: {\n 'platformKey': platformKey,\n 'runtimeDir': runtimeDir.path,\n 'assetCount': assets.length,\n 'nodeExists': await nodePath.exists(),\n 'nodeSize': await nodePath.exists() ? await nodePath.length() : 0,\n 'cliExists': await cliPath.exists(),\n 'cliSize': await cliPath.exists() ? await cliPath.length() : 0,\n },\n );\n return runtimeDir;\n }\n\n Future<void> _copyRuntimeDirectory(\n Directory source,\n Directory destination,\n ) async {\n await destination.create(recursive: true);\n await for (final entity in source.list(\n recursive: true,\n followLinks: false,\n )) {\n final relative = p.relative(entity.path, from: source.path);\n if (relative == '.') continue;\n final target = p.join(destination.path, relative);\n if (entity is Directory) {\n await Directory(target).create(recursive: true);\n } else if (entity is File) {\n final targetFile = File(target);\n await targetFile.parent.create(recursive: true);\n await entity.openRead().pipe(targetFile.openWrite());\n }\n }\n }\n\n Future<void> _moveOrCopyRuntimeDirectory(\n Directory source,\n Directory destination,\n ) async {\n if (await destination.exists()) {\n await destination.delete(recursive: true);\n }\n await destination.parent.create(recursive: true);\n try {\n await source.rename(destination.path);\n return;\n } on FileSystemException {\n await _copyRuntimeDirectory(source, destination);\n }\n }\n\n Future<void> _repairRuntimeLaunchMetadata({\n required Directory runtimeDir,\n required File nodePath,\n required String platformKey,\n required bool cached,\n }) async {\n if (Platform.isWindows) return;\n\n final chmodResult = await Process.run('/bin/chmod', ['+x', nodePath.path]);\n ProcessResult? xattrResult;\n if (Platform.isMacOS) {\n // Files created by a quarantined app can inherit the quarantine xattr.\n // macOS then rejects Process.start with \"Operation not permitted\".\n xattrResult = await Process.run('/usr/bin/xattr', [\n '-dr',\n 'com.apple.quarantine',\n runtimeDir.path,\n ]);\n }\n\n _diag(\n 'bundle.prepare.launch-metadata',\n data: {\n 'platformKey': platformKey,\n 'runtimeDir': runtimeDir.path,\n 'nodePath': nodePath.path,\n 'cached': cached,\n 'chmodExitCode': chmodResult.exitCode,\n if ((chmodResult.stderr as Object).toString().isNotEmpty)\n 'chmodStderr': _previewProcessOutput(chmodResult.stderr),\n if (xattrResult != null) 'xattrExitCode': xattrResult.exitCode,\n if (xattrResult != null &&\n (xattrResult.stderr as Object).toString().isNotEmpty)\n 'xattrStderr': _previewProcessOutput(xattrResult.stderr),\n },\n );\n }\n\n Future<bool> _hasRequiredBundleAssets(\n String platformKey,\n _OpenClawBundleSpec spec,\n ) async {\n final prefix = 'assets/openclaw/$platformKey/';\n final assets = await _listAssets(prefix);\n final hasRequired =\n assets.contains('$prefix${spec.nodeExecutable}') &&\n assets.contains('$prefix${spec.cliEntrypoint}');\n _diag(\n 'bundle.has-required-assets',\n data: {\n 'platformKey': platformKey,\n 'assetCount': assets.length,\n 'hasRequired': hasRequired,\n },\n );\n return hasRequired;\n }\n\n Future<List<String>> _listAssets(String prefix) async {\n final assets = <String>{};\n var jsonCount = 0;\n var binCount = 0;\n var indexCount = 0;\n final failures = <String>[];\n\n try {\n final raw = await rootBundle.loadString('AssetManifest.json');\n final decoded = jsonDecode(raw) as Map<String, dynamic>;\n final items = decoded.keys\n .where((key) => key.startsWith(prefix))\n .toList();\n jsonCount = items.length;\n assets.addAll(items);\n } catch (error) {\n failures.add('AssetManifest.json: $error');\n // Newer Flutter release builds may only include AssetManifest.bin.\n }\n\n try {\n final manifest = await AssetManifest.loadFromAssetBundle(rootBundle);\n final items = manifest\n .listAssets()\n .where((key) => key.startsWith(prefix))\n .toList();\n binCount = items.length;\n assets.addAll(items);\n } catch (error) {\n failures.add('AssetManifest.bin: $error');\n // App Store archives patched after flutter build carry a JSON/index file.\n }\n\n try {\n final raw = await rootBundle.loadString(\n 'assets/openclaw/asset_index.json',\n );\n final decoded = jsonDecode(raw);\n final indexedAssets = decoded is Map\n ? decoded['assets']\n : decoded is List\n ? decoded\n : const [];\n if (indexedAssets is List) {\n final items = indexedAssets\n .map((item) => item.toString())\n .where((key) => key.startsWith(prefix))\n .toList();\n indexCount = items.length;\n assets.addAll(items);\n }\n } catch (error) {\n failures.add('asset_index.json: $error');\n // Older builds do not have an OpenClaw asset index.\n }\n\n final sortedAssets = assets.toList()..sort();\n _diag(\n 'asset.list',\n data: {\n 'prefix': prefix,\n 'total': sortedAssets.length,\n 'jsonCount': jsonCount,\n 'binCount': binCount,\n 'indexCount': indexCount,\n if (failures.isNotEmpty) 'failures': failures,\n 'sample': sortedAssets.take(8).toList(),\n },\n );\n return sortedAssets;\n }\n\n Future<bool> _probe(\n int port,\n String token, {\n String context = 'probe',\n }) async {\n try {\n final healthResponse = await http\n .get(\n Uri.parse('http://127.0.0.1:$port/health'),\n headers: {'Authorization': 'Bearer $token'},\n )\n .timeout(_probeTimeout);\n final healthPayload = _decodeJsonOrNull(healthResponse.body);\n if (healthResponse.statusCode < 200 ||\n healthResponse.statusCode >= 300 ||\n !_looksLikeHealthPayload(healthPayload)) {\n _diag(\n 'probe.health-unhealthy',\n data: {\n 'context': context,\n 'port': port,\n 'statusCode': healthResponse.statusCode,\n 'contentType': healthResponse.headers['content-type'],\n 'bodyPreview': _previewBody(healthResponse.body),\n },\n );\n return false;\n }\n\n final modelsResponse = await http\n .get(\n Uri.parse('http://127.0.0.1:$port/v1/models'),\n headers: {'Authorization': 'Bearer $token'},\n )\n .timeout(_probeTimeout);\n final modelsPayload = _decodeJsonOrNull(modelsResponse.body);\n if (modelsResponse.statusCode < 200 ||\n modelsResponse.statusCode >= 300 ||\n !_looksLikeModelsPayload(modelsPayload)) {\n _diag(\n 'probe.models-unhealthy',\n data: {\n 'context': context,\n 'port': port,\n 'statusCode': modelsResponse.statusCode,\n 'contentType': modelsResponse.headers['content-type'],\n 'bodyPreview': _previewBody(modelsResponse.body),\n },\n );\n return false;\n }\n\n // `/v1/models` can be healthy while `/v1/chat/completions` is disabled\n // by an old config. Use GET so the probe only checks route wiring and\n // never starts a real model request.\n final chatResponse = await http\n .get(\n Uri.parse('http://127.0.0.1:$port/v1/chat/completions'),\n headers: {'Authorization': 'Bearer $token'},\n )\n .timeout(_probeTimeout);\n final healthy =\n chatResponse.statusCode != 401 &&\n chatResponse.statusCode != 403 &&\n chatResponse.statusCode != 404;\n _diag(\n healthy ? 'probe.healthy' : 'probe.chat-unhealthy',\n data: {\n 'context': context,\n 'port': port,\n 'chatStatusCode': chatResponse.statusCode,\n },\n );\n return healthy;\n } catch (error) {\n if (context != 'startup') {\n _diag(\n 'probe.error',\n data: {'context': context, 'port': port},\n error: error,\n );\n }\n return false;\n }\n }\n\n Future<File> _ensureConfigFile({\n required Directory stateRoot,\n required int port,\n required String token,\n required String backendDeepSeekModel,\n required String deepSeekProxyBaseUrl,\n }) async {\n final configDir = Directory(p.join(stateRoot.path, 'config'));\n await configDir.create(recursive: true);\n final workspace = Directory(p.join(stateRoot.path, 'workspace'));\n await workspace.create(recursive: true);\n final configPath = File(p.join(configDir.path, 'openclaw.json'));\n\n final config = <String, dynamic>{\n 'gateway': {\n 'mode': 'local',\n 'port': port,\n 'bind': 'loopback',\n 'auth': {'mode': 'token', 'token': token},\n 'http': {\n 'endpoints': {\n 'chatCompletions': {'enabled': true},\n 'responses': {'enabled': true},\n },\n },\n },\n 'models': {\n 'mode': 'merge',\n 'providers': {\n _backendDeepSeekProviderId: {\n 'baseUrl': deepSeekProxyBaseUrl,\n 'api': 'openai-completions',\n 'apiKey': 'DACHENG_OPENCLAW_PROXY_TOKEN',\n 'authHeader': true,\n 'headers': {'x-dacheng-auth-token': 'DACHENG_AUTH_TOKEN'},\n 'models': [\n {\n 'id': 'deepseek-chat',\n 'name': 'DeepSeek Chat',\n 'contextWindow': 131072,\n 'maxTokens': 8192,\n 'input': ['text'],\n 'compat': {\n 'requiresStringContent': true,\n 'strictMessageKeys': true,\n },\n },\n {\n 'id': 'deepseek-reasoner',\n 'name': 'DeepSeek Reasoner',\n 'contextWindow': 131072,\n 'maxTokens': 8192,\n 'reasoning': true,\n 'input': ['text'],\n 'compat': {\n 'requiresStringContent': true,\n 'strictMessageKeys': true,\n },\n },\n ],\n },\n },\n },\n 'plugins': {\n 'enabled': false,\n 'allow': <String>[],\n 'deny': <String>[],\n 'load': {'paths': <String>[]},\n 'slots': {'memory': 'none'},\n 'entries': <String, dynamic>{\n 'memory-core': {'enabled': false},\n 'bonjour': {'enabled': false},\n 'browser': {'enabled': false},\n 'canvas': {'enabled': false},\n 'device-pair': {'enabled': false},\n 'file-transfer': {'enabled': false},\n 'phone-control': {'enabled': false},\n 'talk-voice': {'enabled': false},\n },\n },\n 'agents': {\n 'defaults': {\n 'workspace': workspace.path,\n 'model': {'primary': backendDeepSeekModel},\n },\n 'list': [\n {\n 'id': 'dacheng',\n 'default': true,\n 'workspace': workspace.path,\n 'model': {'primary': backendDeepSeekModel},\n },\n ],\n },\n };\n\n // Repair older embedded configs in-place. OpenClaw 2026.6 requires\n // gateway.mode=local, and the home screen requires the OpenAI-compatible\n // chat endpoint. Preserve unrelated user edits while fixing the fields\n // needed for the app-owned embedded gateway to start reliably.\n final merged = await _mergeEmbeddedConfig(configPath, config);\n await configPath.writeAsString(\n const JsonEncoder.withIndent(' ').convert(merged),\n );\n _diag(\n 'config.written',\n data: {\n 'configPath': configPath.path,\n 'port': port,\n 'gatewayMode': _mutableMap(merged['gateway'])['mode'],\n 'backendDeepSeekModel': backendDeepSeekModel,\n 'deepSeekProxyBaseUrl': deepSeekProxyBaseUrl,\n },\n );\n return configPath;\n }\n\n Future<File> _ensureDesktopToolsManifest({\n required Directory stateRoot,\n required _DesktopToolsLaunch desktopTools,\n }) async {\n final configDir = Directory(p.join(stateRoot.path, 'config'));\n await configDir.create(recursive: true);\n final manifestPath = File(p.join(configDir.path, 'desktop_tools.json'));\n final tools =\n DesktopControlPolicy.supportedTools\n .map(\n (name) => {\n 'name': name,\n 'endpoint': '/v1/tools/execute',\n 'readOnly': DesktopControlPolicy.isReadOnly(name),\n 'requiresConfirmation':\n DesktopControlPolicy.requiresConfirmation(name),\n },\n )\n .toList()\n ..sort(\n (a, b) => a['name'].toString().compareTo(b['name'].toString()),\n );\n\n await manifestPath.writeAsString(\n const JsonEncoder.withIndent(' ').convert({\n 'version': 1,\n 'transport': 'http-loopback',\n 'baseUrl': desktopTools.uri?.toString(),\n 'auth': {'type': 'bearer', 'env': 'DACHENG_DESKTOP_TOOLS_TOKEN'},\n 'status': desktopTools.statusJson,\n 'tools': tools,\n }),\n );\n _diag(\n 'desktop-tools.manifest-written',\n data: {\n 'manifestPath': manifestPath.path,\n 'hasBaseUrl': desktopTools.uri != null,\n },\n );\n return manifestPath;\n }\n\n Future<File> _ensureAgentModelConfig({\n required Directory stateRoot,\n required String deepSeekProxyBaseUrl,\n }) async {\n final agentDir = Directory(\n p.join(stateRoot.path, 'state', 'agents', 'dacheng', 'agent'),\n );\n await agentDir.create(recursive: true);\n final modelsPath = File(p.join(agentDir.path, 'models.json'));\n final providerConfig = <String, dynamic>{\n 'baseUrl': deepSeekProxyBaseUrl,\n 'api': 'openai-completions',\n 'apiKey': 'DACHENG_OPENCLAW_PROXY_TOKEN',\n 'authHeader': true,\n 'headers': {'x-dacheng-auth-token': 'DACHENG_AUTH_TOKEN'},\n 'models': [\n {\n 'id': 'deepseek-chat',\n 'name': 'DeepSeek Chat',\n 'contextWindow': 131072,\n 'maxTokens': 8192,\n 'input': ['text'],\n 'compat': {'requiresStringContent': true, 'strictMessageKeys': true},\n },\n {\n 'id': 'deepseek-reasoner',\n 'name': 'DeepSeek Reasoner',\n 'contextWindow': 131072,\n 'maxTokens': 8192,\n 'reasoning': true,\n 'input': ['text'],\n 'compat': {'requiresStringContent': true, 'strictMessageKeys': true},\n },\n ],\n };\n await modelsPath.writeAsString(\n const JsonEncoder.withIndent(' ').convert({\n 'providers': {_backendDeepSeekProviderId: providerConfig},\n }),\n );\n _diag(\n 'agent-models.written',\n data: {\n 'modelsPath': modelsPath.path,\n 'provider': _backendDeepSeekProviderId,\n 'deepSeekProxyBaseUrl': deepSeekProxyBaseUrl,\n },\n );\n return modelsPath;\n }\n\n Future<Map<String, dynamic>> _mergeEmbeddedConfig(\n File configPath,\n Map<String, dynamic> defaults,\n ) async {\n Map<String, dynamic> current = <String, dynamic>{};\n if (await configPath.exists()) {\n try {\n final decoded = jsonDecode(await configPath.readAsString());\n if (decoded is Map<String, dynamic>) {\n current = decoded;\n } else if (decoded is Map) {\n current = Map<String, dynamic>.from(decoded);\n }\n } catch (_) {\n current = <String, dynamic>{};\n }\n }\n\n final gateway = _mutableMap(current['gateway']);\n final defaultGateway = _mutableMap(defaults['gateway']);\n gateway['mode'] = 'local';\n gateway['port'] = defaultGateway['port'];\n gateway['bind'] = defaultGateway['bind'];\n gateway['auth'] = Map<String, dynamic>.from(defaultGateway['auth'] as Map);\n\n final gatewayHttp = _mutableMap(gateway['http']);\n final endpoints = _mutableMap(gatewayHttp['endpoints']);\n endpoints['chatCompletions'] = {'enabled': true};\n endpoints['responses'] = {'enabled': true};\n gatewayHttp['endpoints'] = endpoints;\n gateway['http'] = gatewayHttp;\n current['gateway'] = gateway;\n\n final env = _mutableMap(current['env']);\n env.remove('DEEPSEEK_API_KEY');\n if (env.isEmpty) {\n current.remove('env');\n } else {\n current['env'] = env;\n }\n\n final defaultModels = _mutableMap(defaults['models']);\n final models = _mutableMap(current['models']);\n models['mode'] ??= defaultModels['mode'];\n final providers = _mutableMap(models['providers']);\n final defaultProviders = _mutableMap(defaultModels['providers']);\n providers.remove('deepseek');\n providers.remove('dacheng-deepseek');\n providers.remove(_backendDeepSeekProviderId);\n for (final entry in defaultProviders.entries) {\n providers[entry.key] = entry.value is Map\n ? Map<String, dynamic>.from(entry.value as Map)\n : entry.value;\n }\n models['providers'] = providers;\n current['models'] = models;\n\n final defaultPlugins = _mutableMap(defaults['plugins']);\n final plugins = _mutableMap(current['plugins']);\n plugins['enabled'] = false;\n plugins['allow'] = <String>[];\n plugins['deny'] = <String>[];\n plugins['load'] = Map<String, dynamic>.from(\n _mutableMap(defaultPlugins['load']),\n );\n plugins['slots'] = Map<String, dynamic>.from(\n _mutableMap(defaultPlugins['slots']),\n );\n final entries = _mutableMap(plugins['entries']);\n final defaultEntries = _mutableMap(defaultPlugins['entries']);\n for (final entry in defaultEntries.entries) {\n entries[entry.key] = entry.value is Map\n ? Map<String, dynamic>.from(entry.value as Map)\n : entry.value;\n }\n plugins['entries'] = entries;\n current['plugins'] = plugins;\n\n final agents = _mutableMap(current['agents']);\n final defaultAgents = _mutableMap(defaults['agents']);\n final defaultAgentDefaults = _mutableMap(defaultAgents['defaults']);\n final agentDefaults = _mutableMap(agents['defaults']);\n agentDefaults['workspace'] = defaultAgentDefaults['workspace'];\n agentDefaults['model'] = defaultAgentDefaults['model'];\n agents['defaults'] = agentDefaults;\n\n final defaultList = (defaultAgents['list'] as List? ?? const [])\n .whereType<Map>()\n .map((item) => Map<String, dynamic>.from(item))\n .toList();\n final defaultDacheng = defaultList.isNotEmpty\n ? defaultList.first\n : <String, dynamic>{\n 'id': 'dacheng',\n 'default': true,\n 'workspace': agentDefaults['workspace'],\n 'model': agentDefaults['model'],\n };\n final list = (agents['list'] as List? ?? const [])\n .map((item) => item is Map ? Map<String, dynamic>.from(item) : item)\n .toList();\n var foundDacheng = false;\n for (var i = 0; i < list.length; i++) {\n final item = list[i];\n if (item is! Map<String, dynamic>) continue;\n if (item['id'] != 'dacheng') continue;\n foundDacheng = true;\n item['default'] = true;\n item['workspace'] = defaultDacheng['workspace'];\n item['model'] = defaultDacheng['model'];\n list[i] = item;\n }\n if (!foundDacheng) {\n list.add(defaultDacheng);\n }\n agents['list'] = list.isEmpty ? defaultList : list;\n current['agents'] = agents;\n\n return current;\n }\n\n String _gatewayChatModel(String model) {\n final trimmed = model.trim();\n if (trimmed == 'openclaw' || trimmed.startsWith('openclaw/')) {\n return trimmed;\n }\n return _defaultGatewayModel;\n }\n\n String _defaultDeepSeekModelFor(String model) {\n final trimmed = model.trim();\n if (trimmed.startsWith('deepseek/')) return trimmed;\n return _defaultDeepSeekModel;\n }\n\n String _backendDeepSeekModelRef(String model) {\n final trimmed = model.trim().isEmpty ? _defaultDeepSeekModel : model.trim();\n final modelId = trimmed.contains('/')\n ? trimmed.split('/').last.trim()\n : trimmed;\n final normalizedModelId = modelId.isEmpty ? 'deepseek-chat' : modelId;\n return '$_backendDeepSeekProviderId/$normalizedModelId';\n }\n\n String _deepSeekProxyBaseUrl() {\n final baseUrl = AppConfig.currentAiBackendUrl.replaceFirst(\n RegExp(r'/+$'),\n '',\n );\n return '$baseUrl/api/openclaw/deepseek/v1';\n }\n\n Map<String, dynamic> _mutableMap(Object? value) {\n if (value is Map<String, dynamic>) return Map<String, dynamic>.from(value);\n if (value is Map) return Map<String, dynamic>.from(value);\n return <String, dynamic>{};\n }\n\n void _captureLogs(Process process) {\n void addLog(String line) {\n final text = line.trim();\n if (text.isEmpty) return;\n _recentLogs.insert(0, text);\n if (_recentLogs.length > 80) {\n _recentLogs.removeRange(80, _recentLogs.length);\n }\n if (kDebugMode) {\n debugPrint('[OpenClaw] $text');\n }\n _diag('process.output', data: {'line': text});\n }\n\n process.stdout\n .transform(utf8.decoder)\n .transform(const LineSplitter())\n .listen(addLog, onError: (_) {});\n process.stderr\n .transform(utf8.decoder)\n .transform(const LineSplitter())\n .listen(addLog, onError: (_) {});\n }\n\n Future<Directory> _stateRoot() async {\n final support = await getApplicationSupportDirectory();\n final dir = Directory(p.join(support.path, 'openclaw_embedded'));\n await dir.create(recursive: true);\n return dir;\n }\n\n Future<_DesktopToolsLaunch> _ensureDesktopTools() async {\n try {\n final status = await DesktopControlBridge.instance.ensureStarted();\n _diag(\n 'desktop-tools.started',\n data: {\n 'bridgeRunning': status.bridgeRunning,\n 'bridgeUri': status.bridgeUri?.toString(),\n 'message': status.message,\n },\n );\n return _DesktopToolsLaunch(\n uri: status.bridgeUri,\n token: await DesktopControlBridge.instance.bridgeToken,\n statusJson: status.toJson(),\n );\n } catch (error) {\n _diag('desktop-tools.error', error: error);\n return _DesktopToolsLaunch(\n uri: null,\n token: null,\n statusJson: {\n 'enabledByBuild': false,\n 'bridgeRunning': false,\n 'message': error.toString(),\n },\n );\n }\n }\n\n Future<Directory> _runtimeDir(\n _OpenClawBundleSpec spec,\n String platformKey,\n ) async {\n final support = await getApplicationSupportDirectory();\n final runtimeBase = Platform.isMacOS\n ? Directory(\n p.join(\n support.parent.path,\n '${p.basename(support.path)}.openclaw_runtime',\n ),\n )\n : Directory(p.join(support.path, 'openclaw_embedded'));\n return Directory(\n p.join(runtimeBase.path, 'runtime', spec.version, platformKey),\n );\n }\n\n Future<Directory?> _bundledRuntimeDir(\n _OpenClawBundleSpec spec,\n String platformKey,\n ) async {\n if (!Platform.isMacOS) return null;\n\n final contentsDir = Directory(\n p.dirname(p.dirname(Platform.resolvedExecutable)),\n );\n final candidates = <Directory>[\n Directory(\n p.join(\n contentsDir.path,\n 'Frameworks',\n 'App.framework',\n 'Resources',\n 'flutter_assets',\n 'assets',\n 'openclaw',\n platformKey,\n ),\n ),\n Directory(\n p.join(\n contentsDir.path,\n 'Frameworks',\n 'App.framework',\n 'Versions',\n 'A',\n 'Resources',\n 'flutter_assets',\n 'assets',\n 'openclaw',\n platformKey,\n ),\n ),\n Directory(\n p.join(\n contentsDir.path,\n 'Resources',\n 'flutter_assets',\n 'assets',\n 'openclaw',\n platformKey,\n ),\n ),\n ];\n\n for (final dir in candidates) {\n final nodePath = File(p.join(dir.path, spec.nodeExecutable));\n final cliPath = File(p.join(dir.path, spec.cliEntrypoint));\n if (await nodePath.exists() && await cliPath.exists()) {\n return dir;\n }\n }\n _diag(\n 'bundle.macos-bundled-runtime-missing',\n data: {\n 'platformKey': platformKey,\n 'contentsDir': contentsDir.path,\n 'candidates': candidates.map((item) => item.path).toList(),\n },\n );\n return null;\n }\n\n Future<Directory?> _runtimeDirOverride(\n _OpenClawBundleSpec spec,\n String platformKey,\n ) async {\n final raw = _configuredRuntimeDirOverride(platformKey);\n if (raw == null) return null;\n\n final dir = Directory(raw);\n final nodePath = File(p.join(dir.path, spec.nodeExecutable));\n final cliPath = File(p.join(dir.path, spec.cliEntrypoint));\n final nodeExists = await nodePath.exists();\n final cliExists = await cliPath.exists();\n _diag(\n 'bundle.override.paths',\n data: {\n 'platformKey': platformKey,\n 'runtimeDir': dir.path,\n 'nodePath': nodePath.path,\n 'nodeExists': nodeExists,\n 'cliPath': cliPath.path,\n 'cliExists': cliExists,\n },\n );\n\n if (!nodeExists || !cliExists) {\n throw OpenClawRuntimeException(\n 'DACHENG_OPENCLAW_RUNTIME_DIR 指向的 OpenClaw runtime 不完整: ${dir.path}',\n );\n }\n return dir;\n }\n\n String? _configuredRuntimeDirOverride(String platformKey) {\n final raw = _runtimeDirOverrideDefine.trim().isNotEmpty\n ? _runtimeDirOverrideDefine.trim()\n : Platform.environment['DACHENG_OPENCLAW_RUNTIME_DIR']?.trim();\n if (raw == null || raw.isEmpty) return null;\n return p.normalize(raw.replaceAll('{platform}', platformKey));\n }\n\n Future<Directory> _legacyRuntimeDir(\n _OpenClawBundleSpec spec,\n String platformKey,\n ) async {\n final support = await getApplicationSupportDirectory();\n return Directory(\n p.join(\n support.path,\n 'openclaw_embedded',\n 'runtime',\n spec.version,\n platformKey,\n ),\n );\n }\n\n OpenClawRuntimeStatus _remember(OpenClawRuntimeStatus status) {\n _lastStatus = status;\n return status;\n }\n\n String? get _platformKey {\n final abi = Abi.current().toString().toLowerCase();\n if (Platform.isMacOS) {\n return abi.contains('arm64') ? 'macos-arm64' : 'macos-x64';\n }\n if (Platform.isWindows) {\n return abi.contains('arm64') ? 'windows-arm64' : 'windows-x64';\n }\n if (Platform.isLinux) {\n return abi.contains('arm64') ? 'linux-arm64' : 'linux-x64';\n }\n return null;\n }\n\n OpenClawRuntimeStatus? get lastStatus => _lastStatus;\n\n void _diag(\n String message, {\n Map<String, Object?> data = const {},\n Object? error,\n StackTrace? stackTrace,\n }) {\n unawaited(\n DiagnosticLogService.instance.log(\n 'openclaw.runtime',\n message,\n data: data,\n error: error,\n stackTrace: stackTrace,\n ),\n );\n }\n}\n\nint? _readInt(Object? value) {\n if (value is int) return value;\n if (value is num) return value.toInt();\n if (value is String) return int.tryParse(value);\n return null;\n}\n\nObject? _decodeJsonOrNull(String body) {\n try {\n return jsonDecode(body);\n } catch (_) {\n return null;\n }\n}\n\nbool _looksLikeHealthPayload(Object? payload) {\n if (payload is! Map) return false;\n if (payload['ok'] == true) return true;\n final status = payload['status']?.toString().toLowerCase();\n return status == 'live' || status == 'ok' || status == 'healthy';\n}\n\nbool _looksLikeModelsPayload(Object? payload) {\n if (payload is List) return true;\n if (payload is! Map) return false;\n return payload['data'] is List ||\n payload['models'] is List ||\n payload['object'] == 'list';\n}\n\nString _previewBody(String body) {\n final trimmed = body.trim();\n return trimmed.length <= 80 ? trimmed : trimmed.substring(0, 80);\n}\n\nString _previewProcessOutput(Object? output) {\n final text = output?.toString().trim() ?? '';\n return text.length <= 200 ? text : text.substring(0, 200);\n}\n"}, "5c7e77b41a34b89959cff6bc82ac16f657947e3d": {"t": 1785757965.4220808, "n": 35942, "h": "9a8840ac277259fe", "c": "import 'dart:async';\nimport 'dart:convert';\nimport 'dart:ffi' show Abi;\nimport 'dart:io';\nimport 'package:flutter/foundation.dart';\nimport 'package:flutter/services.dart';\nimport 'package:http/http.dart' as http;\nimport 'package:path/path.dart' as p;\nimport 'package:path_provider/path_provider.dart';\n\nimport '../../core/config/app_config.dart';\nimport '../app_settings.dart';\nimport '../diagnostic_log_service.dart';\nimport '../desktop_control/desktop_control_bridge.dart';\nimport '../desktop_control/desktop_control_policy.dart';\n\nenum OpenClawRuntimeState {\n unsupported,\n notBundled,\n stopped,\n starting,\n running,\n failed,\n}\n\nclass OpenClawRuntimeStatus {\n final OpenClawRuntimeState state;\n final String message;\n final int? port;\n final String? platformKey;\n final String? runtimePath;\n final Map<String, dynamic>? desktopToolsStatus;\n final DateTime checkedAt;\n\n const OpenClawRuntimeStatus({\n required this.state,\n required this.message,\n this.port,\n this.platformKey,\n this.runtimePath,\n this.desktopToolsStatus,\n required this.checkedAt,\n });\n\n bool get isHealthy => state == OpenClawRuntimeState.running;\n\n String get label {\n switch (state) {\n case OpenClawRuntimeState.unsupported:\n return '不支持';\n case OpenClawRuntimeState.notBundled:\n return '未内置';\n case OpenClawRuntimeState.stopped:\n return '未启动';\n case OpenClawRuntimeState.starting:\n return '启动中';\n case OpenClawRuntimeState.running:\n return '运行中';\n case OpenClawRuntimeState.failed:\n return '异常';\n }\n }\n}\n\nclass OpenClawGatewayTarget {\n final Uri baseUri;\n final String token;\n final String model;\n final String? modelOverride;\n final Uri? desktopToolsUri;\n final String? desktopToolsToken;\n final Map<String, dynamic>? desktopToolsStatus;\n\n const OpenClawGatewayTarget({\n required this.baseUri,\n required this.token,\n required this.model,\n this.modelOverride,\n this.desktopToolsUri,\n this.desktopToolsToken,\n this.desktopToolsStatus,\n });\n}\n\nclass _OpenClawBundleSpec {\n final String version;\n final int defaultPort;\n final String defaultModel;\n final String? defaultModelOverride;\n final String nodeExecutable;\n final String cliEntrypoint;\n final List<String> gatewayArgs;\n\n const _OpenClawBundleSpec({\n required this.version,\n required this.defaultPort,\n required this.defaultModel,\n this.defaultModelOverride,\n required this.nodeExecutable,\n required this.cliEntrypoint,\n required this.gatewayArgs,\n });\n}\n\nclass _DesktopToolsLaunch {\n final Uri? uri;\n final String? token;\n final Map<String, dynamic> statusJson;\n\n const _DesktopToolsLaunch({\n required this.uri,\n required this.token,\n required this.statusJson,\n });\n}\n\nclass OpenClawRuntimeException implements Exception {\n final String message;\n\n const OpenClawRuntimeException(this.message);\n\n @override\n String toString() => message;\n}\n\nclass OpenClawRuntime {\n OpenClawRuntime._();\n\n static final OpenClawRuntime instance = OpenClawRuntime._();\n\n static const String _manifestAsset = 'assets/openclaw/bundle_manifest.json';\n static const Duration _startupTimeout = Duration(seconds: 45);\n static const Duration _probeTimeout = Duration(seconds: 3);\n\n Process? _process;\n Future<OpenClawGatewayTarget>? _starting;\n OpenClawRuntimeStatus? _lastStatus;\n final List<String> _recentLogs = <String>[];\n\n Future<OpenClawRuntimeStatus> getStatus({bool probe = true}) async {\n final platformKey = _platformKey;\n final port = await AppSettings.getOpenClawGatewayPort();\n _diag(\n 'status.start',\n data: {'platformKey': platformKey, 'port': port, 'probe': probe},\n );\n\n if (platformKey == null) {\n return _remember(\n OpenClawRuntimeStatus(\n state: OpenClawRuntimeState.unsupported,\n message: '当前平台不是 macOS / Windows / Linux 桌面端',\n port: port,\n checkedAt: DateTime.now(),\n ),\n );\n }\n\n try {\n final spec = await _loadSpec(platformKey);\n final runtimeDir = await _runtimeDirForStatus(spec, platformKey);\n final nodePath = p.join(runtimeDir.path, spec.nodeExecutable);\n final cliPath = p.join(runtimeDir.path, spec.cliEntrypoint);\n final nodeExists = await File(nodePath).exists();\n final cliExists = await File(cliPath).exists();\n _diag(\n 'status.paths',\n data: {\n 'platformKey': platformKey,\n 'runtimeDir': runtimeDir.path,\n 'nodePath': nodePath,\n 'nodeExists': nodeExists,\n 'cliPath': cliPath,\n 'cliExists': cliExists,\n },\n );\n\n if (!nodeExists || !cliExists) {\n final hasBundleAssets = await _hasRequiredBundleAssets(\n platformKey,\n spec,\n );\n _diag(\n 'status.bundle-assets',\n data: {\n 'platformKey': platformKey,\n 'hasBundleAssets': hasBundleAssets,\n 'nodeAsset': 'assets/openclaw/$platformKey/${spec.nodeExecutable}',\n 'cliAsset': 'assets/openclaw/$platformKey/${spec.cliEntrypoint}',\n },\n );\n return _remember(\n OpenClawRuntimeStatus(\n state: hasBundleAssets\n ? OpenClawRuntimeState.stopped\n : OpenClawRuntimeState.notBundled,\n message: hasBundleAssets\n ? '内置 OpenClaw 已随 App 打包,尚未释放或启动'\n : '当前 App 包未包含 $platformKey 的 OpenClaw runtime',\n port: port,\n platformKey: platformKey,\n runtimePath: runtimeDir.path,\n checkedAt: DateTime.now(),\n ),\n );\n }\n\n if (probe) {\n final healthy = await _probe(\n port,\n await AppSettings.getOpenClawGatewayToken(),\n );\n if (healthy) {\n return _remember(\n OpenClawRuntimeStatus(\n state: OpenClawRuntimeState.running,\n message: '本机 OpenClaw Gateway 正在运行',\n port: port,\n platformKey: platformKey,\n runtimePath: runtimeDir.path,\n checkedAt: DateTime.now(),\n ),\n );\n }\n }\n\n return _remember(\n OpenClawRuntimeStatus(\n state: _process == null\n ? OpenClawRuntimeState.stopped\n : OpenClawRuntimeState.starting,\n message: _process == null ? '本机 OpenClaw 未启动' : '本机 OpenClaw 正在启动',\n port: port,\n platformKey: platformKey,\n runtimePath: runtimeDir.path,\n checkedAt: DateTime.now(),\n ),\n );\n } catch (error) {\n _diag('status.error', error: error);\n return _remember(\n OpenClawRuntimeStatus(\n state: OpenClawRuntimeState.failed,\n message: error.toString(),\n port: port,\n platformKey: platformKey,\n checkedAt: DateTime.now(),\n ),\n );\n }\n }\n\n Future<OpenClawGatewayTarget> ensureStarted({\n String? authToken,\n String? username,\n bool isMember = false,\n }) {\n _starting ??= _ensureStartedInternal(\n authToken: authToken,\n username: username,\n isMember: isMember,\n ).whenComplete(() => _starting = null);\n return _starting!;\n }\n\n Future<OpenClawRuntimeStatus> restart() async {\n await stop();\n try {\n await ensureStarted();\n return getStatus();\n } catch (error) {\n return _remember(\n OpenClawRuntimeStatus(\n state: OpenClawRuntimeState.failed,\n message: error.toString(),\n port: await AppSettings.getOpenClawGatewayPort(),\n platformKey: _platformKey,\n checkedAt: DateTime.now(),\n ),\n );\n }\n }\n\n Future<void> stop() async {\n final process = _process;\n _process = null;\n if (process == null) return;\n process.kill();\n try {\n await process.exitCode.timeout(const Duration(seconds: 5));\n } catch (_) {\n process.kill();\n }\n }\n\n Future<OpenClawGatewayTarget> _ensureStartedInternal({\n String? authToken,\n String? username,\n bool isMember = false,\n }) async {\n final platformKey = _platformKey;\n _diag('ensure-start.start', data: {'platformKey': platformKey});\n if (platformKey == null) {\n throw const OpenClawRuntimeException('当前平台不支持内置 OpenClaw Gateway');\n }\n\n final spec = await _loadSpec(platformKey);\n final port = await AppSettings.getOpenClawGatewayPort(\n defaultValue: spec.defaultPort,\n );\n final token = await AppSettings.getOpenClawGatewayToken();\n final model = await AppSettings.getOpenClawModel(\n defaultValue: spec.defaultModel,\n );\n final modelOverride = await AppSettings.getOpenClawModelOverride(\n defaultValue: spec.defaultModelOverride ?? '',\n );\n final desktopTools = await _ensureDesktopTools();\n _diag(\n 'ensure-start.config',\n data: {\n 'platformKey': platformKey,\n 'port': port,\n 'model': model,\n 'modelOverrideSet': modelOverride.trim().isNotEmpty,\n 'desktopToolsUri': desktopTools.uri?.toString(),\n 'desktopToolsStatus': desktopTools.statusJson,\n },\n );\n\n if (await _probe(port, token, context: 'pre-start')) {\n _diag('ensure-start.reuse-existing', data: {'port': port});\n return OpenClawGatewayTarget(\n baseUri: Uri.parse('http://127.0.0.1:$port'),\n token: token,\n model: model,\n modelOverride: modelOverride.trim().isEmpty\n ? null\n : modelOverride.trim(),\n desktopToolsUri: desktopTools.uri,\n desktopToolsToken: desktopTools.token,\n desktopToolsStatus: desktopTools.statusJson,\n );\n }\n\n final runtimeDir = await _runtimeDirForLaunch(spec, platformKey);\n final stateRoot = await _stateRoot();\n final configPath = await _ensureConfigFile(\n stateRoot: stateRoot,\n port: port,\n token: token,\n model: model,\n );\n final desktopToolsManifestPath = await _ensureDesktopToolsManifest(\n stateRoot: stateRoot,\n desktopTools: desktopTools,\n );\n\n final nodePath = p.join(runtimeDir.path, spec.nodeExecutable);\n final cliPath = p.join(runtimeDir.path, spec.cliEntrypoint);\n if (!await File(nodePath).exists()) {\n _diag('ensure-start.missing-node', data: {'nodePath': nodePath});\n throw OpenClawRuntimeException('OpenClaw 内置 Node 不存在: $nodePath');\n }\n if (!await File(cliPath).exists()) {\n _diag('ensure-start.missing-cli', data: {'cliPath': cliPath});\n throw OpenClawRuntimeException('OpenClaw CLI 入口不存在: $cliPath');\n }\n\n _remember(\n OpenClawRuntimeStatus(\n state: OpenClawRuntimeState.starting,\n message: '正在启动内置 OpenClaw Gateway',\n port: port,\n platformKey: platformKey,\n runtimePath: runtimeDir.path,\n checkedAt: DateTime.now(),\n ),\n );\n\n final args = <String>[\n cliPath,\n ...spec.gatewayArgs.map((arg) => arg.replaceAll('{port}', '$port')),\n ];\n _diag(\n 'process.starting',\n data: {\n 'nodePath': nodePath,\n 'args': args,\n 'workingDirectory': runtimeDir.path,\n 'configPath': configPath.path,\n 'desktopToolsManifestPath': desktopToolsManifestPath.path,\n },\n );\n\n final deepSeekApiKey = _resolveDeepSeekApiKey();\n final deepSeekEnv = deepSeekApiKey == null\n ? null\n : <String, String>{'DEEPSEEK_API_KEY': deepSeekApiKey};\n final env = Map<String, String>.from(Platform.environment)\n ..addAll({\n 'OPENCLAW_GATEWAY_PORT': '$port',\n 'OPENCLAW_GATEWAY_BIND': 'loopback',\n 'OPENCLAW_GATEWAY_TOKEN': token,\n 'OPENCLAW_CONFIG_PATH': configPath.path,\n 'OPENCLAW_STATE_DIR': p.join(stateRoot.path, 'state'),\n 'OPENCLAW_AGENT_DIR': p.join(stateRoot.path, 'agents'),\n 'OPENCLAW_WORKSPACE': p.join(stateRoot.path, 'workspace'),\n 'DACHENG_DESKTOP_TOOLS_ENABLED': desktopTools.uri == null ? '0' : '1',\n 'DACHENG_DESKTOP_TOOLS_MANIFEST': desktopToolsManifestPath.path,\n if (desktopTools.uri != null)\n 'DACHENG_DESKTOP_TOOLS_URL': desktopTools.uri.toString(),\n if (desktopTools.token != null)\n 'DACHENG_DESKTOP_TOOLS_TOKEN': desktopTools.token!,\n 'DACHENG_APP_RUNTIME': '1',\n if (authToken != null && authToken.isNotEmpty)\n 'DACHENG_AUTH_TOKEN': authToken,\n if (username != null && username.isNotEmpty)\n 'DACHENG_USERNAME': username,\n 'DACHENG_IS_MEMBER': isMember ? '1' : '0',\n ...?deepSeekEnv,\n });\n\n _process = await Process.start(\n nodePath,\n args,\n workingDirectory: runtimeDir.path,\n environment: env,\n mode: ProcessStartMode.normal,\n runInShell: false,\n );\n _captureLogs(_process!);\n _diag('process.started', data: {'pid': _process!.pid, 'port': port});\n\n final deadline = DateTime.now().add(_startupTimeout);\n while (DateTime.now().isBefore(deadline)) {\n if (await _probe(port, token, context: 'startup')) {\n _diag('process.ready', data: {'pid': _process?.pid, 'port': port});\n _remember(\n OpenClawRuntimeStatus(\n state: OpenClawRuntimeState.running,\n message: '本机 OpenClaw Gateway 已启动',\n port: port,\n platformKey: platformKey,\n runtimePath: runtimeDir.path,\n desktopToolsStatus: desktopTools.statusJson,\n checkedAt: DateTime.now(),\n ),\n );\n return OpenClawGatewayTarget(\n baseUri: Uri.parse('http://127.0.0.1:$port'),\n token: token,\n model: model,\n modelOverride: modelOverride.trim().isEmpty\n ? null\n : modelOverride.trim(),\n desktopToolsUri: desktopTools.uri,\n desktopToolsToken: desktopTools.token,\n desktopToolsStatus: desktopTools.statusJson,\n );\n }\n\n if (_process != null) {\n final exited = await _process!.exitCode.timeout(\n const Duration(milliseconds: 1),\n onTimeout: () => -999999,\n );\n if (exited != -999999) {\n final logs = _recentLogs.take(12).join('\\n');\n _diag(\n 'process.exited-early',\n data: {\n 'exitCode': exited,\n 'recentLogs': _recentLogs.take(12).toList(),\n },\n );\n throw OpenClawRuntimeException(\n 'OpenClaw Gateway 提前退出,exitCode=$exited${logs.isEmpty ? '' : '\\n$logs'}',\n );\n }\n }\n await Future<void>.delayed(const Duration(milliseconds: 450));\n }\n\n final logs = _recentLogs.take(12).join('\\n');\n _diag(\n 'process.startup-timeout',\n data: {\n 'timeoutSeconds': _startupTimeout.inSeconds,\n 'recentLogs': _recentLogs.take(12).toList(),\n },\n );\n throw OpenClawRuntimeException(\n 'OpenClaw Gateway 启动超时${logs.isEmpty ? '' : '\\n$logs'}',\n );\n }\n\n Future<_OpenClawBundleSpec> _loadSpec(String platformKey) async {\n final raw = await rootBundle.loadString(_manifestAsset);\n final decoded = jsonDecode(raw) as Map<String, dynamic>;\n final platforms = Map<String, dynamic>.from(\n decoded['platforms'] as Map? ?? const {},\n );\n final platform = Map<String, dynamic>.from(\n platforms[platformKey] as Map? ?? const {},\n );\n if (platform.isEmpty) {\n throw OpenClawRuntimeException('bundle_manifest.json 缺少 $platformKey 配置');\n }\n\n final gatewayArgs =\n (platform['gatewayArgs'] as List? ??\n decoded['gatewayArgs'] as List? ??\n const ['gateway', '--port', '{port}'])\n .map((item) => item.toString())\n .toList();\n\n final spec = _OpenClawBundleSpec(\n version: (decoded['version'] ?? 'dev').toString(),\n defaultPort: _readInt(decoded['defaultPort']) ?? 18789,\n defaultModel: (decoded['defaultModel'] ?? 'deepseek/deepseek-chat')\n .toString(),\n defaultModelOverride: decoded['defaultModelOverride']?.toString(),\n nodeExecutable: (platform['nodeExecutable'] ?? 'node/bin/node')\n .toString(),\n cliEntrypoint: (platform['cliEntrypoint'] ?? 'openclaw/bin/openclaw.js')\n .toString(),\n gatewayArgs: gatewayArgs,\n );\n _diag(\n 'manifest.loaded',\n data: {\n 'platformKey': platformKey,\n 'version': spec.version,\n 'defaultPort': spec.defaultPort,\n 'defaultModel': spec.defaultModel,\n 'nodeExecutable': spec.nodeExecutable,\n 'cliEntrypoint': spec.cliEntrypoint,\n 'gatewayArgs': spec.gatewayArgs,\n },\n );\n return spec;\n }\n\n Future<Directory> _runtimeDirForStatus(\n _OpenClawBundleSpec spec,\n String platformKey,\n ) async {\n final bundledRuntimeDir = await _bundledMacOSRuntimeDir(spec, platformKey);\n if (bundledRuntimeDir != null) return bundledRuntimeDir;\n return _runtimeDir(spec, platformKey);\n }\n\n Future<Directory> _runtimeDirForLaunch(\n _OpenClawBundleSpec spec,\n String platformKey,\n ) async {\n final bundledRuntimeDir = await _bundledMacOSRuntimeDir(spec, platformKey);\n if (bundledRuntimeDir != null) {\n _diag(\n 'bundle.launch.bundled-runtime',\n data: {\n 'platformKey': platformKey,\n 'runtimeDir': bundledRuntimeDir.path,\n },\n );\n return bundledRuntimeDir;\n }\n\n if (Platform.isMacOS && kReleaseMode) {\n final hasBundleAssets = await _hasRequiredBundleAssets(platformKey, spec);\n if (hasBundleAssets) {\n throw OpenClawRuntimeException(\n 'macOS 沙盒要求从已签名的 App bundle 内启动 OpenClaw runtime,但未找到可执行的包内资源路径。请重新安装最新桌面版。',\n );\n }\n }\n\n return _prepareBundle(spec, platformKey);\n }\n\n Future<Directory?> _bundledMacOSRuntimeDir(\n _OpenClawBundleSpec spec,\n String platformKey,\n ) async {\n if (!Platform.isMacOS) return null;\n\n final appBundle = _macOSAppBundleRoot();\n if (appBundle == null) {\n _diag('bundle.macos-app-root-missing');\n return null;\n }\n\n final candidates = <Directory>[\n Directory(\n p.join(\n appBundle.path,\n 'Contents',\n 'Frameworks',\n 'App.framework',\n 'Resources',\n 'flutter_assets',\n 'assets',\n 'openclaw',\n platformKey,\n ),\n ),\n Directory(\n p.join(\n appBundle.path,\n 'Contents',\n 'Frameworks',\n 'App.framework',\n 'Versions',\n 'A',\n 'Resources',\n 'flutter_assets',\n 'assets',\n 'openclaw',\n platformKey,\n ),\n ),\n Directory(\n p.join(\n appBundle.path,\n 'Contents',\n 'Resources',\n 'flutter_assets',\n 'assets',\n 'openclaw',\n platformKey,\n ),\n ),\n ];\n\n for (final candidate in candidates) {\n final nodePath = File(p.join(candidate.path, spec.nodeExecutable));\n final cliPath = File(p.join(candidate.path, spec.cliEntrypoint));\n if (await nodePath.exists() && await cliPath.exists()) {\n return candidate;\n }\n }\n\n _diag(\n 'bundle.macos-bundled-runtime-missing',\n data: {\n 'platformKey': platformKey,\n 'appBundle': appBundle.path,\n 'candidates': candidates.map((item) => item.path).toList(),\n },\n );\n return null;\n }\n\n Directory? _macOSAppBundleRoot() {\n var dir = File(Platform.resolvedExecutable).absolute.parent;\n while (true) {\n if (dir.path.endsWith('.app')) return dir;\n final parent = dir.parent;\n if (parent.path == dir.path) return null;\n dir = parent;\n }\n }\n\n Future<Directory> _prepareBundle(\n _OpenClawBundleSpec spec,\n String platformKey,\n ) async {\n final runtimeDir = await _runtimeDir(spec, platformKey);\n final marker = File(p.join(runtimeDir.path, '.bundle_ready'));\n final nodePath = File(p.join(runtimeDir.path, spec.nodeExecutable));\n final cliPath = File(p.join(runtimeDir.path, spec.cliEntrypoint));\n\n if (await marker.exists() &&\n await nodePath.exists() &&\n await cliPath.exists()) {\n _diag(\n 'bundle.prepare.cached',\n data: {\n 'platformKey': platformKey,\n 'runtimeDir': runtimeDir.path,\n 'marker': marker.path,\n },\n );\n return runtimeDir;\n }\n\n final prefix = 'assets/openclaw/$platformKey/';\n final assets = await _listAssets(prefix);\n final hasNodeAsset = assets.contains('$prefix${spec.nodeExecutable}');\n final hasCliAsset = assets.contains('$prefix${spec.cliEntrypoint}');\n _diag(\n 'bundle.prepare.assets',\n data: {\n 'platformKey': platformKey,\n 'prefix': prefix,\n 'assetCount': assets.length,\n 'hasNodeAsset': hasNodeAsset,\n 'hasCliAsset': hasCliAsset,\n 'nodeAsset': '$prefix${spec.nodeExecutable}',\n 'cliAsset': '$prefix${spec.cliEntrypoint}',\n 'sample': assets.take(8).toList(),\n },\n );\n if (!hasNodeAsset || !hasCliAsset) {\n throw OpenClawRuntimeException(\n '当前安装包没有内置 $platformKey 的 OpenClaw runtime。请在 release 构建中运行 scripts/build_openclaw_desktop_bundle.sh 后再打包。',\n );\n }\n\n if (await runtimeDir.exists()) {\n _diag('bundle.prepare.remove-old', data: {'runtimeDir': runtimeDir.path});\n await runtimeDir.delete(recursive: true);\n }\n await runtimeDir.create(recursive: true);\n\n for (final asset in assets) {\n final relative = asset.substring(prefix.length);\n if (relative.isEmpty || relative.endsWith('/')) continue;\n final bytes = await rootBundle.load(asset);\n final file = File(p.join(runtimeDir.path, relative));\n await file.parent.create(recursive: true);\n await file.writeAsBytes(\n bytes.buffer.asUint8List(bytes.offsetInBytes, bytes.lengthInBytes),\n flush: false,\n );\n }\n\n if (!Platform.isWindows) {\n await Process.run('chmod', ['+x', nodePath.path]);\n }\n await marker.writeAsString(DateTime.now().toIso8601String());\n _diag(\n 'bundle.prepare.complete',\n data: {\n 'platformKey': platformKey,\n 'runtimeDir': runtimeDir.path,\n 'assetCount': assets.length,\n 'nodeExists': await nodePath.exists(),\n 'nodeSize': await nodePath.exists() ? await nodePath.length() : 0,\n 'cliExists': await cliPath.exists(),\n 'cliSize': await cliPath.exists() ? await cliPath.length() : 0,\n },\n );\n return runtimeDir;\n }\n\n Future<bool> _hasRequiredBundleAssets(\n String platformKey,\n _OpenClawBundleSpec spec,\n ) async {\n final prefix = 'assets/openclaw/$platformKey/';\n final assets = await _listAssets(prefix);\n final hasRequired =\n assets.contains('$prefix${spec.nodeExecutable}') &&\n assets.contains('$prefix${spec.cliEntrypoint}');\n _diag(\n 'bundle.has-required-assets',\n data: {\n 'platformKey': platformKey,\n 'assetCount': assets.length,\n 'hasRequired': hasRequired,\n },\n );\n return hasRequired;\n }\n\n Future<List<String>> _listAssets(String prefix) async {\n final assets = <String>{};\n var jsonCount = 0;\n var binCount = 0;\n var indexCount = 0;\n final failures = <String>[];\n\n try {\n final raw = await rootBundle.loadString('AssetManifest.json');\n final decoded = jsonDecode(raw) as Map<String, dynamic>;\n final items = decoded.keys\n .where((key) => key.startsWith(prefix))\n .toList();\n jsonCount = items.length;\n assets.addAll(items);\n } catch (error) {\n failures.add('AssetManifest.json: $error');\n // Newer Flutter release builds may only include AssetManifest.bin.\n }\n\n try {\n final manifest = await AssetManifest.loadFromAssetBundle(rootBundle);\n final items = manifest\n .listAssets()\n .where((key) => key.startsWith(prefix))\n .toList();\n binCount = items.length;\n assets.addAll(items);\n } catch (error) {\n failures.add('AssetManifest.bin: $error');\n // App Store archives patched after flutter build carry a JSON/index file.\n }\n\n try {\n final raw = await rootBundle.loadString(\n 'assets/openclaw/asset_index.json',\n );\n final decoded = jsonDecode(raw);\n final indexedAssets = decoded is Map\n ? decoded['assets']\n : decoded is List\n ? decoded\n : const [];\n if (indexedAssets is List) {\n final items = indexedAssets\n .map((item) => item.toString())\n .where((key) => key.startsWith(prefix))\n .toList();\n indexCount = items.length;\n assets.addAll(items);\n }\n } catch (error) {\n failures.add('asset_index.json: $error');\n // Older builds do not have an OpenClaw asset index.\n }\n\n final sortedAssets = assets.toList()..sort();\n _diag(\n 'asset.list',\n data: {\n 'prefix': prefix,\n 'total': sortedAssets.length,\n 'jsonCount': jsonCount,\n 'binCount': binCount,\n 'indexCount': indexCount,\n if (failures.isNotEmpty) 'failures': failures,\n 'sample': sortedAssets.take(8).toList(),\n },\n );\n return sortedAssets;\n }\n\n Future<bool> _probe(\n int port,\n String token, {\n String context = 'probe',\n }) async {\n try {\n final modelsResponse = await http\n .get(\n Uri.parse('http://127.0.0.1:$port/v1/models'),\n headers: {'Authorization': 'Bearer $token'},\n )\n .timeout(_probeTimeout);\n if (modelsResponse.statusCode < 200 || modelsResponse.statusCode >= 300) {\n _diag(\n 'probe.models-unhealthy',\n data: {\n 'context': context,\n 'port': port,\n 'statusCode': modelsResponse.statusCode,\n },\n );\n return false;\n }\n\n // `/v1/models` can be healthy while `/v1/chat/completions` is disabled\n // by an old config. The home screen uses chat completions, so treat 404\n // or auth failures as an unhealthy embedded runtime and restart it with\n // the repaired config below. A 4xx validation error is acceptable here:\n // it proves the OpenAI-compatible chat route is wired.\n final chatResponse = await http\n .post(\n Uri.parse('http://127.0.0.1:$port/v1/chat/completions'),\n headers: {\n 'Authorization': 'Bearer $token',\n 'Content-Type': 'application/json',\n },\n body: '{}',\n )\n .timeout(_probeTimeout);\n final healthy =\n chatResponse.statusCode != 401 &&\n chatResponse.statusCode != 403 &&\n chatResponse.statusCode != 404;\n _diag(\n healthy ? 'probe.healthy' : 'probe.chat-unhealthy',\n data: {\n 'context': context,\n 'port': port,\n 'chatStatusCode': chatResponse.statusCode,\n },\n );\n return healthy;\n } catch (error) {\n if (context != 'startup') {\n _diag(\n 'probe.error',\n data: {'context': context, 'port': port},\n error: error,\n );\n }\n return false;\n }\n }\n\n Future<File> _ensureConfigFile({\n required Directory stateRoot,\n required int port,\n required String token,\n required String model,\n }) async {\n final configDir = Directory(p.join(stateRoot.path, 'config'));\n await configDir.create(recursive: true);\n final workspace = Directory(p.join(stateRoot.path, 'workspace'));\n await workspace.create(recursive: true);\n final configPath = File(p.join(configDir.path, 'openclaw.json'));\n\n final config = <String, dynamic>{\n 'gateway': {\n 'mode': 'local',\n 'port': port,\n 'bind': 'loopback',\n 'auth': {'mode': 'token', 'token': token},\n 'http': {\n 'endpoints': {\n 'chatCompletions': {'enabled': true},\n 'responses': {'enabled': true},\n },\n },\n },\n 'agents': {\n 'defaults': {\n 'workspace': workspace.path,\n 'model': {'primary': model},\n },\n 'list': [\n {\n 'id': 'dacheng',\n 'default': true,\n 'workspace': workspace.path,\n 'model': {'primary': model},\n },\n ],\n },\n };\n\n // Repair older embedded configs in-place. OpenClaw 2026.6 requires\n // gateway.mode=local, and the home screen requires the OpenAI-compatible\n // chat endpoint. Preserve unrelated user edits while fixing the fields\n // needed for the app-owned embedded gateway to start reliably.\n final merged = await _mergeEmbeddedConfig(configPath, config);\n await configPath.writeAsString(\n const JsonEncoder.withIndent(' ').convert(merged),\n );\n _diag(\n 'config.written',\n data: {\n 'configPath': configPath.path,\n 'port': port,\n 'gatewayMode': _mutableMap(merged['gateway'])['mode'],\n },\n );\n return configPath;\n }\n\n String? _resolveDeepSeekApiKey() {\n for (final value in [\n Platform.environment['DACHENG_OPENCLAW_DEEPSEEK_API_KEY'],\n Platform.environment['DEEPSEEK_API_KEY'],\n AppConfig.configuredOpenClawDeepSeekApiKey,\n ]) {\n final text = value?.trim();\n if (text != null && text.isNotEmpty) return text;\n }\n return null;\n }\n\n Future<File> _ensureDesktopToolsManifest({\n required Directory stateRoot,\n required _DesktopToolsLaunch desktopTools,\n }) async {\n final configDir = Directory(p.join(stateRoot.path, 'config'));\n await configDir.create(recursive: true);\n final manifestPath = File(p.join(configDir.path, 'desktop_tools.json'));\n final tools =\n DesktopControlPolicy.supportedTools\n .map(\n (name) => {\n 'name': name,\n 'endpoint': '/v1/tools/execute',\n 'readOnly': DesktopControlPolicy.isReadOnly(name),\n 'requiresConfirmation':\n DesktopControlPolicy.requiresConfirmation(name),\n },\n )\n .toList()\n ..sort(\n (a, b) => a['name'].toString().compareTo(b['name'].toString()),\n );\n\n await manifestPath.writeAsString(\n const JsonEncoder.withIndent(' ').convert({\n 'version': 1,\n 'transport': 'http-loopback',\n 'baseUrl': desktopTools.uri?.toString(),\n 'auth': {'type': 'bearer', 'env': 'DACHENG_DESKTOP_TOOLS_TOKEN'},\n 'status': desktopTools.statusJson,\n 'tools': tools,\n }),\n );\n _diag(\n 'desktop-tools.manifest-written',\n data: {\n 'manifestPath': manifestPath.path,\n 'hasBaseUrl': desktopTools.uri != null,\n },\n );\n return manifestPath;\n }\n\n Future<Map<String, dynamic>> _mergeEmbeddedConfig(\n File configPath,\n Map<String, dynamic> defaults,\n ) async {\n Map<String, dynamic> current = <String, dynamic>{};\n if (await configPath.exists()) {\n try {\n final decoded = jsonDecode(await configPath.readAsString());\n if (decoded is Map<String, dynamic>) {\n current = decoded;\n } else if (decoded is Map) {\n current = Map<String, dynamic>.from(decoded);\n }\n } catch (_) {\n current = <String, dynamic>{};\n }\n }\n\n final gateway = _mutableMap(current['gateway']);\n final defaultGateway = _mutableMap(defaults['gateway']);\n gateway['mode'] = 'local';\n gateway['port'] = defaultGateway['port'];\n gateway['bind'] = defaultGateway['bind'];\n gateway['auth'] = Map<String, dynamic>.from(defaultGateway['auth'] as Map);\n\n final gatewayHttp = _mutableMap(gateway['http']);\n final endpoints = _mutableMap(gatewayHttp['endpoints']);\n endpoints['chatCompletions'] = {'enabled': true};\n endpoints['responses'] = {'enabled': true};\n gatewayHttp['endpoints'] = endpoints;\n gateway['http'] = gatewayHttp;\n current['gateway'] = gateway;\n\n final agents = _mutableMap(current['agents']);\n final defaultAgents = _mutableMap(defaults['agents']);\n final agentDefaults = _mutableMap(agents['defaults']);\n final defaultAgentDefaults = _mutableMap(defaultAgents['defaults']);\n agentDefaults['workspace'] = defaultAgentDefaults['workspace'];\n agentDefaults['model'] = defaultAgentDefaults['model'];\n agents['defaults'] = agentDefaults;\n agents['list'] = defaultAgents['list'];\n current['agents'] = agents;\n\n return current;\n }\n\n Map<String, dynamic> _mutableMap(Object? value) {\n if (value is Map<String, dynamic>) return Map<String, dynamic>.from(value);\n if (value is Map) return Map<String, dynamic>.from(value);\n return <String, dynamic>{};\n }\n\n void _captureLogs(Process process) {\n void addLog(String line) {\n final text = line.trim();\n if (text.isEmpty) return;\n _recentLogs.insert(0, text);\n if (_recentLogs.length > 80) {\n _recentLogs.removeRange(80, _recentLogs.length);\n }\n if (kDebugMode) {\n debugPrint('[OpenClaw] $text');\n }\n _diag('process.output', data: {'line': text});\n }\n\n process.stdout\n .transform(utf8.decoder)\n .transform(const LineSplitter())\n .listen(addLog, onError: (_) {});\n process.stderr\n .transform(utf8.decoder)\n .transform(const LineSplitter())\n .listen(addLog, onError: (_) {});\n }\n\n Future<Directory> _stateRoot() async {\n final support = await getApplicationSupportDirectory();\n final dir = Directory(p.join(support.path, 'openclaw_embedded'));\n await dir.create(recursive: true);\n return dir;\n }\n\n Future<_DesktopToolsLaunch> _ensureDesktopTools() async {\n try {\n final status = await DesktopControlBridge.instance.ensureStarted();\n _diag(\n 'desktop-tools.started',\n data: {\n 'bridgeRunning': status.bridgeRunning,\n 'bridgeUri': status.bridgeUri?.toString(),\n 'message': status.message,\n },\n );\n return _DesktopToolsLaunch(\n uri: status.bridgeUri,\n token: await DesktopControlBridge.instance.bridgeToken,\n statusJson: status.toJson(),\n );\n } catch (error) {\n _diag('desktop-tools.error', error: error);\n return _DesktopToolsLaunch(\n uri: null,\n token: null,\n statusJson: {\n 'enabledByBuild': false,\n 'bridgeRunning': false,\n 'message': error.toString(),\n },\n );\n }\n }\n\n Future<Directory> _runtimeDir(\n _OpenClawBundleSpec spec,\n String platformKey,\n ) async {\n final support = await getApplicationSupportDirectory();\n return Directory(\n p.join(\n support.path,\n 'openclaw_embedded',\n 'runtime',\n spec.version,\n platformKey,\n ),\n );\n }\n\n OpenClawRuntimeStatus _remember(OpenClawRuntimeStatus status) {\n _lastStatus = status;\n return status;\n }\n\n String? get _platformKey {\n final abi = Abi.current().toString().toLowerCase();\n if (Platform.isMacOS) {\n return abi.contains('arm64') ? 'macos-arm64' : 'macos-x64';\n }\n if (Platform.isWindows) {\n return abi.contains('arm64') ? 'windows-arm64' : 'windows-x64';\n }\n if (Platform.isLinux) {\n return abi.contains('arm64') ? 'linux-arm64' : 'linux-x64';\n }\n return null;\n }\n\n OpenClawRuntimeStatus? get lastStatus => _lastStatus;\n\n void _diag(\n String message, {\n Map<String, Object?> data = const {},\n Object? error,\n StackTrace? stackTrace,\n }) {\n unawaited(\n DiagnosticLogService.instance.log(\n 'openclaw.runtime',\n message,\n data: data,\n error: error,\n stackTrace: stackTrace,\n ),\n );\n }\n}\n\nint? _readInt(Object? value) {\n if (value is int) return value;\n if (value is num) return value.toInt();\n if (value is String) return int.tryParse(value);\n return null;\n}\n"}, "9988134eb212cab8bf1415acd17e782df99ed1c1": {"t": 1785757967.1253386, "n": 34852, "h": "9718de07591f4a35", "c": "import 'dart:async';\nimport 'dart:convert';\nimport 'dart:ffi' show Abi;\nimport 'dart:io';\nimport 'package:flutter/foundation.dart';\nimport 'package:flutter/services.dart';\nimport 'package:http/http.dart' as http;\nimport 'package:path/path.dart' as p;\nimport 'package:path_provider/path_provider.dart';\n\nimport '../app_settings.dart';\nimport '../diagnostic_log_service.dart';\nimport '../desktop_control/desktop_control_bridge.dart';\nimport '../desktop_control/desktop_control_policy.dart';\n\nenum OpenClawRuntimeState {\n unsupported,\n notBundled,\n stopped,\n starting,\n running,\n failed,\n}\n\nclass OpenClawRuntimeStatus {\n final OpenClawRuntimeState state;\n final String message;\n final int? port;\n final String? platformKey;\n final String? runtimePath;\n final Map<String, dynamic>? desktopToolsStatus;\n final DateTime checkedAt;\n\n const OpenClawRuntimeStatus({\n required this.state,\n required this.message,\n this.port,\n this.platformKey,\n this.runtimePath,\n this.desktopToolsStatus,\n required this.checkedAt,\n });\n\n bool get isHealthy => state == OpenClawRuntimeState.running;\n\n String get label {\n switch (state) {\n case OpenClawRuntimeState.unsupported:\n return '不支持';\n case OpenClawRuntimeState.notBundled:\n return '未内置';\n case OpenClawRuntimeState.stopped:\n return '未启动';\n case OpenClawRuntimeState.starting:\n return '启动中';\n case OpenClawRuntimeState.running:\n return '运行中';\n case OpenClawRuntimeState.failed:\n return '异常';\n }\n }\n}\n\nclass OpenClawGatewayTarget {\n final Uri baseUri;\n final String token;\n final String model;\n final String? modelOverride;\n final Uri? desktopToolsUri;\n final String? desktopToolsToken;\n final Map<String, dynamic>? desktopToolsStatus;\n\n const OpenClawGatewayTarget({\n required this.baseUri,\n required this.token,\n required this.model,\n this.modelOverride,\n this.desktopToolsUri,\n this.desktopToolsToken,\n this.desktopToolsStatus,\n });\n}\n\nclass _OpenClawBundleSpec {\n final String version;\n final int defaultPort;\n final String defaultModel;\n final String? defaultModelOverride;\n final String nodeExecutable;\n final String cliEntrypoint;\n final List<String> gatewayArgs;\n\n const _OpenClawBundleSpec({\n required this.version,\n required this.defaultPort,\n required this.defaultModel,\n this.defaultModelOverride,\n required this.nodeExecutable,\n required this.cliEntrypoint,\n required this.gatewayArgs,\n });\n}\n\nclass _DesktopToolsLaunch {\n final Uri? uri;\n final String? token;\n final Map<String, dynamic> statusJson;\n\n const _DesktopToolsLaunch({\n required this.uri,\n required this.token,\n required this.statusJson,\n });\n}\n\nclass OpenClawRuntimeException implements Exception {\n final String message;\n\n const OpenClawRuntimeException(this.message);\n\n @override\n String toString() => message;\n}\n\nclass OpenClawRuntime {\n OpenClawRuntime._();\n\n static final OpenClawRuntime instance = OpenClawRuntime._();\n\n static const String _manifestAsset = 'assets/openclaw/bundle_manifest.json';\n static const Duration _startupTimeout = Duration(seconds: 45);\n static const Duration _probeTimeout = Duration(seconds: 3);\n\n Process? _process;\n Future<OpenClawGatewayTarget>? _starting;\n OpenClawRuntimeStatus? _lastStatus;\n final List<String> _recentLogs = <String>[];\n\n Future<OpenClawRuntimeStatus> getStatus({bool probe = true}) async {\n final platformKey = _platformKey;\n final port = await AppSettings.getOpenClawGatewayPort();\n _diag(\n 'status.start',\n data: {'platformKey': platformKey, 'port': port, 'probe': probe},\n );\n\n if (platformKey == null) {\n return _remember(\n OpenClawRuntimeStatus(\n state: OpenClawRuntimeState.unsupported,\n message: '当前平台不是 macOS / Windows / Linux 桌面端',\n port: port,\n checkedAt: DateTime.now(),\n ),\n );\n }\n\n try {\n final spec = await _loadSpec(platformKey);\n final runtimeDir = await _runtimeDirForStatus(spec, platformKey);\n final nodePath = p.join(runtimeDir.path, spec.nodeExecutable);\n final cliPath = p.join(runtimeDir.path, spec.cliEntrypoint);\n final nodeExists = await File(nodePath).exists();\n final cliExists = await File(cliPath).exists();\n _diag(\n 'status.paths',\n data: {\n 'platformKey': platformKey,\n 'runtimeDir': runtimeDir.path,\n 'nodePath': nodePath,\n 'nodeExists': nodeExists,\n 'cliPath': cliPath,\n 'cliExists': cliExists,\n },\n );\n\n if (!nodeExists || !cliExists) {\n final hasBundleAssets = await _hasRequiredBundleAssets(\n platformKey,\n spec,\n );\n _diag(\n 'status.bundle-assets',\n data: {\n 'platformKey': platformKey,\n 'hasBundleAssets': hasBundleAssets,\n 'nodeAsset': 'assets/openclaw/$platformKey/${spec.nodeExecutable}',\n 'cliAsset': 'assets/openclaw/$platformKey/${spec.cliEntrypoint}',\n },\n );\n return _remember(\n OpenClawRuntimeStatus(\n state: hasBundleAssets\n ? OpenClawRuntimeState.stopped\n : OpenClawRuntimeState.notBundled,\n message: hasBundleAssets\n ? '内置 OpenClaw 已随 App 打包,尚未释放或启动'\n : '当前 App 包未包含 $platformKey 的 OpenClaw runtime',\n port: port,\n platformKey: platformKey,\n runtimePath: runtimeDir.path,\n checkedAt: DateTime.now(),\n ),\n );\n }\n\n if (probe) {\n final healthy = await _probe(\n port,\n await AppSettings.getOpenClawGatewayToken(),\n );\n if (healthy) {\n return _remember(\n OpenClawRuntimeStatus(\n state: OpenClawRuntimeState.running,\n message: '本机 OpenClaw Gateway 正在运行',\n port: port,\n platformKey: platformKey,\n runtimePath: runtimeDir.path,\n checkedAt: DateTime.now(),\n ),\n );\n }\n }\n\n return _remember(\n OpenClawRuntimeStatus(\n state: _process == null\n ? OpenClawRuntimeState.stopped\n : OpenClawRuntimeState.starting,\n message: _process == null ? '本机 OpenClaw 未启动' : '本机 OpenClaw 正在启动',\n port: port,\n platformKey: platformKey,\n runtimePath: runtimeDir.path,\n checkedAt: DateTime.now(),\n ),\n );\n } catch (error) {\n _diag('status.error', error: error);\n return _remember(\n OpenClawRuntimeStatus(\n state: OpenClawRuntimeState.failed,\n message: error.toString(),\n port: port,\n platformKey: platformKey,\n checkedAt: DateTime.now(),\n ),\n );\n }\n }\n\n Future<OpenClawGatewayTarget> ensureStarted({\n String? authToken,\n String? username,\n bool isMember = false,\n }) {\n _starting ??= _ensureStartedInternal(\n authToken: authToken,\n username: username,\n isMember: isMember,\n ).whenComplete(() => _starting = null);\n return _starting!;\n }\n\n Future<OpenClawRuntimeStatus> restart() async {\n await stop();\n try {\n await ensureStarted();\n return getStatus();\n } catch (error) {\n return _remember(\n OpenClawRuntimeStatus(\n state: OpenClawRuntimeState.failed,\n message: error.toString(),\n port: await AppSettings.getOpenClawGatewayPort(),\n platformKey: _platformKey,\n checkedAt: DateTime.now(),\n ),\n );\n }\n }\n\n Future<void> stop() async {\n final process = _process;\n _process = null;\n if (process == null) return;\n process.kill();\n try {\n await process.exitCode.timeout(const Duration(seconds: 5));\n } catch (_) {\n process.kill();\n }\n }\n\n Future<OpenClawGatewayTarget> _ensureStartedInternal({\n String? authToken,\n String? username,\n bool isMember = false,\n }) async {\n final platformKey = _platformKey;\n _diag('ensure-start.start', data: {'platformKey': platformKey});\n if (platformKey == null) {\n throw const OpenClawRuntimeException('当前平台不支持内置 OpenClaw Gateway');\n }\n\n final spec = await _loadSpec(platformKey);\n final port = await AppSettings.getOpenClawGatewayPort(\n defaultValue: spec.defaultPort,\n );\n final token = await AppSettings.getOpenClawGatewayToken();\n final model = await AppSettings.getOpenClawModel(\n defaultValue: spec.defaultModel,\n );\n final modelOverride = await AppSettings.getOpenClawModelOverride(\n defaultValue: spec.defaultModelOverride ?? '',\n );\n final desktopTools = await _ensureDesktopTools();\n _diag(\n 'ensure-start.config',\n data: {\n 'platformKey': platformKey,\n 'port': port,\n 'model': model,\n 'modelOverrideSet': modelOverride.trim().isNotEmpty,\n 'desktopToolsUri': desktopTools.uri?.toString(),\n 'desktopToolsStatus': desktopTools.statusJson,\n },\n );\n\n if (await _probe(port, token, context: 'pre-start')) {\n _diag('ensure-start.reuse-existing', data: {'port': port});\n return OpenClawGatewayTarget(\n baseUri: Uri.parse('http://127.0.0.1:$port'),\n token: token,\n model: model,\n modelOverride: modelOverride.trim().isEmpty\n ? null\n : modelOverride.trim(),\n desktopToolsUri: desktopTools.uri,\n desktopToolsToken: desktopTools.token,\n desktopToolsStatus: desktopTools.statusJson,\n );\n }\n\n final runtimeDir = await _runtimeDirForLaunch(spec, platformKey);\n final stateRoot = await _stateRoot();\n final configPath = await _ensureConfigFile(\n stateRoot: stateRoot,\n port: port,\n token: token,\n );\n final desktopToolsManifestPath = await _ensureDesktopToolsManifest(\n stateRoot: stateRoot,\n desktopTools: desktopTools,\n );\n\n final nodePath = p.join(runtimeDir.path, spec.nodeExecutable);\n final cliPath = p.join(runtimeDir.path, spec.cliEntrypoint);\n if (!await File(nodePath).exists()) {\n _diag('ensure-start.missing-node', data: {'nodePath': nodePath});\n throw OpenClawRuntimeException('OpenClaw 内置 Node 不存在: $nodePath');\n }\n if (!await File(cliPath).exists()) {\n _diag('ensure-start.missing-cli', data: {'cliPath': cliPath});\n throw OpenClawRuntimeException('OpenClaw CLI 入口不存在: $cliPath');\n }\n\n _remember(\n OpenClawRuntimeStatus(\n state: OpenClawRuntimeState.starting,\n message: '正在启动内置 OpenClaw Gateway',\n port: port,\n platformKey: platformKey,\n runtimePath: runtimeDir.path,\n checkedAt: DateTime.now(),\n ),\n );\n\n final args = <String>[\n cliPath,\n ...spec.gatewayArgs.map((arg) => arg.replaceAll('{port}', '$port')),\n ];\n _diag(\n 'process.starting',\n data: {\n 'nodePath': nodePath,\n 'args': args,\n 'workingDirectory': runtimeDir.path,\n 'configPath': configPath.path,\n 'desktopToolsManifestPath': desktopToolsManifestPath.path,\n },\n );\n\n final env = Map<String, String>.from(Platform.environment)\n ..addAll({\n 'OPENCLAW_GATEWAY_PORT': '$port',\n 'OPENCLAW_GATEWAY_BIND': 'loopback',\n 'OPENCLAW_GATEWAY_TOKEN': token,\n 'OPENCLAW_CONFIG_PATH': configPath.path,\n 'OPENCLAW_STATE_DIR': p.join(stateRoot.path, 'state'),\n 'OPENCLAW_AGENT_DIR': p.join(stateRoot.path, 'agents'),\n 'OPENCLAW_WORKSPACE': p.join(stateRoot.path, 'workspace'),\n 'DACHENG_DESKTOP_TOOLS_ENABLED': desktopTools.uri == null ? '0' : '1',\n 'DACHENG_DESKTOP_TOOLS_MANIFEST': desktopToolsManifestPath.path,\n if (desktopTools.uri != null)\n 'DACHENG_DESKTOP_TOOLS_URL': desktopTools.uri.toString(),\n if (desktopTools.token != null)\n 'DACHENG_DESKTOP_TOOLS_TOKEN': desktopTools.token!,\n 'DACHENG_APP_RUNTIME': '1',\n if (authToken != null && authToken.isNotEmpty)\n 'DACHENG_AUTH_TOKEN': authToken,\n if (username != null && username.isNotEmpty)\n 'DACHENG_USERNAME': username,\n 'DACHENG_IS_MEMBER': isMember ? '1' : '0',\n });\n\n _process = await Process.start(\n nodePath,\n args,\n workingDirectory: runtimeDir.path,\n environment: env,\n mode: ProcessStartMode.normal,\n runInShell: false,\n );\n _captureLogs(_process!);\n _diag('process.started', data: {'pid': _process!.pid, 'port': port});\n\n final deadline = DateTime.now().add(_startupTimeout);\n while (DateTime.now().isBefore(deadline)) {\n if (await _probe(port, token, context: 'startup')) {\n _diag('process.ready', data: {'pid': _process?.pid, 'port': port});\n _remember(\n OpenClawRuntimeStatus(\n state: OpenClawRuntimeState.running,\n message: '本机 OpenClaw Gateway 已启动',\n port: port,\n platformKey: platformKey,\n runtimePath: runtimeDir.path,\n desktopToolsStatus: desktopTools.statusJson,\n checkedAt: DateTime.now(),\n ),\n );\n return OpenClawGatewayTarget(\n baseUri: Uri.parse('http://127.0.0.1:$port'),\n token: token,\n model: model,\n modelOverride: modelOverride.trim().isEmpty\n ? null\n : modelOverride.trim(),\n desktopToolsUri: desktopTools.uri,\n desktopToolsToken: desktopTools.token,\n desktopToolsStatus: desktopTools.statusJson,\n );\n }\n\n if (_process != null) {\n final exited = await _process!.exitCode.timeout(\n const Duration(milliseconds: 1),\n onTimeout: () => -999999,\n );\n if (exited != -999999) {\n final logs = _recentLogs.take(12).join('\\n');\n _diag(\n 'process.exited-early',\n data: {\n 'exitCode': exited,\n 'recentLogs': _recentLogs.take(12).toList(),\n },\n );\n throw OpenClawRuntimeException(\n 'OpenClaw Gateway 提前退出,exitCode=$exited${logs.isEmpty ? '' : '\\n$logs'}',\n );\n }\n }\n await Future<void>.delayed(const Duration(milliseconds: 450));\n }\n\n final logs = _recentLogs.take(12).join('\\n');\n _diag(\n 'process.startup-timeout',\n data: {\n 'timeoutSeconds': _startupTimeout.inSeconds,\n 'recentLogs': _recentLogs.take(12).toList(),\n },\n );\n throw OpenClawRuntimeException(\n 'OpenClaw Gateway 启动超时${logs.isEmpty ? '' : '\\n$logs'}',\n );\n }\n\n Future<_OpenClawBundleSpec> _loadSpec(String platformKey) async {\n final raw = await rootBundle.loadString(_manifestAsset);\n final decoded = jsonDecode(raw) as Map<String, dynamic>;\n final platforms = Map<String, dynamic>.from(\n decoded['platforms'] as Map? ?? const {},\n );\n final platform = Map<String, dynamic>.from(\n platforms[platformKey] as Map? ?? const {},\n );\n if (platform.isEmpty) {\n throw OpenClawRuntimeException('bundle_manifest.json 缺少 $platformKey 配置');\n }\n\n final gatewayArgs =\n (platform['gatewayArgs'] as List? ??\n decoded['gatewayArgs'] as List? ??\n const ['gateway', '--port', '{port}'])\n .map((item) => item.toString())\n .toList();\n\n final spec = _OpenClawBundleSpec(\n version: (decoded['version'] ?? 'dev').toString(),\n defaultPort: _readInt(decoded['defaultPort']) ?? 18789,\n defaultModel: (decoded['defaultModel'] ?? 'openclaw/default').toString(),\n defaultModelOverride: decoded['defaultModelOverride']?.toString(),\n nodeExecutable: (platform['nodeExecutable'] ?? 'node/bin/node')\n .toString(),\n cliEntrypoint: (platform['cliEntrypoint'] ?? 'openclaw/bin/openclaw.js')\n .toString(),\n gatewayArgs: gatewayArgs,\n );\n _diag(\n 'manifest.loaded',\n data: {\n 'platformKey': platformKey,\n 'version': spec.version,\n 'defaultPort': spec.defaultPort,\n 'defaultModel': spec.defaultModel,\n 'nodeExecutable': spec.nodeExecutable,\n 'cliEntrypoint': spec.cliEntrypoint,\n 'gatewayArgs': spec.gatewayArgs,\n },\n );\n return spec;\n }\n\n Future<Directory> _runtimeDirForStatus(\n _OpenClawBundleSpec spec,\n String platformKey,\n ) async {\n final bundledRuntimeDir = await _bundledMacOSRuntimeDir(spec, platformKey);\n if (bundledRuntimeDir != null) return bundledRuntimeDir;\n return _runtimeDir(spec, platformKey);\n }\n\n Future<Directory> _runtimeDirForLaunch(\n _OpenClawBundleSpec spec,\n String platformKey,\n ) async {\n final bundledRuntimeDir = await _bundledMacOSRuntimeDir(spec, platformKey);\n if (bundledRuntimeDir != null) {\n _diag(\n 'bundle.launch.bundled-runtime',\n data: {'platformKey': platformKey, 'runtimeDir': bundledRuntimeDir.path},\n );\n return bundledRuntimeDir;\n }\n\n if (Platform.isMacOS && kReleaseMode) {\n final hasBundleAssets = await _hasRequiredBundleAssets(platformKey, spec);\n if (hasBundleAssets) {\n throw OpenClawRuntimeException(\n 'macOS 沙盒要求从已签名的 App bundle 内启动 OpenClaw runtime,但未找到可执行的包内资源路径。请重新安装最新桌面版。',\n );\n }\n }\n\n return _prepareBundle(spec, platformKey);\n }\n\n Future<Directory?> _bundledMacOSRuntimeDir(\n _OpenClawBundleSpec spec,\n String platformKey,\n ) async {\n if (!Platform.isMacOS) return null;\n\n final appBundle = _macOSAppBundleRoot();\n if (appBundle == null) {\n _diag('bundle.macos-app-root-missing');\n return null;\n }\n\n final candidates = <Directory>[\n Directory(\n p.join(\n appBundle.path,\n 'Contents',\n 'Frameworks',\n 'App.framework',\n 'Resources',\n 'flutter_assets',\n 'assets',\n 'openclaw',\n platformKey,\n ),\n ),\n Directory(\n p.join(\n appBundle.path,\n 'Contents',\n 'Frameworks',\n 'App.framework',\n 'Versions',\n 'A',\n 'Resources',\n 'flutter_assets',\n 'assets',\n 'openclaw',\n platformKey,\n ),\n ),\n Directory(\n p.join(\n appBundle.path,\n 'Contents',\n 'Resources',\n 'flutter_assets',\n 'assets',\n 'openclaw',\n platformKey,\n ),\n ),\n ];\n\n for (final candidate in candidates) {\n final nodePath = File(p.join(candidate.path, spec.nodeExecutable));\n final cliPath = File(p.join(candidate.path, spec.cliEntrypoint));\n if (await nodePath.exists() && await cliPath.exists()) {\n return candidate;\n }\n }\n\n _diag(\n 'bundle.macos-bundled-runtime-missing',\n data: {\n 'platformKey': platformKey,\n 'appBundle': appBundle.path,\n 'candidates': candidates.map((item) => item.path).toList(),\n },\n );\n return null;\n }\n\n Directory? _macOSAppBundleRoot() {\n var dir = File(Platform.resolvedExecutable).absolute.parent;\n while (true) {\n if (dir.path.endsWith('.app')) return dir;\n final parent = dir.parent;\n if (parent.path == dir.path) return null;\n dir = parent;\n }\n }\n\n Future<Directory> _prepareBundle(\n _OpenClawBundleSpec spec,\n String platformKey,\n ) async {\n final runtimeDir = await _runtimeDir(spec, platformKey);\n final marker = File(p.join(runtimeDir.path, '.bundle_ready'));\n final nodePath = File(p.join(runtimeDir.path, spec.nodeExecutable));\n final cliPath = File(p.join(runtimeDir.path, spec.cliEntrypoint));\n\n if (await marker.exists() &&\n await nodePath.exists() &&\n await cliPath.exists()) {\n _diag(\n 'bundle.prepare.cached',\n data: {\n 'platformKey': platformKey,\n 'runtimeDir': runtimeDir.path,\n 'marker': marker.path,\n },\n );\n return runtimeDir;\n }\n\n final prefix = 'assets/openclaw/$platformKey/';\n final assets = await _listAssets(prefix);\n final hasNodeAsset = assets.contains('$prefix${spec.nodeExecutable}');\n final hasCliAsset = assets.contains('$prefix${spec.cliEntrypoint}');\n _diag(\n 'bundle.prepare.assets',\n data: {\n 'platformKey': platformKey,\n 'prefix': prefix,\n 'assetCount': assets.length,\n 'hasNodeAsset': hasNodeAsset,\n 'hasCliAsset': hasCliAsset,\n 'nodeAsset': '$prefix${spec.nodeExecutable}',\n 'cliAsset': '$prefix${spec.cliEntrypoint}',\n 'sample': assets.take(8).toList(),\n },\n );\n if (!hasNodeAsset || !hasCliAsset) {\n throw OpenClawRuntimeException(\n '当前安装包没有内置 $platformKey 的 OpenClaw runtime。请在 release 构建中运行 scripts/build_openclaw_desktop_bundle.sh 后再打包。',\n );\n }\n\n if (await runtimeDir.exists()) {\n _diag('bundle.prepare.remove-old', data: {'runtimeDir': runtimeDir.path});\n await runtimeDir.delete(recursive: true);\n }\n await runtimeDir.create(recursive: true);\n\n for (final asset in assets) {\n final relative = asset.substring(prefix.length);\n if (relative.isEmpty || relative.endsWith('/')) continue;\n final bytes = await rootBundle.load(asset);\n final file = File(p.join(runtimeDir.path, relative));\n await file.parent.create(recursive: true);\n await file.writeAsBytes(\n bytes.buffer.asUint8List(bytes.offsetInBytes, bytes.lengthInBytes),\n flush: false,\n );\n }\n\n if (!Platform.isWindows) {\n await Process.run('chmod', ['+x', nodePath.path]);\n }\n await marker.writeAsString(DateTime.now().toIso8601String());\n _diag(\n 'bundle.prepare.complete',\n data: {\n 'platformKey': platformKey,\n 'runtimeDir': runtimeDir.path,\n 'assetCount': assets.length,\n 'nodeExists': await nodePath.exists(),\n 'nodeSize': await nodePath.exists() ? await nodePath.length() : 0,\n 'cliExists': await cliPath.exists(),\n 'cliSize': await cliPath.exists() ? await cliPath.length() : 0,\n },\n );\n return runtimeDir;\n }\n\n Future<bool> _hasRequiredBundleAssets(\n String platformKey,\n _OpenClawBundleSpec spec,\n ) async {\n final prefix = 'assets/openclaw/$platformKey/';\n final assets = await _listAssets(prefix);\n final hasRequired =\n assets.contains('$prefix${spec.nodeExecutable}') &&\n assets.contains('$prefix${spec.cliEntrypoint}');\n _diag(\n 'bundle.has-required-assets',\n data: {\n 'platformKey': platformKey,\n 'assetCount': assets.length,\n 'hasRequired': hasRequired,\n },\n );\n return hasRequired;\n }\n\n Future<List<String>> _listAssets(String prefix) async {\n final assets = <String>{};\n var jsonCount = 0;\n var binCount = 0;\n var indexCount = 0;\n final failures = <String>[];\n\n try {\n final raw = await rootBundle.loadString('AssetManifest.json');\n final decoded = jsonDecode(raw) as Map<String, dynamic>;\n final items = decoded.keys\n .where((key) => key.startsWith(prefix))\n .toList();\n jsonCount = items.length;\n assets.addAll(items);\n } catch (error) {\n failures.add('AssetManifest.json: $error');\n // Newer Flutter release builds may only include AssetManifest.bin.\n }\n\n try {\n final manifest = await AssetManifest.loadFromAssetBundle(rootBundle);\n final items = manifest\n .listAssets()\n .where((key) => key.startsWith(prefix))\n .toList();\n binCount = items.length;\n assets.addAll(items);\n } catch (error) {\n failures.add('AssetManifest.bin: $error');\n // App Store archives patched after flutter build carry a JSON/index file.\n }\n\n try {\n final raw = await rootBundle.loadString(\n 'assets/openclaw/asset_index.json',\n );\n final decoded = jsonDecode(raw);\n final indexedAssets = decoded is Map\n ? decoded['assets']\n : decoded is List\n ? decoded\n : const [];\n if (indexedAssets is List) {\n final items = indexedAssets\n .map((item) => item.toString())\n .where((key) => key.startsWith(prefix))\n .toList();\n indexCount = items.length;\n assets.addAll(items);\n }\n } catch (error) {\n failures.add('asset_index.json: $error');\n // Older builds do not have an OpenClaw asset index.\n }\n\n final sortedAssets = assets.toList()..sort();\n _diag(\n 'asset.list',\n data: {\n 'prefix': prefix,\n 'total': sortedAssets.length,\n 'jsonCount': jsonCount,\n 'binCount': binCount,\n 'indexCount': indexCount,\n if (failures.isNotEmpty) 'failures': failures,\n 'sample': sortedAssets.take(8).toList(),\n },\n );\n return sortedAssets;\n }\n\n Future<bool> _probe(\n int port,\n String token, {\n String context = 'probe',\n }) async {\n try {\n final modelsResponse = await http\n .get(\n Uri.parse('http://127.0.0.1:$port/v1/models'),\n headers: {'Authorization': 'Bearer $token'},\n )\n .timeout(_probeTimeout);\n if (modelsResponse.statusCode < 200 || modelsResponse.statusCode >= 300) {\n _diag(\n 'probe.models-unhealthy',\n data: {\n 'context': context,\n 'port': port,\n 'statusCode': modelsResponse.statusCode,\n },\n );\n return false;\n }\n\n // `/v1/models` can be healthy while `/v1/chat/completions` is disabled\n // by an old config. The home screen uses chat completions, so treat 404\n // or auth failures as an unhealthy embedded runtime and restart it with\n // the repaired config below. A 4xx validation error is acceptable here:\n // it proves the OpenAI-compatible chat route is wired.\n final chatResponse = await http\n .post(\n Uri.parse('http://127.0.0.1:$port/v1/chat/completions'),\n headers: {\n 'Authorization': 'Bearer $token',\n 'Content-Type': 'application/json',\n },\n body: '{}',\n )\n .timeout(_probeTimeout);\n final healthy =\n chatResponse.statusCode != 401 &&\n chatResponse.statusCode != 403 &&\n chatResponse.statusCode != 404;\n _diag(\n healthy ? 'probe.healthy' : 'probe.chat-unhealthy',\n data: {\n 'context': context,\n 'port': port,\n 'chatStatusCode': chatResponse.statusCode,\n },\n );\n return healthy;\n } catch (error) {\n if (context != 'startup') {\n _diag(\n 'probe.error',\n data: {'context': context, 'port': port},\n error: error,\n );\n }\n return false;\n }\n }\n\n Future<File> _ensureConfigFile({\n required Directory stateRoot,\n required int port,\n required String token,\n }) async {\n final configDir = Directory(p.join(stateRoot.path, 'config'));\n await configDir.create(recursive: true);\n final workspace = Directory(p.join(stateRoot.path, 'workspace'));\n await workspace.create(recursive: true);\n final configPath = File(p.join(configDir.path, 'openclaw.json'));\n\n final config = <String, dynamic>{\n 'gateway': {\n 'mode': 'local',\n 'port': port,\n 'bind': 'loopback',\n 'auth': {'mode': 'token', 'token': token},\n 'http': {\n 'endpoints': {\n 'chatCompletions': {'enabled': true},\n 'responses': {'enabled': true},\n },\n },\n },\n 'agents': {\n 'defaults': {'workspace': workspace.path},\n 'list': [\n {'id': 'dacheng', 'default': true, 'workspace': workspace.path},\n ],\n },\n };\n\n // Repair older embedded configs in-place. OpenClaw 2026.6 requires\n // gateway.mode=local, and the home screen requires the OpenAI-compatible\n // chat endpoint. Preserve unrelated user edits while fixing the fields\n // needed for the app-owned embedded gateway to start reliably.\n final merged = await _mergeEmbeddedConfig(configPath, config);\n await configPath.writeAsString(\n const JsonEncoder.withIndent(' ').convert(merged),\n );\n _diag(\n 'config.written',\n data: {\n 'configPath': configPath.path,\n 'port': port,\n 'gatewayMode': _mutableMap(merged['gateway'])['mode'],\n },\n );\n return configPath;\n }\n\n Future<File> _ensureDesktopToolsManifest({\n required Directory stateRoot,\n required _DesktopToolsLaunch desktopTools,\n }) async {\n final configDir = Directory(p.join(stateRoot.path, 'config'));\n await configDir.create(recursive: true);\n final manifestPath = File(p.join(configDir.path, 'desktop_tools.json'));\n final tools =\n DesktopControlPolicy.supportedTools\n .map(\n (name) => {\n 'name': name,\n 'endpoint': '/v1/tools/execute',\n 'readOnly': DesktopControlPolicy.isReadOnly(name),\n 'requiresConfirmation':\n DesktopControlPolicy.requiresConfirmation(name),\n },\n )\n .toList()\n ..sort(\n (a, b) => a['name'].toString().compareTo(b['name'].toString()),\n );\n\n await manifestPath.writeAsString(\n const JsonEncoder.withIndent(' ').convert({\n 'version': 1,\n 'transport': 'http-loopback',\n 'baseUrl': desktopTools.uri?.toString(),\n 'auth': {'type': 'bearer', 'env': 'DACHENG_DESKTOP_TOOLS_TOKEN'},\n 'status': desktopTools.statusJson,\n 'tools': tools,\n }),\n );\n _diag(\n 'desktop-tools.manifest-written',\n data: {\n 'manifestPath': manifestPath.path,\n 'hasBaseUrl': desktopTools.uri != null,\n },\n );\n return manifestPath;\n }\n\n Future<Map<String, dynamic>> _mergeEmbeddedConfig(\n File configPath,\n Map<String, dynamic> defaults,\n ) async {\n Map<String, dynamic> current = <String, dynamic>{};\n if (await configPath.exists()) {\n try {\n final decoded = jsonDecode(await configPath.readAsString());\n if (decoded is Map<String, dynamic>) {\n current = decoded;\n } else if (decoded is Map) {\n current = Map<String, dynamic>.from(decoded);\n }\n } catch (_) {\n current = <String, dynamic>{};\n }\n }\n\n final gateway = _mutableMap(current['gateway']);\n final defaultGateway = _mutableMap(defaults['gateway']);\n gateway['mode'] = 'local';\n gateway['port'] = defaultGateway['port'];\n gateway['bind'] = defaultGateway['bind'];\n gateway['auth'] = Map<String, dynamic>.from(defaultGateway['auth'] as Map);\n\n final gatewayHttp = _mutableMap(gateway['http']);\n final endpoints = _mutableMap(gatewayHttp['endpoints']);\n endpoints['chatCompletions'] = {'enabled': true};\n endpoints['responses'] = {'enabled': true};\n gatewayHttp['endpoints'] = endpoints;\n gateway['http'] = gatewayHttp;\n current['gateway'] = gateway;\n\n final agents = _mutableMap(current['agents']);\n final defaultAgents = _mutableMap(defaults['agents']);\n agents['defaults'] ??= defaultAgents['defaults'];\n agents['list'] ??= defaultAgents['list'];\n current['agents'] = agents;\n\n return current;\n }\n\n Map<String, dynamic> _mutableMap(Object? value) {\n if (value is Map<String, dynamic>) return Map<String, dynamic>.from(value);\n if (value is Map) return Map<String, dynamic>.from(value);\n return <String, dynamic>{};\n }\n\n void _captureLogs(Process process) {\n void addLog(String line) {\n final text = line.trim();\n if (text.isEmpty) return;\n _recentLogs.insert(0, text);\n if (_recentLogs.length > 80) {\n _recentLogs.removeRange(80, _recentLogs.length);\n }\n if (kDebugMode) {\n debugPrint('[OpenClaw] $text');\n }\n _diag('process.output', data: {'line': text});\n }\n\n process.stdout\n .transform(utf8.decoder)\n .transform(const LineSplitter())\n .listen(addLog, onError: (_) {});\n process.stderr\n .transform(utf8.decoder)\n .transform(const LineSplitter())\n .listen(addLog, onError: (_) {});\n }\n\n Future<Directory> _stateRoot() async {\n final support = await getApplicationSupportDirectory();\n final dir = Directory(p.join(support.path, 'openclaw_embedded'));\n await dir.create(recursive: true);\n return dir;\n }\n\n Future<_DesktopToolsLaunch> _ensureDesktopTools() async {\n try {\n final status = await DesktopControlBridge.instance.ensureStarted();\n _diag(\n 'desktop-tools.started',\n data: {\n 'bridgeRunning': status.bridgeRunning,\n 'bridgeUri': status.bridgeUri?.toString(),\n 'message': status.message,\n },\n );\n return _DesktopToolsLaunch(\n uri: status.bridgeUri,\n token: await DesktopControlBridge.instance.bridgeToken,\n statusJson: status.toJson(),\n );\n } catch (error) {\n _diag('desktop-tools.error', error: error);\n return _DesktopToolsLaunch(\n uri: null,\n token: null,\n statusJson: {\n 'enabledByBuild': false,\n 'bridgeRunning': false,\n 'message': error.toString(),\n },\n );\n }\n }\n\n Future<Directory> _runtimeDir(\n _OpenClawBundleSpec spec,\n String platformKey,\n ) async {\n final support = await getApplicationSupportDirectory();\n return Directory(\n p.join(\n support.path,\n 'openclaw_embedded',\n 'runtime',\n spec.version,\n platformKey,\n ),\n );\n }\n\n OpenClawRuntimeStatus _remember(OpenClawRuntimeStatus status) {\n _lastStatus = status;\n return status;\n }\n\n String? get _platformKey {\n final abi = Abi.current().toString().toLowerCase();\n if (Platform.isMacOS) {\n return abi.contains('arm64') ? 'macos-arm64' : 'macos-x64';\n }\n if (Platform.isWindows) {\n return abi.contains('arm64') ? 'windows-arm64' : 'windows-x64';\n }\n if (Platform.isLinux) {\n return abi.contains('arm64') ? 'linux-arm64' : 'linux-x64';\n }\n return null;\n }\n\n OpenClawRuntimeStatus? get lastStatus => _lastStatus;\n\n void _diag(\n String message, {\n Map<String, Object?> data = const {},\n Object? error,\n StackTrace? stackTrace,\n }) {\n unawaited(\n DiagnosticLogService.instance.log(\n 'openclaw.runtime',\n message,\n data: data,\n error: error,\n stackTrace: stackTrace,\n ),\n );\n }\n}\n\nint? _readInt(Object? value) {\n if (value is int) return value;\n if (value is num) return value.toInt();\n if (value is String) return int.tryParse(value);\n return null;\n}\n"}, "e1a9edabdb80d335339331576e978848876839b8": {"t": 1785757968.6839194, "n": 31815, "h": "7143c32c4ed9afea", "c": "import 'dart:async';\nimport 'dart:convert';\nimport 'dart:ffi' show Abi;\nimport 'dart:io';\nimport 'package:flutter/foundation.dart';\nimport 'package:flutter/services.dart';\nimport 'package:http/http.dart' as http;\nimport 'package:path/path.dart' as p;\nimport 'package:path_provider/path_provider.dart';\n\nimport '../app_settings.dart';\nimport '../diagnostic_log_service.dart';\nimport '../desktop_control/desktop_control_bridge.dart';\nimport '../desktop_control/desktop_control_policy.dart';\n\nenum OpenClawRuntimeState {\n unsupported,\n notBundled,\n stopped,\n starting,\n running,\n failed,\n}\n\nclass OpenClawRuntimeStatus {\n final OpenClawRuntimeState state;\n final String message;\n final int? port;\n final String? platformKey;\n final String? runtimePath;\n final Map<String, dynamic>? desktopToolsStatus;\n final DateTime checkedAt;\n\n const OpenClawRuntimeStatus({\n required this.state,\n required this.message,\n this.port,\n this.platformKey,\n this.runtimePath,\n this.desktopToolsStatus,\n required this.checkedAt,\n });\n\n bool get isHealthy => state == OpenClawRuntimeState.running;\n\n String get label {\n switch (state) {\n case OpenClawRuntimeState.unsupported:\n return '不支持';\n case OpenClawRuntimeState.notBundled:\n return '未内置';\n case OpenClawRuntimeState.stopped:\n return '未启动';\n case OpenClawRuntimeState.starting:\n return '启动中';\n case OpenClawRuntimeState.running:\n return '运行中';\n case OpenClawRuntimeState.failed:\n return '异常';\n }\n }\n}\n\nclass OpenClawGatewayTarget {\n final Uri baseUri;\n final String token;\n final String model;\n final String? modelOverride;\n final Uri? desktopToolsUri;\n final String? desktopToolsToken;\n final Map<String, dynamic>? desktopToolsStatus;\n\n const OpenClawGatewayTarget({\n required this.baseUri,\n required this.token,\n required this.model,\n this.modelOverride,\n this.desktopToolsUri,\n this.desktopToolsToken,\n this.desktopToolsStatus,\n });\n}\n\nclass _OpenClawBundleSpec {\n final String version;\n final int defaultPort;\n final String defaultModel;\n final String? defaultModelOverride;\n final String nodeExecutable;\n final String cliEntrypoint;\n final List<String> gatewayArgs;\n\n const _OpenClawBundleSpec({\n required this.version,\n required this.defaultPort,\n required this.defaultModel,\n this.defaultModelOverride,\n required this.nodeExecutable,\n required this.cliEntrypoint,\n required this.gatewayArgs,\n });\n}\n\nclass _DesktopToolsLaunch {\n final Uri? uri;\n final String? token;\n final Map<String, dynamic> statusJson;\n\n const _DesktopToolsLaunch({\n required this.uri,\n required this.token,\n required this.statusJson,\n });\n}\n\nclass OpenClawRuntimeException implements Exception {\n final String message;\n\n const OpenClawRuntimeException(this.message);\n\n @override\n String toString() => message;\n}\n\nclass OpenClawRuntime {\n OpenClawRuntime._();\n\n static final OpenClawRuntime instance = OpenClawRuntime._();\n\n static const String _manifestAsset = 'assets/openclaw/bundle_manifest.json';\n static const Duration _startupTimeout = Duration(seconds: 45);\n static const Duration _probeTimeout = Duration(seconds: 3);\n\n Process? _process;\n Future<OpenClawGatewayTarget>? _starting;\n OpenClawRuntimeStatus? _lastStatus;\n final List<String> _recentLogs = <String>[];\n\n Future<OpenClawRuntimeStatus> getStatus({bool probe = true}) async {\n final platformKey = _platformKey;\n final port = await AppSettings.getOpenClawGatewayPort();\n _diag(\n 'status.start',\n data: {'platformKey': platformKey, 'port': port, 'probe': probe},\n );\n\n if (platformKey == null) {\n return _remember(\n OpenClawRuntimeStatus(\n state: OpenClawRuntimeState.unsupported,\n message: '当前平台不是 macOS / Windows / Linux 桌面端',\n port: port,\n checkedAt: DateTime.now(),\n ),\n );\n }\n\n try {\n final spec = await _loadSpec(platformKey);\n final runtimeDir = await _runtimeDir(spec, platformKey);\n final nodePath = p.join(runtimeDir.path, spec.nodeExecutable);\n final cliPath = p.join(runtimeDir.path, spec.cliEntrypoint);\n final nodeExists = await File(nodePath).exists();\n final cliExists = await File(cliPath).exists();\n _diag(\n 'status.paths',\n data: {\n 'platformKey': platformKey,\n 'runtimeDir': runtimeDir.path,\n 'nodePath': nodePath,\n 'nodeExists': nodeExists,\n 'cliPath': cliPath,\n 'cliExists': cliExists,\n },\n );\n\n if (!nodeExists || !cliExists) {\n final hasBundleAssets = await _hasRequiredBundleAssets(\n platformKey,\n spec,\n );\n _diag(\n 'status.bundle-assets',\n data: {\n 'platformKey': platformKey,\n 'hasBundleAssets': hasBundleAssets,\n 'nodeAsset': 'assets/openclaw/$platformKey/${spec.nodeExecutable}',\n 'cliAsset': 'assets/openclaw/$platformKey/${spec.cliEntrypoint}',\n },\n );\n return _remember(\n OpenClawRuntimeStatus(\n state: hasBundleAssets\n ? OpenClawRuntimeState.stopped\n : OpenClawRuntimeState.notBundled,\n message: hasBundleAssets\n ? '内置 OpenClaw 已随 App 打包,尚未释放或启动'\n : '当前 App 包未包含 $platformKey 的 OpenClaw runtime',\n port: port,\n platformKey: platformKey,\n runtimePath: runtimeDir.path,\n checkedAt: DateTime.now(),\n ),\n );\n }\n\n if (probe) {\n final healthy = await _probe(\n port,\n await AppSettings.getOpenClawGatewayToken(),\n );\n if (healthy) {\n return _remember(\n OpenClawRuntimeStatus(\n state: OpenClawRuntimeState.running,\n message: '本机 OpenClaw Gateway 正在运行',\n port: port,\n platformKey: platformKey,\n runtimePath: runtimeDir.path,\n checkedAt: DateTime.now(),\n ),\n );\n }\n }\n\n return _remember(\n OpenClawRuntimeStatus(\n state: _process == null\n ? OpenClawRuntimeState.stopped\n : OpenClawRuntimeState.starting,\n message: _process == null ? '本机 OpenClaw 未启动' : '本机 OpenClaw 正在启动',\n port: port,\n platformKey: platformKey,\n runtimePath: runtimeDir.path,\n checkedAt: DateTime.now(),\n ),\n );\n } catch (error) {\n _diag('status.error', error: error);\n return _remember(\n OpenClawRuntimeStatus(\n state: OpenClawRuntimeState.failed,\n message: error.toString(),\n port: port,\n platformKey: platformKey,\n checkedAt: DateTime.now(),\n ),\n );\n }\n }\n\n Future<OpenClawGatewayTarget> ensureStarted({\n String? authToken,\n String? username,\n bool isMember = false,\n }) {\n _starting ??= _ensureStartedInternal(\n authToken: authToken,\n username: username,\n isMember: isMember,\n ).whenComplete(() => _starting = null);\n return _starting!;\n }\n\n Future<OpenClawRuntimeStatus> restart() async {\n await stop();\n try {\n await ensureStarted();\n return getStatus();\n } catch (error) {\n return _remember(\n OpenClawRuntimeStatus(\n state: OpenClawRuntimeState.failed,\n message: error.toString(),\n port: await AppSettings.getOpenClawGatewayPort(),\n platformKey: _platformKey,\n checkedAt: DateTime.now(),\n ),\n );\n }\n }\n\n Future<void> stop() async {\n final process = _process;\n _process = null;\n if (process == null) return;\n process.kill();\n try {\n await process.exitCode.timeout(const Duration(seconds: 5));\n } catch (_) {\n process.kill();\n }\n }\n\n Future<OpenClawGatewayTarget> _ensureStartedInternal({\n String? authToken,\n String? username,\n bool isMember = false,\n }) async {\n final platformKey = _platformKey;\n _diag('ensure-start.start', data: {'platformKey': platformKey});\n if (platformKey == null) {\n throw const OpenClawRuntimeException('当前平台不支持内置 OpenClaw Gateway');\n }\n\n final spec = await _loadSpec(platformKey);\n final port = await AppSettings.getOpenClawGatewayPort(\n defaultValue: spec.defaultPort,\n );\n final token = await AppSettings.getOpenClawGatewayToken();\n final model = await AppSettings.getOpenClawModel(\n defaultValue: spec.defaultModel,\n );\n final modelOverride = await AppSettings.getOpenClawModelOverride(\n defaultValue: spec.defaultModelOverride ?? '',\n );\n final desktopTools = await _ensureDesktopTools();\n _diag(\n 'ensure-start.config',\n data: {\n 'platformKey': platformKey,\n 'port': port,\n 'model': model,\n 'modelOverrideSet': modelOverride.trim().isNotEmpty,\n 'desktopToolsUri': desktopTools.uri?.toString(),\n 'desktopToolsStatus': desktopTools.statusJson,\n },\n );\n\n if (await _probe(port, token, context: 'pre-start')) {\n _diag('ensure-start.reuse-existing', data: {'port': port});\n return OpenClawGatewayTarget(\n baseUri: Uri.parse('http://127.0.0.1:$port'),\n token: token,\n model: model,\n modelOverride: modelOverride.trim().isEmpty\n ? null\n : modelOverride.trim(),\n desktopToolsUri: desktopTools.uri,\n desktopToolsToken: desktopTools.token,\n desktopToolsStatus: desktopTools.statusJson,\n );\n }\n\n final runtimeDir = await _prepareBundle(spec, platformKey);\n final stateRoot = await _stateRoot();\n final configPath = await _ensureConfigFile(\n stateRoot: stateRoot,\n port: port,\n token: token,\n );\n final desktopToolsManifestPath = await _ensureDesktopToolsManifest(\n stateRoot: stateRoot,\n desktopTools: desktopTools,\n );\n\n final nodePath = p.join(runtimeDir.path, spec.nodeExecutable);\n final cliPath = p.join(runtimeDir.path, spec.cliEntrypoint);\n if (!await File(nodePath).exists()) {\n _diag('ensure-start.missing-node', data: {'nodePath': nodePath});\n throw OpenClawRuntimeException('OpenClaw 内置 Node 不存在: $nodePath');\n }\n if (!await File(cliPath).exists()) {\n _diag('ensure-start.missing-cli', data: {'cliPath': cliPath});\n throw OpenClawRuntimeException('OpenClaw CLI 入口不存在: $cliPath');\n }\n\n _remember(\n OpenClawRuntimeStatus(\n state: OpenClawRuntimeState.starting,\n message: '正在启动内置 OpenClaw Gateway',\n port: port,\n platformKey: platformKey,\n runtimePath: runtimeDir.path,\n checkedAt: DateTime.now(),\n ),\n );\n\n final args = <String>[\n cliPath,\n ...spec.gatewayArgs.map((arg) => arg.replaceAll('{port}', '$port')),\n ];\n _diag(\n 'process.starting',\n data: {\n 'nodePath': nodePath,\n 'args': args,\n 'workingDirectory': runtimeDir.path,\n 'configPath': configPath.path,\n 'desktopToolsManifestPath': desktopToolsManifestPath.path,\n },\n );\n\n final env = Map<String, String>.from(Platform.environment)\n ..addAll({\n 'OPENCLAW_GATEWAY_PORT': '$port',\n 'OPENCLAW_GATEWAY_BIND': 'loopback',\n 'OPENCLAW_GATEWAY_TOKEN': token,\n 'OPENCLAW_CONFIG_PATH': configPath.path,\n 'OPENCLAW_STATE_DIR': p.join(stateRoot.path, 'state'),\n 'OPENCLAW_AGENT_DIR': p.join(stateRoot.path, 'agents'),\n 'OPENCLAW_WORKSPACE': p.join(stateRoot.path, 'workspace'),\n 'DACHENG_DESKTOP_TOOLS_ENABLED': desktopTools.uri == null ? '0' : '1',\n 'DACHENG_DESKTOP_TOOLS_MANIFEST': desktopToolsManifestPath.path,\n if (desktopTools.uri != null)\n 'DACHENG_DESKTOP_TOOLS_URL': desktopTools.uri.toString(),\n if (desktopTools.token != null)\n 'DACHENG_DESKTOP_TOOLS_TOKEN': desktopTools.token!,\n 'DACHENG_APP_RUNTIME': '1',\n if (authToken != null && authToken.isNotEmpty)\n 'DACHENG_AUTH_TOKEN': authToken,\n if (username != null && username.isNotEmpty)\n 'DACHENG_USERNAME': username,\n 'DACHENG_IS_MEMBER': isMember ? '1' : '0',\n });\n\n _process = await Process.start(\n nodePath,\n args,\n workingDirectory: runtimeDir.path,\n environment: env,\n mode: ProcessStartMode.normal,\n runInShell: false,\n );\n _captureLogs(_process!);\n _diag('process.started', data: {'pid': _process!.pid, 'port': port});\n\n final deadline = DateTime.now().add(_startupTimeout);\n while (DateTime.now().isBefore(deadline)) {\n if (await _probe(port, token, context: 'startup')) {\n _diag('process.ready', data: {'pid': _process?.pid, 'port': port});\n _remember(\n OpenClawRuntimeStatus(\n state: OpenClawRuntimeState.running,\n message: '本机 OpenClaw Gateway 已启动',\n port: port,\n platformKey: platformKey,\n runtimePath: runtimeDir.path,\n desktopToolsStatus: desktopTools.statusJson,\n checkedAt: DateTime.now(),\n ),\n );\n return OpenClawGatewayTarget(\n baseUri: Uri.parse('http://127.0.0.1:$port'),\n token: token,\n model: model,\n modelOverride: modelOverride.trim().isEmpty\n ? null\n : modelOverride.trim(),\n desktopToolsUri: desktopTools.uri,\n desktopToolsToken: desktopTools.token,\n desktopToolsStatus: desktopTools.statusJson,\n );\n }\n\n if (_process != null) {\n final exited = await _process!.exitCode.timeout(\n const Duration(milliseconds: 1),\n onTimeout: () => -999999,\n );\n if (exited != -999999) {\n final logs = _recentLogs.take(12).join('\\n');\n _diag(\n 'process.exited-early',\n data: {\n 'exitCode': exited,\n 'recentLogs': _recentLogs.take(12).toList(),\n },\n );\n throw OpenClawRuntimeException(\n 'OpenClaw Gateway 提前退出,exitCode=$exited${logs.isEmpty ? '' : '\\n$logs'}',\n );\n }\n }\n await Future<void>.delayed(const Duration(milliseconds: 450));\n }\n\n final logs = _recentLogs.take(12).join('\\n');\n _diag(\n 'process.startup-timeout',\n data: {\n 'timeoutSeconds': _startupTimeout.inSeconds,\n 'recentLogs': _recentLogs.take(12).toList(),\n },\n );\n throw OpenClawRuntimeException(\n 'OpenClaw Gateway 启动超时${logs.isEmpty ? '' : '\\n$logs'}',\n );\n }\n\n Future<_OpenClawBundleSpec> _loadSpec(String platformKey) async {\n final raw = await rootBundle.loadString(_manifestAsset);\n final decoded = jsonDecode(raw) as Map<String, dynamic>;\n final platforms = Map<String, dynamic>.from(\n decoded['platforms'] as Map? ?? const {},\n );\n final platform = Map<String, dynamic>.from(\n platforms[platformKey] as Map? ?? const {},\n );\n if (platform.isEmpty) {\n throw OpenClawRuntimeException('bundle_manifest.json 缺少 $platformKey 配置');\n }\n\n final gatewayArgs =\n (platform['gatewayArgs'] as List? ??\n decoded['gatewayArgs'] as List? ??\n const ['gateway', '--port', '{port}'])\n .map((item) => item.toString())\n .toList();\n\n final spec = _OpenClawBundleSpec(\n version: (decoded['version'] ?? 'dev').toString(),\n defaultPort: _readInt(decoded['defaultPort']) ?? 18789,\n defaultModel: (decoded['defaultModel'] ?? 'openclaw/default').toString(),\n defaultModelOverride: decoded['defaultModelOverride']?.toString(),\n nodeExecutable: (platform['nodeExecutable'] ?? 'node/bin/node')\n .toString(),\n cliEntrypoint: (platform['cliEntrypoint'] ?? 'openclaw/bin/openclaw.js')\n .toString(),\n gatewayArgs: gatewayArgs,\n );\n _diag(\n 'manifest.loaded',\n data: {\n 'platformKey': platformKey,\n 'version': spec.version,\n 'defaultPort': spec.defaultPort,\n 'defaultModel': spec.defaultModel,\n 'nodeExecutable': spec.nodeExecutable,\n 'cliEntrypoint': spec.cliEntrypoint,\n 'gatewayArgs': spec.gatewayArgs,\n },\n );\n return spec;\n }\n\n Future<Directory> _prepareBundle(\n _OpenClawBundleSpec spec,\n String platformKey,\n ) async {\n final runtimeDir = await _runtimeDir(spec, platformKey);\n final marker = File(p.join(runtimeDir.path, '.bundle_ready'));\n final nodePath = File(p.join(runtimeDir.path, spec.nodeExecutable));\n final cliPath = File(p.join(runtimeDir.path, spec.cliEntrypoint));\n\n if (await marker.exists() &&\n await nodePath.exists() &&\n await cliPath.exists()) {\n _diag(\n 'bundle.prepare.cached',\n data: {\n 'platformKey': platformKey,\n 'runtimeDir': runtimeDir.path,\n 'marker': marker.path,\n },\n );\n return runtimeDir;\n }\n\n final prefix = 'assets/openclaw/$platformKey/';\n final assets = await _listAssets(prefix);\n final hasNodeAsset = assets.contains('$prefix${spec.nodeExecutable}');\n final hasCliAsset = assets.contains('$prefix${spec.cliEntrypoint}');\n _diag(\n 'bundle.prepare.assets',\n data: {\n 'platformKey': platformKey,\n 'prefix': prefix,\n 'assetCount': assets.length,\n 'hasNodeAsset': hasNodeAsset,\n 'hasCliAsset': hasCliAsset,\n 'nodeAsset': '$prefix${spec.nodeExecutable}',\n 'cliAsset': '$prefix${spec.cliEntrypoint}',\n 'sample': assets.take(8).toList(),\n },\n );\n if (!hasNodeAsset || !hasCliAsset) {\n throw OpenClawRuntimeException(\n '当前安装包没有内置 $platformKey 的 OpenClaw runtime。请在 release 构建中运行 scripts/build_openclaw_desktop_bundle.sh 后再打包。',\n );\n }\n\n if (await runtimeDir.exists()) {\n _diag('bundle.prepare.remove-old', data: {'runtimeDir': runtimeDir.path});\n await runtimeDir.delete(recursive: true);\n }\n await runtimeDir.create(recursive: true);\n\n for (final asset in assets) {\n final relative = asset.substring(prefix.length);\n if (relative.isEmpty || relative.endsWith('/')) continue;\n final bytes = await rootBundle.load(asset);\n final file = File(p.join(runtimeDir.path, relative));\n await file.parent.create(recursive: true);\n await file.writeAsBytes(\n bytes.buffer.asUint8List(bytes.offsetInBytes, bytes.lengthInBytes),\n flush: false,\n );\n }\n\n if (!Platform.isWindows) {\n await Process.run('chmod', ['+x', nodePath.path]);\n }\n await marker.writeAsString(DateTime.now().toIso8601String());\n _diag(\n 'bundle.prepare.complete',\n data: {\n 'platformKey': platformKey,\n 'runtimeDir': runtimeDir.path,\n 'assetCount': assets.length,\n 'nodeExists': await nodePath.exists(),\n 'nodeSize': await nodePath.exists() ? await nodePath.length() : 0,\n 'cliExists': await cliPath.exists(),\n 'cliSize': await cliPath.exists() ? await cliPath.length() : 0,\n },\n );\n return runtimeDir;\n }\n\n Future<bool> _hasRequiredBundleAssets(\n String platformKey,\n _OpenClawBundleSpec spec,\n ) async {\n final prefix = 'assets/openclaw/$platformKey/';\n final assets = await _listAssets(prefix);\n final hasRequired =\n assets.contains('$prefix${spec.nodeExecutable}') &&\n assets.contains('$prefix${spec.cliEntrypoint}');\n _diag(\n 'bundle.has-required-assets',\n data: {\n 'platformKey': platformKey,\n 'assetCount': assets.length,\n 'hasRequired': hasRequired,\n },\n );\n return hasRequired;\n }\n\n Future<List<String>> _listAssets(String prefix) async {\n final assets = <String>{};\n var jsonCount = 0;\n var binCount = 0;\n var indexCount = 0;\n final failures = <String>[];\n\n try {\n final raw = await rootBundle.loadString('AssetManifest.json');\n final decoded = jsonDecode(raw) as Map<String, dynamic>;\n final items = decoded.keys\n .where((key) => key.startsWith(prefix))\n .toList();\n jsonCount = items.length;\n assets.addAll(items);\n } catch (error) {\n failures.add('AssetManifest.json: $error');\n // Newer Flutter release builds may only include AssetManifest.bin.\n }\n\n try {\n final manifest = await AssetManifest.loadFromAssetBundle(rootBundle);\n final items = manifest\n .listAssets()\n .where((key) => key.startsWith(prefix))\n .toList();\n binCount = items.length;\n assets.addAll(items);\n } catch (error) {\n failures.add('AssetManifest.bin: $error');\n // App Store archives patched after flutter build carry a JSON/index file.\n }\n\n try {\n final raw = await rootBundle.loadString(\n 'assets/openclaw/asset_index.json',\n );\n final decoded = jsonDecode(raw);\n final indexedAssets = decoded is Map\n ? decoded['assets']\n : decoded is List\n ? decoded\n : const [];\n if (indexedAssets is List) {\n final items = indexedAssets\n .map((item) => item.toString())\n .where((key) => key.startsWith(prefix))\n .toList();\n indexCount = items.length;\n assets.addAll(items);\n }\n } catch (error) {\n failures.add('asset_index.json: $error');\n // Older builds do not have an OpenClaw asset index.\n }\n\n final sortedAssets = assets.toList()..sort();\n _diag(\n 'asset.list',\n data: {\n 'prefix': prefix,\n 'total': sortedAssets.length,\n 'jsonCount': jsonCount,\n 'binCount': binCount,\n 'indexCount': indexCount,\n if (failures.isNotEmpty) 'failures': failures,\n 'sample': sortedAssets.take(8).toList(),\n },\n );\n return sortedAssets;\n }\n\n Future<bool> _probe(\n int port,\n String token, {\n String context = 'probe',\n }) async {\n try {\n final modelsResponse = await http\n .get(\n Uri.parse('http://127.0.0.1:$port/v1/models'),\n headers: {'Authorization': 'Bearer $token'},\n )\n .timeout(_probeTimeout);\n if (modelsResponse.statusCode < 200 || modelsResponse.statusCode >= 300) {\n _diag(\n 'probe.models-unhealthy',\n data: {\n 'context': context,\n 'port': port,\n 'statusCode': modelsResponse.statusCode,\n },\n );\n return false;\n }\n\n // `/v1/models` can be healthy while `/v1/chat/completions` is disabled\n // by an old config. The home screen uses chat completions, so treat 404\n // or auth failures as an unhealthy embedded runtime and restart it with\n // the repaired config below. A 4xx validation error is acceptable here:\n // it proves the OpenAI-compatible chat route is wired.\n final chatResponse = await http\n .post(\n Uri.parse('http://127.0.0.1:$port/v1/chat/completions'),\n headers: {\n 'Authorization': 'Bearer $token',\n 'Content-Type': 'application/json',\n },\n body: '{}',\n )\n .timeout(_probeTimeout);\n final healthy =\n chatResponse.statusCode != 401 &&\n chatResponse.statusCode != 403 &&\n chatResponse.statusCode != 404;\n _diag(\n healthy ? 'probe.healthy' : 'probe.chat-unhealthy',\n data: {\n 'context': context,\n 'port': port,\n 'chatStatusCode': chatResponse.statusCode,\n },\n );\n return healthy;\n } catch (error) {\n if (context != 'startup') {\n _diag(\n 'probe.error',\n data: {'context': context, 'port': port},\n error: error,\n );\n }\n return false;\n }\n }\n\n Future<File> _ensureConfigFile({\n required Directory stateRoot,\n required int port,\n required String token,\n }) async {\n final configDir = Directory(p.join(stateRoot.path, 'config'));\n await configDir.create(recursive: true);\n final workspace = Directory(p.join(stateRoot.path, 'workspace'));\n await workspace.create(recursive: true);\n final configPath = File(p.join(configDir.path, 'openclaw.json'));\n\n final config = <String, dynamic>{\n 'gateway': {\n 'mode': 'local',\n 'port': port,\n 'bind': 'loopback',\n 'auth': {'mode': 'token', 'token': token},\n 'http': {\n 'endpoints': {\n 'chatCompletions': {'enabled': true},\n 'responses': {'enabled': true},\n },\n },\n },\n 'agents': {\n 'defaults': {'workspace': workspace.path},\n 'list': [\n {'id': 'dacheng', 'default': true, 'workspace': workspace.path},\n ],\n },\n };\n\n // Repair older embedded configs in-place. OpenClaw 2026.6 requires\n // gateway.mode=local, and the home screen requires the OpenAI-compatible\n // chat endpoint. Preserve unrelated user edits while fixing the fields\n // needed for the app-owned embedded gateway to start reliably.\n final merged = await _mergeEmbeddedConfig(configPath, config);\n await configPath.writeAsString(\n const JsonEncoder.withIndent(' ').convert(merged),\n );\n _diag(\n 'config.written',\n data: {\n 'configPath': configPath.path,\n 'port': port,\n 'gatewayMode': _mutableMap(merged['gateway'])['mode'],\n },\n );\n return configPath;\n }\n\n Future<File> _ensureDesktopToolsManifest({\n required Directory stateRoot,\n required _DesktopToolsLaunch desktopTools,\n }) async {\n final configDir = Directory(p.join(stateRoot.path, 'config'));\n await configDir.create(recursive: true);\n final manifestPath = File(p.join(configDir.path, 'desktop_tools.json'));\n final tools =\n DesktopControlPolicy.supportedTools\n .map(\n (name) => {\n 'name': name,\n 'endpoint': '/v1/tools/execute',\n 'readOnly': DesktopControlPolicy.isReadOnly(name),\n 'requiresConfirmation':\n DesktopControlPolicy.requiresConfirmation(name),\n },\n )\n .toList()\n ..sort(\n (a, b) => a['name'].toString().compareTo(b['name'].toString()),\n );\n\n await manifestPath.writeAsString(\n const JsonEncoder.withIndent(' ').convert({\n 'version': 1,\n 'transport': 'http-loopback',\n 'baseUrl': desktopTools.uri?.toString(),\n 'auth': {'type': 'bearer', 'env': 'DACHENG_DESKTOP_TOOLS_TOKEN'},\n 'status': desktopTools.statusJson,\n 'tools': tools,\n }),\n );\n _diag(\n 'desktop-tools.manifest-written',\n data: {\n 'manifestPath': manifestPath.path,\n 'hasBaseUrl': desktopTools.uri != null,\n },\n );\n return manifestPath;\n }\n\n Future<Map<String, dynamic>> _mergeEmbeddedConfig(\n File configPath,\n Map<String, dynamic> defaults,\n ) async {\n Map<String, dynamic> current = <String, dynamic>{};\n if (await configPath.exists()) {\n try {\n final decoded = jsonDecode(await configPath.readAsString());\n if (decoded is Map<String, dynamic>) {\n current = decoded;\n } else if (decoded is Map) {\n current = Map<String, dynamic>.from(decoded);\n }\n } catch (_) {\n current = <String, dynamic>{};\n }\n }\n\n final gateway = _mutableMap(current['gateway']);\n final defaultGateway = _mutableMap(defaults['gateway']);\n gateway['mode'] = 'local';\n gateway['port'] = defaultGateway['port'];\n gateway['bind'] = defaultGateway['bind'];\n gateway['auth'] = Map<String, dynamic>.from(defaultGateway['auth'] as Map);\n\n final gatewayHttp = _mutableMap(gateway['http']);\n final endpoints = _mutableMap(gatewayHttp['endpoints']);\n endpoints['chatCompletions'] = {'enabled': true};\n endpoints['responses'] = {'enabled': true};\n gatewayHttp['endpoints'] = endpoints;\n gateway['http'] = gatewayHttp;\n current['gateway'] = gateway;\n\n final agents = _mutableMap(current['agents']);\n final defaultAgents = _mutableMap(defaults['agents']);\n agents['defaults'] ??= defaultAgents['defaults'];\n agents['list'] ??= defaultAgents['list'];\n current['agents'] = agents;\n\n return current;\n }\n\n Map<String, dynamic> _mutableMap(Object? value) {\n if (value is Map<String, dynamic>) return Map<String, dynamic>.from(value);\n if (value is Map) return Map<String, dynamic>.from(value);\n return <String, dynamic>{};\n }\n\n void _captureLogs(Process process) {\n void addLog(String line) {\n final text = line.trim();\n if (text.isEmpty) return;\n _recentLogs.insert(0, text);\n if (_recentLogs.length > 80) {\n _recentLogs.removeRange(80, _recentLogs.length);\n }\n if (kDebugMode) {\n debugPrint('[OpenClaw] $text');\n }\n _diag('process.output', data: {'line': text});\n }\n\n process.stdout\n .transform(utf8.decoder)\n .transform(const LineSplitter())\n .listen(addLog, onError: (_) {});\n process.stderr\n .transform(utf8.decoder)\n .transform(const LineSplitter())\n .listen(addLog, onError: (_) {});\n }\n\n Future<Directory> _stateRoot() async {\n final support = await getApplicationSupportDirectory();\n final dir = Directory(p.join(support.path, 'openclaw_embedded'));\n await dir.create(recursive: true);\n return dir;\n }\n\n Future<_DesktopToolsLaunch> _ensureDesktopTools() async {\n try {\n final status = await DesktopControlBridge.instance.ensureStarted();\n _diag(\n 'desktop-tools.started',\n data: {\n 'bridgeRunning': status.bridgeRunning,\n 'bridgeUri': status.bridgeUri?.toString(),\n 'message': status.message,\n },\n );\n return _DesktopToolsLaunch(\n uri: status.bridgeUri,\n token: await DesktopControlBridge.instance.bridgeToken,\n statusJson: status.toJson(),\n );\n } catch (error) {\n _diag('desktop-tools.error', error: error);\n return _DesktopToolsLaunch(\n uri: null,\n token: null,\n statusJson: {\n 'enabledByBuild': false,\n 'bridgeRunning': false,\n 'message': error.toString(),\n },\n );\n }\n }\n\n Future<Directory> _runtimeDir(\n _OpenClawBundleSpec spec,\n String platformKey,\n ) async {\n final support = await getApplicationSupportDirectory();\n return Directory(\n p.join(\n support.path,\n 'openclaw_embedded',\n 'runtime',\n spec.version,\n platformKey,\n ),\n );\n }\n\n OpenClawRuntimeStatus _remember(OpenClawRuntimeStatus status) {\n _lastStatus = status;\n return status;\n }\n\n String? get _platformKey {\n final abi = Abi.current().toString().toLowerCase();\n if (Platform.isMacOS) {\n return abi.contains('arm64') ? 'macos-arm64' : 'macos-x64';\n }\n if (Platform.isWindows) {\n return abi.contains('arm64') ? 'windows-arm64' : 'windows-x64';\n }\n if (Platform.isLinux) {\n return abi.contains('arm64') ? 'linux-arm64' : 'linux-x64';\n }\n return null;\n }\n\n OpenClawRuntimeStatus? get lastStatus => _lastStatus;\n\n void _diag(\n String message, {\n Map<String, Object?> data = const {},\n Object? error,\n StackTrace? stackTrace,\n }) {\n unawaited(\n DiagnosticLogService.instance.log(\n 'openclaw.runtime',\n message,\n data: data,\n error: error,\n stackTrace: stackTrace,\n ),\n );\n }\n}\n\nint? _readInt(Object? value) {\n if (value is int) return value;\n if (value is num) return value.toInt();\n if (value is String) return int.tryParse(value);\n return null;\n}\n"}, "396ea14f4d366b9dd7a70d88cfb5f4ade4e1a9f6": {"t": 1785757970.1359787, "n": 25169, "h": "9766384999225f66", "c": "import 'dart:async';\nimport 'dart:convert';\nimport 'dart:ffi' show Abi;\nimport 'dart:io';\nimport 'package:flutter/foundation.dart';\nimport 'package:flutter/services.dart';\nimport 'package:http/http.dart' as http;\nimport 'package:path/path.dart' as p;\nimport 'package:path_provider/path_provider.dart';\n\nimport '../app_settings.dart';\nimport '../desktop_control/desktop_control_bridge.dart';\nimport '../desktop_control/desktop_control_policy.dart';\n\nenum OpenClawRuntimeState {\n unsupported,\n notBundled,\n stopped,\n starting,\n running,\n failed,\n}\n\nclass OpenClawRuntimeStatus {\n final OpenClawRuntimeState state;\n final String message;\n final int? port;\n final String? platformKey;\n final String? runtimePath;\n final Map<String, dynamic>? desktopToolsStatus;\n final DateTime checkedAt;\n\n const OpenClawRuntimeStatus({\n required this.state,\n required this.message,\n this.port,\n this.platformKey,\n this.runtimePath,\n this.desktopToolsStatus,\n required this.checkedAt,\n });\n\n bool get isHealthy => state == OpenClawRuntimeState.running;\n\n String get label {\n switch (state) {\n case OpenClawRuntimeState.unsupported:\n return '不支持';\n case OpenClawRuntimeState.notBundled:\n return '未内置';\n case OpenClawRuntimeState.stopped:\n return '未启动';\n case OpenClawRuntimeState.starting:\n return '启动中';\n case OpenClawRuntimeState.running:\n return '运行中';\n case OpenClawRuntimeState.failed:\n return '异常';\n }\n }\n}\n\nclass OpenClawGatewayTarget {\n final Uri baseUri;\n final String token;\n final String model;\n final String? modelOverride;\n final Uri? desktopToolsUri;\n final String? desktopToolsToken;\n final Map<String, dynamic>? desktopToolsStatus;\n\n const OpenClawGatewayTarget({\n required this.baseUri,\n required this.token,\n required this.model,\n this.modelOverride,\n this.desktopToolsUri,\n this.desktopToolsToken,\n this.desktopToolsStatus,\n });\n}\n\nclass _OpenClawBundleSpec {\n final String version;\n final int defaultPort;\n final String defaultModel;\n final String? defaultModelOverride;\n final String nodeExecutable;\n final String cliEntrypoint;\n final List<String> gatewayArgs;\n\n const _OpenClawBundleSpec({\n required this.version,\n required this.defaultPort,\n required this.defaultModel,\n this.defaultModelOverride,\n required this.nodeExecutable,\n required this.cliEntrypoint,\n required this.gatewayArgs,\n });\n}\n\nclass _DesktopToolsLaunch {\n final Uri? uri;\n final String? token;\n final Map<String, dynamic> statusJson;\n\n const _DesktopToolsLaunch({\n required this.uri,\n required this.token,\n required this.statusJson,\n });\n}\n\nclass OpenClawRuntimeException implements Exception {\n final String message;\n\n const OpenClawRuntimeException(this.message);\n\n @override\n String toString() => message;\n}\n\nclass OpenClawRuntime {\n OpenClawRuntime._();\n\n static final OpenClawRuntime instance = OpenClawRuntime._();\n\n static const String _manifestAsset = 'assets/openclaw/bundle_manifest.json';\n static const Duration _startupTimeout = Duration(seconds: 45);\n static const Duration _probeTimeout = Duration(seconds: 3);\n\n Process? _process;\n Future<OpenClawGatewayTarget>? _starting;\n OpenClawRuntimeStatus? _lastStatus;\n final List<String> _recentLogs = <String>[];\n\n Future<OpenClawRuntimeStatus> getStatus({bool probe = true}) async {\n final platformKey = _platformKey;\n final port = await AppSettings.getOpenClawGatewayPort();\n\n if (platformKey == null) {\n return _remember(\n OpenClawRuntimeStatus(\n state: OpenClawRuntimeState.unsupported,\n message: '当前平台不是 macOS / Windows / Linux 桌面端',\n port: port,\n checkedAt: DateTime.now(),\n ),\n );\n }\n\n try {\n final spec = await _loadSpec(platformKey);\n final runtimeDir = await _runtimeDir(spec, platformKey);\n final nodePath = p.join(runtimeDir.path, spec.nodeExecutable);\n final cliPath = p.join(runtimeDir.path, spec.cliEntrypoint);\n\n if (!await File(nodePath).exists() || !await File(cliPath).exists()) {\n final hasBundleAssets = await _hasRequiredBundleAssets(\n platformKey,\n spec,\n );\n return _remember(\n OpenClawRuntimeStatus(\n state: hasBundleAssets\n ? OpenClawRuntimeState.stopped\n : OpenClawRuntimeState.notBundled,\n message: hasBundleAssets\n ? '内置 OpenClaw 已随 App 打包,尚未释放或启动'\n : '当前 App 包未包含 $platformKey 的 OpenClaw runtime',\n port: port,\n platformKey: platformKey,\n runtimePath: runtimeDir.path,\n checkedAt: DateTime.now(),\n ),\n );\n }\n\n if (probe) {\n final healthy = await _probe(\n port,\n await AppSettings.getOpenClawGatewayToken(),\n );\n if (healthy) {\n return _remember(\n OpenClawRuntimeStatus(\n state: OpenClawRuntimeState.running,\n message: '本机 OpenClaw Gateway 正在运行',\n port: port,\n platformKey: platformKey,\n runtimePath: runtimeDir.path,\n checkedAt: DateTime.now(),\n ),\n );\n }\n }\n\n return _remember(\n OpenClawRuntimeStatus(\n state: _process == null\n ? OpenClawRuntimeState.stopped\n : OpenClawRuntimeState.starting,\n message: _process == null ? '本机 OpenClaw 未启动' : '本机 OpenClaw 正在启动',\n port: port,\n platformKey: platformKey,\n runtimePath: runtimeDir.path,\n checkedAt: DateTime.now(),\n ),\n );\n } catch (error) {\n return _remember(\n OpenClawRuntimeStatus(\n state: OpenClawRuntimeState.failed,\n message: error.toString(),\n port: port,\n platformKey: platformKey,\n checkedAt: DateTime.now(),\n ),\n );\n }\n }\n\n Future<OpenClawGatewayTarget> ensureStarted({\n String? authToken,\n String? username,\n bool isMember = false,\n }) {\n _starting ??= _ensureStartedInternal(\n authToken: authToken,\n username: username,\n isMember: isMember,\n ).whenComplete(() => _starting = null);\n return _starting!;\n }\n\n Future<OpenClawRuntimeStatus> restart() async {\n await stop();\n try {\n await ensureStarted();\n return getStatus();\n } catch (error) {\n return _remember(\n OpenClawRuntimeStatus(\n state: OpenClawRuntimeState.failed,\n message: error.toString(),\n port: await AppSettings.getOpenClawGatewayPort(),\n platformKey: _platformKey,\n checkedAt: DateTime.now(),\n ),\n );\n }\n }\n\n Future<void> stop() async {\n final process = _process;\n _process = null;\n if (process == null) return;\n process.kill();\n try {\n await process.exitCode.timeout(const Duration(seconds: 5));\n } catch (_) {\n process.kill();\n }\n }\n\n Future<OpenClawGatewayTarget> _ensureStartedInternal({\n String? authToken,\n String? username,\n bool isMember = false,\n }) async {\n final platformKey = _platformKey;\n if (platformKey == null) {\n throw const OpenClawRuntimeException('当前平台不支持内置 OpenClaw Gateway');\n }\n\n final spec = await _loadSpec(platformKey);\n final port = await AppSettings.getOpenClawGatewayPort(\n defaultValue: spec.defaultPort,\n );\n final token = await AppSettings.getOpenClawGatewayToken();\n final model = await AppSettings.getOpenClawModel(\n defaultValue: spec.defaultModel,\n );\n final modelOverride = await AppSettings.getOpenClawModelOverride(\n defaultValue: spec.defaultModelOverride ?? '',\n );\n final desktopTools = await _ensureDesktopTools();\n\n if (await _probe(port, token)) {\n return OpenClawGatewayTarget(\n baseUri: Uri.parse('http://127.0.0.1:$port'),\n token: token,\n model: model,\n modelOverride: modelOverride.trim().isEmpty\n ? null\n : modelOverride.trim(),\n desktopToolsUri: desktopTools.uri,\n desktopToolsToken: desktopTools.token,\n desktopToolsStatus: desktopTools.statusJson,\n );\n }\n\n final runtimeDir = await _prepareBundle(spec, platformKey);\n final stateRoot = await _stateRoot();\n final configPath = await _ensureConfigFile(\n stateRoot: stateRoot,\n port: port,\n token: token,\n );\n final desktopToolsManifestPath = await _ensureDesktopToolsManifest(\n stateRoot: stateRoot,\n desktopTools: desktopTools,\n );\n\n final nodePath = p.join(runtimeDir.path, spec.nodeExecutable);\n final cliPath = p.join(runtimeDir.path, spec.cliEntrypoint);\n if (!await File(nodePath).exists()) {\n throw OpenClawRuntimeException('OpenClaw 内置 Node 不存在: $nodePath');\n }\n if (!await File(cliPath).exists()) {\n throw OpenClawRuntimeException('OpenClaw CLI 入口不存在: $cliPath');\n }\n\n _remember(\n OpenClawRuntimeStatus(\n state: OpenClawRuntimeState.starting,\n message: '正在启动内置 OpenClaw Gateway',\n port: port,\n platformKey: platformKey,\n runtimePath: runtimeDir.path,\n checkedAt: DateTime.now(),\n ),\n );\n\n final args = <String>[\n cliPath,\n ...spec.gatewayArgs.map((arg) => arg.replaceAll('{port}', '$port')),\n ];\n\n final env = Map<String, String>.from(Platform.environment)\n ..addAll({\n 'OPENCLAW_GATEWAY_PORT': '$port',\n 'OPENCLAW_GATEWAY_BIND': 'loopback',\n 'OPENCLAW_GATEWAY_TOKEN': token,\n 'OPENCLAW_CONFIG_PATH': configPath.path,\n 'OPENCLAW_STATE_DIR': p.join(stateRoot.path, 'state'),\n 'OPENCLAW_AGENT_DIR': p.join(stateRoot.path, 'agents'),\n 'OPENCLAW_WORKSPACE': p.join(stateRoot.path, 'workspace'),\n 'DACHENG_DESKTOP_TOOLS_ENABLED': desktopTools.uri == null ? '0' : '1',\n 'DACHENG_DESKTOP_TOOLS_MANIFEST': desktopToolsManifestPath.path,\n if (desktopTools.uri != null)\n 'DACHENG_DESKTOP_TOOLS_URL': desktopTools.uri.toString(),\n if (desktopTools.token != null)\n 'DACHENG_DESKTOP_TOOLS_TOKEN': desktopTools.token!,\n 'DACHENG_APP_RUNTIME': '1',\n if (authToken != null && authToken.isNotEmpty)\n 'DACHENG_AUTH_TOKEN': authToken,\n if (username != null && username.isNotEmpty)\n 'DACHENG_USERNAME': username,\n 'DACHENG_IS_MEMBER': isMember ? '1' : '0',\n });\n\n _process = await Process.start(\n nodePath,\n args,\n workingDirectory: runtimeDir.path,\n environment: env,\n mode: ProcessStartMode.normal,\n runInShell: false,\n );\n _captureLogs(_process!);\n\n final deadline = DateTime.now().add(_startupTimeout);\n while (DateTime.now().isBefore(deadline)) {\n if (await _probe(port, token)) {\n _remember(\n OpenClawRuntimeStatus(\n state: OpenClawRuntimeState.running,\n message: '本机 OpenClaw Gateway 已启动',\n port: port,\n platformKey: platformKey,\n runtimePath: runtimeDir.path,\n desktopToolsStatus: desktopTools.statusJson,\n checkedAt: DateTime.now(),\n ),\n );\n return OpenClawGatewayTarget(\n baseUri: Uri.parse('http://127.0.0.1:$port'),\n token: token,\n model: model,\n modelOverride: modelOverride.trim().isEmpty\n ? null\n : modelOverride.trim(),\n desktopToolsUri: desktopTools.uri,\n desktopToolsToken: desktopTools.token,\n desktopToolsStatus: desktopTools.statusJson,\n );\n }\n\n if (_process != null) {\n final exited = await _process!.exitCode.timeout(\n const Duration(milliseconds: 1),\n onTimeout: () => -999999,\n );\n if (exited != -999999) {\n final logs = _recentLogs.take(12).join('\\n');\n throw OpenClawRuntimeException(\n 'OpenClaw Gateway 提前退出,exitCode=$exited${logs.isEmpty ? '' : '\\n$logs'}',\n );\n }\n }\n await Future<void>.delayed(const Duration(milliseconds: 450));\n }\n\n final logs = _recentLogs.take(12).join('\\n');\n throw OpenClawRuntimeException(\n 'OpenClaw Gateway 启动超时${logs.isEmpty ? '' : '\\n$logs'}',\n );\n }\n\n Future<_OpenClawBundleSpec> _loadSpec(String platformKey) async {\n final raw = await rootBundle.loadString(_manifestAsset);\n final decoded = jsonDecode(raw) as Map<String, dynamic>;\n final platforms = Map<String, dynamic>.from(\n decoded['platforms'] as Map? ?? const {},\n );\n final platform = Map<String, dynamic>.from(\n platforms[platformKey] as Map? ?? const {},\n );\n if (platform.isEmpty) {\n throw OpenClawRuntimeException('bundle_manifest.json 缺少 $platformKey 配置');\n }\n\n final gatewayArgs =\n (platform['gatewayArgs'] as List? ??\n decoded['gatewayArgs'] as List? ??\n const ['gateway', '--port', '{port}'])\n .map((item) => item.toString())\n .toList();\n\n return _OpenClawBundleSpec(\n version: (decoded['version'] ?? 'dev').toString(),\n defaultPort: _readInt(decoded['defaultPort']) ?? 18789,\n defaultModel: (decoded['defaultModel'] ?? 'openclaw/default').toString(),\n defaultModelOverride: decoded['defaultModelOverride']?.toString(),\n nodeExecutable: (platform['nodeExecutable'] ?? 'node/bin/node')\n .toString(),\n cliEntrypoint: (platform['cliEntrypoint'] ?? 'openclaw/bin/openclaw.js')\n .toString(),\n gatewayArgs: gatewayArgs,\n );\n }\n\n Future<Directory> _prepareBundle(\n _OpenClawBundleSpec spec,\n String platformKey,\n ) async {\n final runtimeDir = await _runtimeDir(spec, platformKey);\n final marker = File(p.join(runtimeDir.path, '.bundle_ready'));\n final nodePath = File(p.join(runtimeDir.path, spec.nodeExecutable));\n final cliPath = File(p.join(runtimeDir.path, spec.cliEntrypoint));\n\n if (await marker.exists() &&\n await nodePath.exists() &&\n await cliPath.exists()) {\n return runtimeDir;\n }\n\n final prefix = 'assets/openclaw/$platformKey/';\n final assets = await _listAssets(prefix);\n if (!assets.contains('$prefix${spec.nodeExecutable}') ||\n !assets.contains('$prefix${spec.cliEntrypoint}')) {\n throw OpenClawRuntimeException(\n '当前安装包没有内置 $platformKey 的 OpenClaw runtime。请在 release 构建中运行 scripts/build_openclaw_desktop_bundle.sh 后再打包。',\n );\n }\n\n if (await runtimeDir.exists()) {\n await runtimeDir.delete(recursive: true);\n }\n await runtimeDir.create(recursive: true);\n\n for (final asset in assets) {\n final relative = asset.substring(prefix.length);\n if (relative.isEmpty || relative.endsWith('/')) continue;\n final bytes = await rootBundle.load(asset);\n final file = File(p.join(runtimeDir.path, relative));\n await file.parent.create(recursive: true);\n await file.writeAsBytes(\n bytes.buffer.asUint8List(bytes.offsetInBytes, bytes.lengthInBytes),\n flush: false,\n );\n }\n\n if (!Platform.isWindows) {\n await Process.run('chmod', ['+x', nodePath.path]);\n }\n await marker.writeAsString(DateTime.now().toIso8601String());\n return runtimeDir;\n }\n\n Future<bool> _hasRequiredBundleAssets(\n String platformKey,\n _OpenClawBundleSpec spec,\n ) async {\n final prefix = 'assets/openclaw/$platformKey/';\n final assets = await _listAssets(prefix);\n return assets.contains('$prefix${spec.nodeExecutable}') &&\n assets.contains('$prefix${spec.cliEntrypoint}');\n }\n\n Future<List<String>> _listAssets(String prefix) async {\n final assets = <String>{};\n\n try {\n final raw = await rootBundle.loadString('AssetManifest.json');\n final decoded = jsonDecode(raw) as Map<String, dynamic>;\n assets.addAll(decoded.keys.where((key) => key.startsWith(prefix)));\n } catch (_) {\n // Newer Flutter release builds may only include AssetManifest.bin.\n }\n\n try {\n final manifest = await AssetManifest.loadFromAssetBundle(rootBundle);\n assets.addAll(\n manifest.listAssets().where((key) => key.startsWith(prefix)),\n );\n } catch (_) {\n // App Store archives patched after flutter build carry a JSON/index file.\n }\n\n try {\n final raw = await rootBundle.loadString(\n 'assets/openclaw/asset_index.json',\n );\n final decoded = jsonDecode(raw);\n final indexedAssets = decoded is Map\n ? decoded['assets']\n : decoded is List\n ? decoded\n : const [];\n if (indexedAssets is List) {\n assets.addAll(\n indexedAssets\n .map((item) => item.toString())\n .where((key) => key.startsWith(prefix)),\n );\n }\n } catch (_) {\n // Older builds do not have an OpenClaw asset index.\n }\n\n return assets.toList()..sort();\n }\n\n Future<bool> _probe(int port, String token) async {\n try {\n final modelsResponse = await http\n .get(\n Uri.parse('http://127.0.0.1:$port/v1/models'),\n headers: {'Authorization': 'Bearer $token'},\n )\n .timeout(_probeTimeout);\n if (modelsResponse.statusCode < 200 || modelsResponse.statusCode >= 300) {\n return false;\n }\n\n // `/v1/models` can be healthy while `/v1/chat/completions` is disabled\n // by an old config. The home screen uses chat completions, so treat 404\n // or auth failures as an unhealthy embedded runtime and restart it with\n // the repaired config below. A 4xx validation error is acceptable here:\n // it proves the OpenAI-compatible chat route is wired.\n final chatResponse = await http\n .post(\n Uri.parse('http://127.0.0.1:$port/v1/chat/completions'),\n headers: {\n 'Authorization': 'Bearer $token',\n 'Content-Type': 'application/json',\n },\n body: '{}',\n )\n .timeout(_probeTimeout);\n return chatResponse.statusCode != 401 &&\n chatResponse.statusCode != 403 &&\n chatResponse.statusCode != 404;\n } catch (_) {\n return false;\n }\n }\n\n Future<File> _ensureConfigFile({\n required Directory stateRoot,\n required int port,\n required String token,\n }) async {\n final configDir = Directory(p.join(stateRoot.path, 'config'));\n await configDir.create(recursive: true);\n final workspace = Directory(p.join(stateRoot.path, 'workspace'));\n await workspace.create(recursive: true);\n final configPath = File(p.join(configDir.path, 'openclaw.json'));\n\n final config = <String, dynamic>{\n 'gateway': {\n 'mode': 'local',\n 'port': port,\n 'bind': 'loopback',\n 'auth': {'mode': 'token', 'token': token},\n 'http': {\n 'endpoints': {\n 'chatCompletions': {'enabled': true},\n 'responses': {'enabled': true},\n },\n },\n },\n 'agents': {\n 'defaults': {'workspace': workspace.path},\n 'list': [\n {'id': 'dacheng', 'default': true, 'workspace': workspace.path},\n ],\n },\n };\n\n // Repair older embedded configs in-place. OpenClaw 2026.6 requires\n // gateway.mode=local, and the home screen requires the OpenAI-compatible\n // chat endpoint. Preserve unrelated user edits while fixing the fields\n // needed for the app-owned embedded gateway to start reliably.\n final merged = await _mergeEmbeddedConfig(configPath, config);\n await configPath.writeAsString(\n const JsonEncoder.withIndent(' ').convert(merged),\n );\n return configPath;\n }\n\n Future<File> _ensureDesktopToolsManifest({\n required Directory stateRoot,\n required _DesktopToolsLaunch desktopTools,\n }) async {\n final configDir = Directory(p.join(stateRoot.path, 'config'));\n await configDir.create(recursive: true);\n final manifestPath = File(p.join(configDir.path, 'desktop_tools.json'));\n final tools =\n DesktopControlPolicy.supportedTools\n .map(\n (name) => {\n 'name': name,\n 'endpoint': '/v1/tools/execute',\n 'readOnly': DesktopControlPolicy.isReadOnly(name),\n 'requiresConfirmation':\n DesktopControlPolicy.requiresConfirmation(name),\n },\n )\n .toList()\n ..sort(\n (a, b) => a['name'].toString().compareTo(b['name'].toString()),\n );\n\n await manifestPath.writeAsString(\n const JsonEncoder.withIndent(' ').convert({\n 'version': 1,\n 'transport': 'http-loopback',\n 'baseUrl': desktopTools.uri?.toString(),\n 'auth': {'type': 'bearer', 'env': 'DACHENG_DESKTOP_TOOLS_TOKEN'},\n 'status': desktopTools.statusJson,\n 'tools': tools,\n }),\n );\n return manifestPath;\n }\n\n Future<Map<String, dynamic>> _mergeEmbeddedConfig(\n File configPath,\n Map<String, dynamic> defaults,\n ) async {\n Map<String, dynamic> current = <String, dynamic>{};\n if (await configPath.exists()) {\n try {\n final decoded = jsonDecode(await configPath.readAsString());\n if (decoded is Map<String, dynamic>) {\n current = decoded;\n } else if (decoded is Map) {\n current = Map<String, dynamic>.from(decoded);\n }\n } catch (_) {\n current = <String, dynamic>{};\n }\n }\n\n final gateway = _mutableMap(current['gateway']);\n final defaultGateway = _mutableMap(defaults['gateway']);\n gateway['mode'] = 'local';\n gateway['port'] = defaultGateway['port'];\n gateway['bind'] = defaultGateway['bind'];\n gateway['auth'] = Map<String, dynamic>.from(defaultGateway['auth'] as Map);\n\n final gatewayHttp = _mutableMap(gateway['http']);\n final endpoints = _mutableMap(gatewayHttp['endpoints']);\n endpoints['chatCompletions'] = {'enabled': true};\n endpoints['responses'] = {'enabled': true};\n gatewayHttp['endpoints'] = endpoints;\n gateway['http'] = gatewayHttp;\n current['gateway'] = gateway;\n\n final agents = _mutableMap(current['agents']);\n final defaultAgents = _mutableMap(defaults['agents']);\n agents['defaults'] ??= defaultAgents['defaults'];\n agents['list'] ??= defaultAgents['list'];\n current['agents'] = agents;\n\n return current;\n }\n\n Map<String, dynamic> _mutableMap(Object? value) {\n if (value is Map<String, dynamic>) return Map<String, dynamic>.from(value);\n if (value is Map) return Map<String, dynamic>.from(value);\n return <String, dynamic>{};\n }\n\n void _captureLogs(Process process) {\n void addLog(String line) {\n final text = line.trim();\n if (text.isEmpty) return;\n _recentLogs.insert(0, text);\n if (_recentLogs.length > 80) {\n _recentLogs.removeRange(80, _recentLogs.length);\n }\n if (kDebugMode) {\n debugPrint('[OpenClaw] $text');\n }\n }\n\n process.stdout\n .transform(utf8.decoder)\n .transform(const LineSplitter())\n .listen(addLog, onError: (_) {});\n process.stderr\n .transform(utf8.decoder)\n .transform(const LineSplitter())\n .listen(addLog, onError: (_) {});\n }\n\n Future<Directory> _stateRoot() async {\n final support = await getApplicationSupportDirectory();\n final dir = Directory(p.join(support.path, 'openclaw_embedded'));\n await dir.create(recursive: true);\n return dir;\n }\n\n Future<_DesktopToolsLaunch> _ensureDesktopTools() async {\n try {\n final status = await DesktopControlBridge.instance.ensureStarted();\n return _DesktopToolsLaunch(\n uri: status.bridgeUri,\n token: await DesktopControlBridge.instance.bridgeToken,\n statusJson: status.toJson(),\n );\n } catch (error) {\n return _DesktopToolsLaunch(\n uri: null,\n token: null,\n statusJson: {\n 'enabledByBuild': false,\n 'bridgeRunning': false,\n 'message': error.toString(),\n },\n );\n }\n }\n\n Future<Directory> _runtimeDir(\n _OpenClawBundleSpec spec,\n String platformKey,\n ) async {\n final support = await getApplicationSupportDirectory();\n return Directory(\n p.join(\n support.path,\n 'openclaw_embedded',\n 'runtime',\n spec.version,\n platformKey,\n ),\n );\n }\n\n OpenClawRuntimeStatus _remember(OpenClawRuntimeStatus status) {\n _lastStatus = status;\n return status;\n }\n\n String? get _platformKey {\n final abi = Abi.current().toString().toLowerCase();\n if (Platform.isMacOS) {\n return abi.contains('arm64') ? 'macos-arm64' : 'macos-x64';\n }\n if (Platform.isWindows) {\n return abi.contains('arm64') ? 'windows-arm64' : 'windows-x64';\n }\n if (Platform.isLinux) {\n return abi.contains('arm64') ? 'linux-arm64' : 'linux-x64';\n }\n return null;\n }\n\n OpenClawRuntimeStatus? get lastStatus => _lastStatus;\n}\n\nint? _readInt(Object? value) {\n if (value is int) return value;\n if (value is num) return value.toInt();\n if (value is String) return int.tryParse(value);\n return null;\n}\n"}, "307982225c5260797f47a78760a75ed6e3adc541": {"t": 1785757971.8400142, "n": 24197, "h": "079299ac8498cd7d", "c": "import 'dart:async';\nimport 'dart:convert';\nimport 'dart:ffi' show Abi;\nimport 'dart:io';\nimport 'package:flutter/foundation.dart';\nimport 'package:flutter/services.dart';\nimport 'package:http/http.dart' as http;\nimport 'package:path/path.dart' as p;\nimport 'package:path_provider/path_provider.dart';\n\nimport '../app_settings.dart';\nimport '../desktop_control/desktop_control_bridge.dart';\nimport '../desktop_control/desktop_control_policy.dart';\n\nenum OpenClawRuntimeState {\n unsupported,\n notBundled,\n stopped,\n starting,\n running,\n failed,\n}\n\nclass OpenClawRuntimeStatus {\n final OpenClawRuntimeState state;\n final String message;\n final int? port;\n final String? platformKey;\n final String? runtimePath;\n final Map<String, dynamic>? desktopToolsStatus;\n final DateTime checkedAt;\n\n const OpenClawRuntimeStatus({\n required this.state,\n required this.message,\n this.port,\n this.platformKey,\n this.runtimePath,\n this.desktopToolsStatus,\n required this.checkedAt,\n });\n\n bool get isHealthy => state == OpenClawRuntimeState.running;\n\n String get label {\n switch (state) {\n case OpenClawRuntimeState.unsupported:\n return '不支持';\n case OpenClawRuntimeState.notBundled:\n return '未内置';\n case OpenClawRuntimeState.stopped:\n return '未启动';\n case OpenClawRuntimeState.starting:\n return '启动中';\n case OpenClawRuntimeState.running:\n return '运行中';\n case OpenClawRuntimeState.failed:\n return '异常';\n }\n }\n}\n\nclass OpenClawGatewayTarget {\n final Uri baseUri;\n final String token;\n final String model;\n final String? modelOverride;\n final Uri? desktopToolsUri;\n final String? desktopToolsToken;\n final Map<String, dynamic>? desktopToolsStatus;\n\n const OpenClawGatewayTarget({\n required this.baseUri,\n required this.token,\n required this.model,\n this.modelOverride,\n this.desktopToolsUri,\n this.desktopToolsToken,\n this.desktopToolsStatus,\n });\n}\n\nclass _OpenClawBundleSpec {\n final String version;\n final int defaultPort;\n final String defaultModel;\n final String? defaultModelOverride;\n final String nodeExecutable;\n final String cliEntrypoint;\n final List<String> gatewayArgs;\n\n const _OpenClawBundleSpec({\n required this.version,\n required this.defaultPort,\n required this.defaultModel,\n this.defaultModelOverride,\n required this.nodeExecutable,\n required this.cliEntrypoint,\n required this.gatewayArgs,\n });\n}\n\nclass _DesktopToolsLaunch {\n final Uri? uri;\n final String? token;\n final Map<String, dynamic> statusJson;\n\n const _DesktopToolsLaunch({\n required this.uri,\n required this.token,\n required this.statusJson,\n });\n}\n\nclass OpenClawRuntimeException implements Exception {\n final String message;\n\n const OpenClawRuntimeException(this.message);\n\n @override\n String toString() => message;\n}\n\nclass OpenClawRuntime {\n OpenClawRuntime._();\n\n static final OpenClawRuntime instance = OpenClawRuntime._();\n\n static const String _manifestAsset = 'assets/openclaw/bundle_manifest.json';\n static const Duration _startupTimeout = Duration(seconds: 45);\n static const Duration _probeTimeout = Duration(seconds: 3);\n\n Process? _process;\n Future<OpenClawGatewayTarget>? _starting;\n OpenClawRuntimeStatus? _lastStatus;\n final List<String> _recentLogs = <String>[];\n\n Future<OpenClawRuntimeStatus> getStatus({bool probe = true}) async {\n final platformKey = _platformKey;\n final port = await AppSettings.getOpenClawGatewayPort();\n\n if (platformKey == null) {\n return _remember(\n OpenClawRuntimeStatus(\n state: OpenClawRuntimeState.unsupported,\n message: '当前平台不是 macOS / Windows / Linux 桌面端',\n port: port,\n checkedAt: DateTime.now(),\n ),\n );\n }\n\n try {\n final spec = await _loadSpec(platformKey);\n final runtimeDir = await _runtimeDir(spec, platformKey);\n final nodePath = p.join(runtimeDir.path, spec.nodeExecutable);\n final cliPath = p.join(runtimeDir.path, spec.cliEntrypoint);\n\n if (!await File(nodePath).exists() || !await File(cliPath).exists()) {\n final hasBundleAssets = await _hasRequiredBundleAssets(\n platformKey,\n spec,\n );\n return _remember(\n OpenClawRuntimeStatus(\n state: hasBundleAssets\n ? OpenClawRuntimeState.stopped\n : OpenClawRuntimeState.notBundled,\n message: hasBundleAssets\n ? '内置 OpenClaw 已随 App 打包,尚未释放或启动'\n : '当前 App 包未包含 $platformKey 的 OpenClaw runtime',\n port: port,\n platformKey: platformKey,\n runtimePath: runtimeDir.path,\n checkedAt: DateTime.now(),\n ),\n );\n }\n\n if (probe) {\n final healthy = await _probe(\n port,\n await AppSettings.getOpenClawGatewayToken(),\n );\n if (healthy) {\n return _remember(\n OpenClawRuntimeStatus(\n state: OpenClawRuntimeState.running,\n message: '本机 OpenClaw Gateway 正在运行',\n port: port,\n platformKey: platformKey,\n runtimePath: runtimeDir.path,\n checkedAt: DateTime.now(),\n ),\n );\n }\n }\n\n return _remember(\n OpenClawRuntimeStatus(\n state: _process == null\n ? OpenClawRuntimeState.stopped\n : OpenClawRuntimeState.starting,\n message: _process == null ? '本机 OpenClaw 未启动' : '本机 OpenClaw 正在启动',\n port: port,\n platformKey: platformKey,\n runtimePath: runtimeDir.path,\n checkedAt: DateTime.now(),\n ),\n );\n } catch (error) {\n return _remember(\n OpenClawRuntimeStatus(\n state: OpenClawRuntimeState.failed,\n message: error.toString(),\n port: port,\n platformKey: platformKey,\n checkedAt: DateTime.now(),\n ),\n );\n }\n }\n\n Future<OpenClawGatewayTarget> ensureStarted({\n String? authToken,\n String? username,\n bool isMember = false,\n }) {\n _starting ??= _ensureStartedInternal(\n authToken: authToken,\n username: username,\n isMember: isMember,\n ).whenComplete(() => _starting = null);\n return _starting!;\n }\n\n Future<OpenClawRuntimeStatus> restart() async {\n await stop();\n try {\n await ensureStarted();\n return getStatus();\n } catch (error) {\n return _remember(\n OpenClawRuntimeStatus(\n state: OpenClawRuntimeState.failed,\n message: error.toString(),\n port: await AppSettings.getOpenClawGatewayPort(),\n platformKey: _platformKey,\n checkedAt: DateTime.now(),\n ),\n );\n }\n }\n\n Future<void> stop() async {\n final process = _process;\n _process = null;\n if (process == null) return;\n process.kill();\n try {\n await process.exitCode.timeout(const Duration(seconds: 5));\n } catch (_) {\n process.kill();\n }\n }\n\n Future<OpenClawGatewayTarget> _ensureStartedInternal({\n String? authToken,\n String? username,\n bool isMember = false,\n }) async {\n final platformKey = _platformKey;\n if (platformKey == null) {\n throw const OpenClawRuntimeException('当前平台不支持内置 OpenClaw Gateway');\n }\n\n final spec = await _loadSpec(platformKey);\n final port = await AppSettings.getOpenClawGatewayPort(\n defaultValue: spec.defaultPort,\n );\n final token = await AppSettings.getOpenClawGatewayToken();\n final model = await AppSettings.getOpenClawModel(\n defaultValue: spec.defaultModel,\n );\n final modelOverride = await AppSettings.getOpenClawModelOverride(\n defaultValue: spec.defaultModelOverride ?? '',\n );\n final desktopTools = await _ensureDesktopTools();\n\n if (await _probe(port, token)) {\n return OpenClawGatewayTarget(\n baseUri: Uri.parse('http://127.0.0.1:$port'),\n token: token,\n model: model,\n modelOverride: modelOverride.trim().isEmpty\n ? null\n : modelOverride.trim(),\n desktopToolsUri: desktopTools.uri,\n desktopToolsToken: desktopTools.token,\n desktopToolsStatus: desktopTools.statusJson,\n );\n }\n\n final runtimeDir = await _prepareBundle(spec, platformKey);\n final stateRoot = await _stateRoot();\n final configPath = await _ensureConfigFile(\n stateRoot: stateRoot,\n port: port,\n token: token,\n );\n final desktopToolsManifestPath = await _ensureDesktopToolsManifest(\n stateRoot: stateRoot,\n desktopTools: desktopTools,\n );\n\n final nodePath = p.join(runtimeDir.path, spec.nodeExecutable);\n final cliPath = p.join(runtimeDir.path, spec.cliEntrypoint);\n if (!await File(nodePath).exists()) {\n throw OpenClawRuntimeException('OpenClaw 内置 Node 不存在: $nodePath');\n }\n if (!await File(cliPath).exists()) {\n throw OpenClawRuntimeException('OpenClaw CLI 入口不存在: $cliPath');\n }\n\n _remember(\n OpenClawRuntimeStatus(\n state: OpenClawRuntimeState.starting,\n message: '正在启动内置 OpenClaw Gateway',\n port: port,\n platformKey: platformKey,\n runtimePath: runtimeDir.path,\n checkedAt: DateTime.now(),\n ),\n );\n\n final args = <String>[\n cliPath,\n ...spec.gatewayArgs.map((arg) => arg.replaceAll('{port}', '$port')),\n ];\n\n final env = Map<String, String>.from(Platform.environment)\n ..addAll({\n 'OPENCLAW_GATEWAY_PORT': '$port',\n 'OPENCLAW_GATEWAY_BIND': 'loopback',\n 'OPENCLAW_GATEWAY_TOKEN': token,\n 'OPENCLAW_CONFIG_PATH': configPath.path,\n 'OPENCLAW_STATE_DIR': p.join(stateRoot.path, 'state'),\n 'OPENCLAW_AGENT_DIR': p.join(stateRoot.path, 'agents'),\n 'OPENCLAW_WORKSPACE': p.join(stateRoot.path, 'workspace'),\n 'DACHENG_DESKTOP_TOOLS_ENABLED': desktopTools.uri == null ? '0' : '1',\n 'DACHENG_DESKTOP_TOOLS_MANIFEST': desktopToolsManifestPath.path,\n if (desktopTools.uri != null)\n 'DACHENG_DESKTOP_TOOLS_URL': desktopTools.uri.toString(),\n if (desktopTools.token != null)\n 'DACHENG_DESKTOP_TOOLS_TOKEN': desktopTools.token!,\n 'DACHENG_APP_RUNTIME': '1',\n if (authToken != null && authToken.isNotEmpty)\n 'DACHENG_AUTH_TOKEN': authToken,\n if (username != null && username.isNotEmpty)\n 'DACHENG_USERNAME': username,\n 'DACHENG_IS_MEMBER': isMember ? '1' : '0',\n });\n\n _process = await Process.start(\n nodePath,\n args,\n workingDirectory: runtimeDir.path,\n environment: env,\n mode: ProcessStartMode.normal,\n runInShell: false,\n );\n _captureLogs(_process!);\n\n final deadline = DateTime.now().add(_startupTimeout);\n while (DateTime.now().isBefore(deadline)) {\n if (await _probe(port, token)) {\n _remember(\n OpenClawRuntimeStatus(\n state: OpenClawRuntimeState.running,\n message: '本机 OpenClaw Gateway 已启动',\n port: port,\n platformKey: platformKey,\n runtimePath: runtimeDir.path,\n desktopToolsStatus: desktopTools.statusJson,\n checkedAt: DateTime.now(),\n ),\n );\n return OpenClawGatewayTarget(\n baseUri: Uri.parse('http://127.0.0.1:$port'),\n token: token,\n model: model,\n modelOverride: modelOverride.trim().isEmpty\n ? null\n : modelOverride.trim(),\n desktopToolsUri: desktopTools.uri,\n desktopToolsToken: desktopTools.token,\n desktopToolsStatus: desktopTools.statusJson,\n );\n }\n\n if (_process != null) {\n final exited = await _process!.exitCode.timeout(\n const Duration(milliseconds: 1),\n onTimeout: () => -999999,\n );\n if (exited != -999999) {\n final logs = _recentLogs.take(12).join('\\n');\n throw OpenClawRuntimeException(\n 'OpenClaw Gateway 提前退出,exitCode=$exited${logs.isEmpty ? '' : '\\n$logs'}',\n );\n }\n }\n await Future<void>.delayed(const Duration(milliseconds: 450));\n }\n\n final logs = _recentLogs.take(12).join('\\n');\n throw OpenClawRuntimeException(\n 'OpenClaw Gateway 启动超时${logs.isEmpty ? '' : '\\n$logs'}',\n );\n }\n\n Future<_OpenClawBundleSpec> _loadSpec(String platformKey) async {\n final raw = await rootBundle.loadString(_manifestAsset);\n final decoded = jsonDecode(raw) as Map<String, dynamic>;\n final platforms = Map<String, dynamic>.from(\n decoded['platforms'] as Map? ?? const {},\n );\n final platform = Map<String, dynamic>.from(\n platforms[platformKey] as Map? ?? const {},\n );\n if (platform.isEmpty) {\n throw OpenClawRuntimeException('bundle_manifest.json 缺少 $platformKey 配置');\n }\n\n final gatewayArgs =\n (platform['gatewayArgs'] as List? ??\n decoded['gatewayArgs'] as List? ??\n const ['gateway', '--port', '{port}'])\n .map((item) => item.toString())\n .toList();\n\n return _OpenClawBundleSpec(\n version: (decoded['version'] ?? 'dev').toString(),\n defaultPort: _readInt(decoded['defaultPort']) ?? 18789,\n defaultModel: (decoded['defaultModel'] ?? 'openclaw/default').toString(),\n defaultModelOverride: decoded['defaultModelOverride']?.toString(),\n nodeExecutable: (platform['nodeExecutable'] ?? 'node/bin/node')\n .toString(),\n cliEntrypoint: (platform['cliEntrypoint'] ?? 'openclaw/bin/openclaw.js')\n .toString(),\n gatewayArgs: gatewayArgs,\n );\n }\n\n Future<Directory> _prepareBundle(\n _OpenClawBundleSpec spec,\n String platformKey,\n ) async {\n final runtimeDir = await _runtimeDir(spec, platformKey);\n final marker = File(p.join(runtimeDir.path, '.bundle_ready'));\n final nodePath = File(p.join(runtimeDir.path, spec.nodeExecutable));\n final cliPath = File(p.join(runtimeDir.path, spec.cliEntrypoint));\n\n if (await marker.exists() &&\n await nodePath.exists() &&\n await cliPath.exists()) {\n return runtimeDir;\n }\n\n final prefix = 'assets/openclaw/$platformKey/';\n final assets = await _listAssets(prefix);\n if (!assets.contains('$prefix${spec.nodeExecutable}') ||\n !assets.contains('$prefix${spec.cliEntrypoint}')) {\n throw OpenClawRuntimeException(\n '当前安装包没有内置 $platformKey 的 OpenClaw runtime。请在 release 构建中运行 scripts/build_openclaw_desktop_bundle.sh 后再打包。',\n );\n }\n\n if (await runtimeDir.exists()) {\n await runtimeDir.delete(recursive: true);\n }\n await runtimeDir.create(recursive: true);\n\n for (final asset in assets) {\n final relative = asset.substring(prefix.length);\n if (relative.isEmpty || relative.endsWith('/')) continue;\n final bytes = await rootBundle.load(asset);\n final file = File(p.join(runtimeDir.path, relative));\n await file.parent.create(recursive: true);\n await file.writeAsBytes(\n bytes.buffer.asUint8List(bytes.offsetInBytes, bytes.lengthInBytes),\n flush: false,\n );\n }\n\n if (!Platform.isWindows) {\n await Process.run('chmod', ['+x', nodePath.path]);\n }\n await marker.writeAsString(DateTime.now().toIso8601String());\n return runtimeDir;\n }\n\n Future<bool> _hasRequiredBundleAssets(\n String platformKey,\n _OpenClawBundleSpec spec,\n ) async {\n final prefix = 'assets/openclaw/$platformKey/';\n final assets = await _listAssets(prefix);\n return assets.contains('$prefix${spec.nodeExecutable}') &&\n assets.contains('$prefix${spec.cliEntrypoint}');\n }\n\n Future<List<String>> _listAssets(String prefix) async {\n try {\n final raw = await rootBundle.loadString('AssetManifest.json');\n final decoded = jsonDecode(raw) as Map<String, dynamic>;\n return decoded.keys.where((key) => key.startsWith(prefix)).toList()\n ..sort();\n } catch (_) {\n return const [];\n }\n }\n\n Future<bool> _probe(int port, String token) async {\n try {\n final modelsResponse = await http\n .get(\n Uri.parse('http://127.0.0.1:$port/v1/models'),\n headers: {'Authorization': 'Bearer $token'},\n )\n .timeout(_probeTimeout);\n if (modelsResponse.statusCode < 200 || modelsResponse.statusCode >= 300) {\n return false;\n }\n\n // `/v1/models` can be healthy while `/v1/chat/completions` is disabled\n // by an old config. The home screen uses chat completions, so treat 404\n // or auth failures as an unhealthy embedded runtime and restart it with\n // the repaired config below. A 4xx validation error is acceptable here:\n // it proves the OpenAI-compatible chat route is wired.\n final chatResponse = await http\n .post(\n Uri.parse('http://127.0.0.1:$port/v1/chat/completions'),\n headers: {\n 'Authorization': 'Bearer $token',\n 'Content-Type': 'application/json',\n },\n body: '{}',\n )\n .timeout(_probeTimeout);\n return chatResponse.statusCode != 401 &&\n chatResponse.statusCode != 403 &&\n chatResponse.statusCode != 404;\n } catch (_) {\n return false;\n }\n }\n\n Future<File> _ensureConfigFile({\n required Directory stateRoot,\n required int port,\n required String token,\n }) async {\n final configDir = Directory(p.join(stateRoot.path, 'config'));\n await configDir.create(recursive: true);\n final workspace = Directory(p.join(stateRoot.path, 'workspace'));\n await workspace.create(recursive: true);\n final configPath = File(p.join(configDir.path, 'openclaw.json'));\n\n final config = <String, dynamic>{\n 'gateway': {\n 'mode': 'local',\n 'port': port,\n 'bind': 'loopback',\n 'auth': {'mode': 'token', 'token': token},\n 'http': {\n 'endpoints': {\n 'chatCompletions': {'enabled': true},\n 'responses': {'enabled': true},\n },\n },\n },\n 'agents': {\n 'defaults': {'workspace': workspace.path},\n 'list': [\n {'id': 'dacheng', 'default': true, 'workspace': workspace.path},\n ],\n },\n };\n\n // Repair older embedded configs in-place. OpenClaw 2026.6 requires\n // gateway.mode=local, and the home screen requires the OpenAI-compatible\n // chat endpoint. Preserve unrelated user edits while fixing the fields\n // needed for the app-owned embedded gateway to start reliably.\n final merged = await _mergeEmbeddedConfig(configPath, config);\n await configPath.writeAsString(\n const JsonEncoder.withIndent(' ').convert(merged),\n );\n return configPath;\n }\n\n Future<File> _ensureDesktopToolsManifest({\n required Directory stateRoot,\n required _DesktopToolsLaunch desktopTools,\n }) async {\n final configDir = Directory(p.join(stateRoot.path, 'config'));\n await configDir.create(recursive: true);\n final manifestPath = File(p.join(configDir.path, 'desktop_tools.json'));\n final tools =\n DesktopControlPolicy.supportedTools\n .map(\n (name) => {\n 'name': name,\n 'endpoint': '/v1/tools/execute',\n 'readOnly': DesktopControlPolicy.isReadOnly(name),\n 'requiresConfirmation':\n DesktopControlPolicy.requiresConfirmation(name),\n },\n )\n .toList()\n ..sort(\n (a, b) => a['name'].toString().compareTo(b['name'].toString()),\n );\n\n await manifestPath.writeAsString(\n const JsonEncoder.withIndent(' ').convert({\n 'version': 1,\n 'transport': 'http-loopback',\n 'baseUrl': desktopTools.uri?.toString(),\n 'auth': {'type': 'bearer', 'env': 'DACHENG_DESKTOP_TOOLS_TOKEN'},\n 'status': desktopTools.statusJson,\n 'tools': tools,\n }),\n );\n return manifestPath;\n }\n\n Future<Map<String, dynamic>> _mergeEmbeddedConfig(\n File configPath,\n Map<String, dynamic> defaults,\n ) async {\n Map<String, dynamic> current = <String, dynamic>{};\n if (await configPath.exists()) {\n try {\n final decoded = jsonDecode(await configPath.readAsString());\n if (decoded is Map<String, dynamic>) {\n current = decoded;\n } else if (decoded is Map) {\n current = Map<String, dynamic>.from(decoded);\n }\n } catch (_) {\n current = <String, dynamic>{};\n }\n }\n\n final gateway = _mutableMap(current['gateway']);\n final defaultGateway = _mutableMap(defaults['gateway']);\n gateway['mode'] = 'local';\n gateway['port'] = defaultGateway['port'];\n gateway['bind'] = defaultGateway['bind'];\n gateway['auth'] = Map<String, dynamic>.from(defaultGateway['auth'] as Map);\n\n final gatewayHttp = _mutableMap(gateway['http']);\n final endpoints = _mutableMap(gatewayHttp['endpoints']);\n endpoints['chatCompletions'] = {'enabled': true};\n endpoints['responses'] = {'enabled': true};\n gatewayHttp['endpoints'] = endpoints;\n gateway['http'] = gatewayHttp;\n current['gateway'] = gateway;\n\n final agents = _mutableMap(current['agents']);\n final defaultAgents = _mutableMap(defaults['agents']);\n agents['defaults'] ??= defaultAgents['defaults'];\n agents['list'] ??= defaultAgents['list'];\n current['agents'] = agents;\n\n return current;\n }\n\n Map<String, dynamic> _mutableMap(Object? value) {\n if (value is Map<String, dynamic>) return Map<String, dynamic>.from(value);\n if (value is Map) return Map<String, dynamic>.from(value);\n return <String, dynamic>{};\n }\n\n void _captureLogs(Process process) {\n void addLog(String line) {\n final text = line.trim();\n if (text.isEmpty) return;\n _recentLogs.insert(0, text);\n if (_recentLogs.length > 80) {\n _recentLogs.removeRange(80, _recentLogs.length);\n }\n if (kDebugMode) {\n debugPrint('[OpenClaw] $text');\n }\n }\n\n process.stdout\n .transform(utf8.decoder)\n .transform(const LineSplitter())\n .listen(addLog, onError: (_) {});\n process.stderr\n .transform(utf8.decoder)\n .transform(const LineSplitter())\n .listen(addLog, onError: (_) {});\n }\n\n Future<Directory> _stateRoot() async {\n final support = await getApplicationSupportDirectory();\n final dir = Directory(p.join(support.path, 'openclaw_embedded'));\n await dir.create(recursive: true);\n return dir;\n }\n\n Future<_DesktopToolsLaunch> _ensureDesktopTools() async {\n try {\n final status = await DesktopControlBridge.instance.ensureStarted();\n return _DesktopToolsLaunch(\n uri: status.bridgeUri,\n token: await DesktopControlBridge.instance.bridgeToken,\n statusJson: status.toJson(),\n );\n } catch (error) {\n return _DesktopToolsLaunch(\n uri: null,\n token: null,\n statusJson: {\n 'enabledByBuild': false,\n 'bridgeRunning': false,\n 'message': error.toString(),\n },\n );\n }\n }\n\n Future<Directory> _runtimeDir(\n _OpenClawBundleSpec spec,\n String platformKey,\n ) async {\n final support = await getApplicationSupportDirectory();\n return Directory(\n p.join(\n support.path,\n 'openclaw_embedded',\n 'runtime',\n spec.version,\n platformKey,\n ),\n );\n }\n\n OpenClawRuntimeStatus _remember(OpenClawRuntimeStatus status) {\n _lastStatus = status;\n return status;\n }\n\n String? get _platformKey {\n final abi = Abi.current().toString().toLowerCase();\n if (Platform.isMacOS) {\n return abi.contains('arm64') ? 'macos-arm64' : 'macos-x64';\n }\n if (Platform.isWindows) {\n return abi.contains('arm64') ? 'windows-arm64' : 'windows-x64';\n }\n if (Platform.isLinux) {\n return abi.contains('arm64') ? 'linux-arm64' : 'linux-x64';\n }\n return null;\n }\n\n OpenClawRuntimeStatus? get lastStatus => _lastStatus;\n}\n\nint? _readInt(Object? value) {\n if (value is int) return value;\n if (value is num) return value.toInt();\n if (value is String) return int.tryParse(value);\n return null;\n}\n"}, "05532d825da86869f401303ce62a89c8f304ab3e": {"t": 1785757973.3687246, "n": 20862, "h": "09903a1fb340f613", "c": "import 'dart:async';\nimport 'dart:convert';\nimport 'dart:ffi' show Abi;\nimport 'dart:io';\nimport 'package:flutter/foundation.dart';\nimport 'package:flutter/services.dart';\nimport 'package:http/http.dart' as http;\nimport 'package:path/path.dart' as p;\nimport 'package:path_provider/path_provider.dart';\n\nimport '../app_settings.dart';\n\nenum OpenClawRuntimeState {\n unsupported,\n notBundled,\n stopped,\n starting,\n running,\n failed,\n}\n\nclass OpenClawRuntimeStatus {\n final OpenClawRuntimeState state;\n final String message;\n final int? port;\n final String? platformKey;\n final String? runtimePath;\n final DateTime checkedAt;\n\n const OpenClawRuntimeStatus({\n required this.state,\n required this.message,\n this.port,\n this.platformKey,\n this.runtimePath,\n required this.checkedAt,\n });\n\n bool get isHealthy => state == OpenClawRuntimeState.running;\n\n String get label {\n switch (state) {\n case OpenClawRuntimeState.unsupported:\n return '不支持';\n case OpenClawRuntimeState.notBundled:\n return '未内置';\n case OpenClawRuntimeState.stopped:\n return '未启动';\n case OpenClawRuntimeState.starting:\n return '启动中';\n case OpenClawRuntimeState.running:\n return '运行中';\n case OpenClawRuntimeState.failed:\n return '异常';\n }\n }\n}\n\nclass OpenClawGatewayTarget {\n final Uri baseUri;\n final String token;\n final String model;\n final String? modelOverride;\n\n const OpenClawGatewayTarget({\n required this.baseUri,\n required this.token,\n required this.model,\n this.modelOverride,\n });\n}\n\nclass _OpenClawBundleSpec {\n final String version;\n final int defaultPort;\n final String defaultModel;\n final String? defaultModelOverride;\n final String nodeExecutable;\n final String cliEntrypoint;\n final List<String> gatewayArgs;\n\n const _OpenClawBundleSpec({\n required this.version,\n required this.defaultPort,\n required this.defaultModel,\n this.defaultModelOverride,\n required this.nodeExecutable,\n required this.cliEntrypoint,\n required this.gatewayArgs,\n });\n}\n\nclass OpenClawRuntimeException implements Exception {\n final String message;\n\n const OpenClawRuntimeException(this.message);\n\n @override\n String toString() => message;\n}\n\nclass OpenClawRuntime {\n OpenClawRuntime._();\n\n static final OpenClawRuntime instance = OpenClawRuntime._();\n\n static const String _manifestAsset = 'assets/openclaw/bundle_manifest.json';\n static const Duration _startupTimeout = Duration(seconds: 45);\n static const Duration _probeTimeout = Duration(seconds: 3);\n\n Process? _process;\n Future<OpenClawGatewayTarget>? _starting;\n OpenClawRuntimeStatus? _lastStatus;\n final List<String> _recentLogs = <String>[];\n\n Future<OpenClawRuntimeStatus> getStatus({bool probe = true}) async {\n final platformKey = _platformKey;\n final port = await AppSettings.getOpenClawGatewayPort();\n\n if (platformKey == null) {\n return _remember(\n OpenClawRuntimeStatus(\n state: OpenClawRuntimeState.unsupported,\n message: '当前平台不是 macOS / Windows / Linux 桌面端',\n port: port,\n checkedAt: DateTime.now(),\n ),\n );\n }\n\n try {\n final spec = await _loadSpec(platformKey);\n final runtimeDir = await _runtimeDir(spec, platformKey);\n final nodePath = p.join(runtimeDir.path, spec.nodeExecutable);\n final cliPath = p.join(runtimeDir.path, spec.cliEntrypoint);\n\n if (!await File(nodePath).exists() || !await File(cliPath).exists()) {\n final hasBundleAssets = await _hasRequiredBundleAssets(\n platformKey,\n spec,\n );\n return _remember(\n OpenClawRuntimeStatus(\n state: hasBundleAssets\n ? OpenClawRuntimeState.stopped\n : OpenClawRuntimeState.notBundled,\n message: hasBundleAssets\n ? '内置 OpenClaw 已随 App 打包,尚未释放或启动'\n : '当前 App 包未包含 $platformKey 的 OpenClaw runtime',\n port: port,\n platformKey: platformKey,\n runtimePath: runtimeDir.path,\n checkedAt: DateTime.now(),\n ),\n );\n }\n\n if (probe) {\n final healthy = await _probe(\n port,\n await AppSettings.getOpenClawGatewayToken(),\n );\n if (healthy) {\n return _remember(\n OpenClawRuntimeStatus(\n state: OpenClawRuntimeState.running,\n message: '本机 OpenClaw Gateway 正在运行',\n port: port,\n platformKey: platformKey,\n runtimePath: runtimeDir.path,\n checkedAt: DateTime.now(),\n ),\n );\n }\n }\n\n return _remember(\n OpenClawRuntimeStatus(\n state: _process == null\n ? OpenClawRuntimeState.stopped\n : OpenClawRuntimeState.starting,\n message: _process == null ? '本机 OpenClaw 未启动' : '本机 OpenClaw 正在启动',\n port: port,\n platformKey: platformKey,\n runtimePath: runtimeDir.path,\n checkedAt: DateTime.now(),\n ),\n );\n } catch (error) {\n return _remember(\n OpenClawRuntimeStatus(\n state: OpenClawRuntimeState.failed,\n message: error.toString(),\n port: port,\n platformKey: platformKey,\n checkedAt: DateTime.now(),\n ),\n );\n }\n }\n\n Future<OpenClawGatewayTarget> ensureStarted({\n String? authToken,\n String? username,\n bool isMember = false,\n }) {\n _starting ??= _ensureStartedInternal(\n authToken: authToken,\n username: username,\n isMember: isMember,\n ).whenComplete(() => _starting = null);\n return _starting!;\n }\n\n Future<OpenClawRuntimeStatus> restart() async {\n await stop();\n try {\n await ensureStarted();\n return getStatus();\n } catch (error) {\n return _remember(\n OpenClawRuntimeStatus(\n state: OpenClawRuntimeState.failed,\n message: error.toString(),\n port: await AppSettings.getOpenClawGatewayPort(),\n platformKey: _platformKey,\n checkedAt: DateTime.now(),\n ),\n );\n }\n }\n\n Future<void> stop() async {\n final process = _process;\n _process = null;\n if (process == null) return;\n process.kill();\n try {\n await process.exitCode.timeout(const Duration(seconds: 5));\n } catch (_) {\n process.kill();\n }\n }\n\n Future<OpenClawGatewayTarget> _ensureStartedInternal({\n String? authToken,\n String? username,\n bool isMember = false,\n }) async {\n final platformKey = _platformKey;\n if (platformKey == null) {\n throw const OpenClawRuntimeException('当前平台不支持内置 OpenClaw Gateway');\n }\n\n final spec = await _loadSpec(platformKey);\n final port = await AppSettings.getOpenClawGatewayPort(\n defaultValue: spec.defaultPort,\n );\n final token = await AppSettings.getOpenClawGatewayToken();\n final model = await AppSettings.getOpenClawModel(\n defaultValue: spec.defaultModel,\n );\n final modelOverride = await AppSettings.getOpenClawModelOverride(\n defaultValue: spec.defaultModelOverride ?? '',\n );\n\n if (await _probe(port, token)) {\n return OpenClawGatewayTarget(\n baseUri: Uri.parse('http://127.0.0.1:$port'),\n token: token,\n model: model,\n modelOverride: modelOverride.trim().isEmpty\n ? null\n : modelOverride.trim(),\n );\n }\n\n final runtimeDir = await _prepareBundle(spec, platformKey);\n final stateRoot = await _stateRoot();\n final configPath = await _ensureConfigFile(\n stateRoot: stateRoot,\n port: port,\n token: token,\n );\n\n final nodePath = p.join(runtimeDir.path, spec.nodeExecutable);\n final cliPath = p.join(runtimeDir.path, spec.cliEntrypoint);\n if (!await File(nodePath).exists()) {\n throw OpenClawRuntimeException('OpenClaw 内置 Node 不存在: $nodePath');\n }\n if (!await File(cliPath).exists()) {\n throw OpenClawRuntimeException('OpenClaw CLI 入口不存在: $cliPath');\n }\n\n _remember(\n OpenClawRuntimeStatus(\n state: OpenClawRuntimeState.starting,\n message: '正在启动内置 OpenClaw Gateway',\n port: port,\n platformKey: platformKey,\n runtimePath: runtimeDir.path,\n checkedAt: DateTime.now(),\n ),\n );\n\n final args = <String>[\n cliPath,\n ...spec.gatewayArgs.map((arg) => arg.replaceAll('{port}', '$port')),\n ];\n\n final env = Map<String, String>.from(Platform.environment)\n ..addAll({\n 'OPENCLAW_GATEWAY_PORT': '$port',\n 'OPENCLAW_GATEWAY_BIND': 'loopback',\n 'OPENCLAW_GATEWAY_TOKEN': token,\n 'OPENCLAW_CONFIG_PATH': configPath.path,\n 'OPENCLAW_STATE_DIR': p.join(stateRoot.path, 'state'),\n 'OPENCLAW_AGENT_DIR': p.join(stateRoot.path, 'agents'),\n 'OPENCLAW_WORKSPACE': p.join(stateRoot.path, 'workspace'),\n 'DACHENG_APP_RUNTIME': '1',\n if (authToken != null && authToken.isNotEmpty)\n 'DACHENG_AUTH_TOKEN': authToken,\n if (username != null && username.isNotEmpty)\n 'DACHENG_USERNAME': username,\n 'DACHENG_IS_MEMBER': isMember ? '1' : '0',\n });\n\n _process = await Process.start(\n nodePath,\n args,\n workingDirectory: runtimeDir.path,\n environment: env,\n mode: ProcessStartMode.normal,\n runInShell: false,\n );\n _captureLogs(_process!);\n\n final deadline = DateTime.now().add(_startupTimeout);\n while (DateTime.now().isBefore(deadline)) {\n if (await _probe(port, token)) {\n _remember(\n OpenClawRuntimeStatus(\n state: OpenClawRuntimeState.running,\n message: '本机 OpenClaw Gateway 已启动',\n port: port,\n platformKey: platformKey,\n runtimePath: runtimeDir.path,\n checkedAt: DateTime.now(),\n ),\n );\n return OpenClawGatewayTarget(\n baseUri: Uri.parse('http://127.0.0.1:$port'),\n token: token,\n model: model,\n modelOverride: modelOverride.trim().isEmpty\n ? null\n : modelOverride.trim(),\n );\n }\n\n if (_process != null) {\n final exited = await _process!.exitCode.timeout(\n const Duration(milliseconds: 1),\n onTimeout: () => -999999,\n );\n if (exited != -999999) {\n final logs = _recentLogs.take(12).join('\\n');\n throw OpenClawRuntimeException(\n 'OpenClaw Gateway 提前退出,exitCode=$exited${logs.isEmpty ? '' : '\\n$logs'}',\n );\n }\n }\n await Future<void>.delayed(const Duration(milliseconds: 450));\n }\n\n final logs = _recentLogs.take(12).join('\\n');\n throw OpenClawRuntimeException(\n 'OpenClaw Gateway 启动超时${logs.isEmpty ? '' : '\\n$logs'}',\n );\n }\n\n Future<_OpenClawBundleSpec> _loadSpec(String platformKey) async {\n final raw = await rootBundle.loadString(_manifestAsset);\n final decoded = jsonDecode(raw) as Map<String, dynamic>;\n final platforms = Map<String, dynamic>.from(\n decoded['platforms'] as Map? ?? const {},\n );\n final platform = Map<String, dynamic>.from(\n platforms[platformKey] as Map? ?? const {},\n );\n if (platform.isEmpty) {\n throw OpenClawRuntimeException('bundle_manifest.json 缺少 $platformKey 配置');\n }\n\n final gatewayArgs =\n (platform['gatewayArgs'] as List? ??\n decoded['gatewayArgs'] as List? ??\n const ['gateway', '--port', '{port}'])\n .map((item) => item.toString())\n .toList();\n\n return _OpenClawBundleSpec(\n version: (decoded['version'] ?? 'dev').toString(),\n defaultPort: _readInt(decoded['defaultPort']) ?? 18789,\n defaultModel: (decoded['defaultModel'] ?? 'openclaw/default').toString(),\n defaultModelOverride: decoded['defaultModelOverride']?.toString(),\n nodeExecutable: (platform['nodeExecutable'] ?? 'node/bin/node')\n .toString(),\n cliEntrypoint: (platform['cliEntrypoint'] ?? 'openclaw/bin/openclaw.js')\n .toString(),\n gatewayArgs: gatewayArgs,\n );\n }\n\n Future<Directory> _prepareBundle(\n _OpenClawBundleSpec spec,\n String platformKey,\n ) async {\n final runtimeDir = await _runtimeDir(spec, platformKey);\n final marker = File(p.join(runtimeDir.path, '.bundle_ready'));\n final nodePath = File(p.join(runtimeDir.path, spec.nodeExecutable));\n final cliPath = File(p.join(runtimeDir.path, spec.cliEntrypoint));\n\n if (await marker.exists() &&\n await nodePath.exists() &&\n await cliPath.exists()) {\n return runtimeDir;\n }\n\n final prefix = 'assets/openclaw/$platformKey/';\n final assets = await _listAssets(prefix);\n if (!assets.contains('$prefix${spec.nodeExecutable}') ||\n !assets.contains('$prefix${spec.cliEntrypoint}')) {\n throw OpenClawRuntimeException(\n '当前安装包没有内置 $platformKey 的 OpenClaw runtime。请在 release 构建中运行 scripts/build_openclaw_desktop_bundle.sh 后再打包。',\n );\n }\n\n if (await runtimeDir.exists()) {\n await runtimeDir.delete(recursive: true);\n }\n await runtimeDir.create(recursive: true);\n\n for (final asset in assets) {\n final relative = asset.substring(prefix.length);\n if (relative.isEmpty || relative.endsWith('/')) continue;\n final bytes = await rootBundle.load(asset);\n final file = File(p.join(runtimeDir.path, relative));\n await file.parent.create(recursive: true);\n await file.writeAsBytes(\n bytes.buffer.asUint8List(bytes.offsetInBytes, bytes.lengthInBytes),\n flush: false,\n );\n }\n\n if (!Platform.isWindows) {\n await Process.run('chmod', ['+x', nodePath.path]);\n }\n await marker.writeAsString(DateTime.now().toIso8601String());\n return runtimeDir;\n }\n\n Future<bool> _hasRequiredBundleAssets(\n String platformKey,\n _OpenClawBundleSpec spec,\n ) async {\n final prefix = 'assets/openclaw/$platformKey/';\n final assets = await _listAssets(prefix);\n return assets.contains('$prefix${spec.nodeExecutable}') &&\n assets.contains('$prefix${spec.cliEntrypoint}');\n }\n\n Future<List<String>> _listAssets(String prefix) async {\n try {\n final raw = await rootBundle.loadString('AssetManifest.json');\n final decoded = jsonDecode(raw) as Map<String, dynamic>;\n return decoded.keys.where((key) => key.startsWith(prefix)).toList()\n ..sort();\n } catch (_) {\n return const [];\n }\n }\n\n Future<bool> _probe(int port, String token) async {\n try {\n final modelsResponse = await http\n .get(\n Uri.parse('http://127.0.0.1:$port/v1/models'),\n headers: {'Authorization': 'Bearer $token'},\n )\n .timeout(_probeTimeout);\n if (modelsResponse.statusCode < 200 || modelsResponse.statusCode >= 300) {\n return false;\n }\n\n // `/v1/models` can be healthy while `/v1/chat/completions` is disabled\n // by an old config. The home screen uses chat completions, so treat 404\n // or auth failures as an unhealthy embedded runtime and restart it with\n // the repaired config below. A 4xx validation error is acceptable here:\n // it proves the OpenAI-compatible chat route is wired.\n final chatResponse = await http\n .post(\n Uri.parse('http://127.0.0.1:$port/v1/chat/completions'),\n headers: {\n 'Authorization': 'Bearer $token',\n 'Content-Type': 'application/json',\n },\n body: '{}',\n )\n .timeout(_probeTimeout);\n return chatResponse.statusCode != 401 &&\n chatResponse.statusCode != 403 &&\n chatResponse.statusCode != 404;\n } catch (_) {\n return false;\n }\n }\n\n Future<File> _ensureConfigFile({\n required Directory stateRoot,\n required int port,\n required String token,\n }) async {\n final configDir = Directory(p.join(stateRoot.path, 'config'));\n await configDir.create(recursive: true);\n final workspace = Directory(p.join(stateRoot.path, 'workspace'));\n await workspace.create(recursive: true);\n final configPath = File(p.join(configDir.path, 'openclaw.json'));\n\n final config = <String, dynamic>{\n 'gateway': {\n 'mode': 'local',\n 'port': port,\n 'bind': 'loopback',\n 'auth': {'mode': 'token', 'token': token},\n 'http': {\n 'endpoints': {\n 'chatCompletions': {'enabled': true},\n 'responses': {'enabled': true},\n },\n },\n },\n 'agents': {\n 'defaults': {'workspace': workspace.path},\n 'list': [\n {'id': 'dacheng', 'default': true, 'workspace': workspace.path},\n ],\n },\n };\n\n // Repair older embedded configs in-place. OpenClaw 2026.6 requires\n // gateway.mode=local, and the home screen requires the OpenAI-compatible\n // chat endpoint. Preserve unrelated user edits while fixing the fields\n // needed for the app-owned embedded gateway to start reliably.\n final merged = await _mergeEmbeddedConfig(configPath, config);\n await configPath.writeAsString(\n const JsonEncoder.withIndent(' ').convert(merged),\n );\n return configPath;\n }\n\n Future<Map<String, dynamic>> _mergeEmbeddedConfig(\n File configPath,\n Map<String, dynamic> defaults,\n ) async {\n Map<String, dynamic> current = <String, dynamic>{};\n if (await configPath.exists()) {\n try {\n final decoded = jsonDecode(await configPath.readAsString());\n if (decoded is Map<String, dynamic>) {\n current = decoded;\n } else if (decoded is Map) {\n current = Map<String, dynamic>.from(decoded);\n }\n } catch (_) {\n current = <String, dynamic>{};\n }\n }\n\n final gateway = _mutableMap(current['gateway']);\n final defaultGateway = _mutableMap(defaults['gateway']);\n gateway['mode'] = 'local';\n gateway['port'] = defaultGateway['port'];\n gateway['bind'] = defaultGateway['bind'];\n gateway['auth'] = Map<String, dynamic>.from(defaultGateway['auth'] as Map);\n\n final gatewayHttp = _mutableMap(gateway['http']);\n final endpoints = _mutableMap(gatewayHttp['endpoints']);\n endpoints['chatCompletions'] = {'enabled': true};\n endpoints['responses'] = {'enabled': true};\n gatewayHttp['endpoints'] = endpoints;\n gateway['http'] = gatewayHttp;\n current['gateway'] = gateway;\n\n final agents = _mutableMap(current['agents']);\n final defaultAgents = _mutableMap(defaults['agents']);\n agents['defaults'] ??= defaultAgents['defaults'];\n agents['list'] ??= defaultAgents['list'];\n current['agents'] = agents;\n\n return current;\n }\n\n Map<String, dynamic> _mutableMap(Object? value) {\n if (value is Map<String, dynamic>) return Map<String, dynamic>.from(value);\n if (value is Map) return Map<String, dynamic>.from(value);\n return <String, dynamic>{};\n }\n\n void _captureLogs(Process process) {\n void addLog(String line) {\n final text = line.trim();\n if (text.isEmpty) return;\n _recentLogs.insert(0, text);\n if (_recentLogs.length > 80) {\n _recentLogs.removeRange(80, _recentLogs.length);\n }\n if (kDebugMode) {\n debugPrint('[OpenClaw] $text');\n }\n }\n\n process.stdout\n .transform(utf8.decoder)\n .transform(const LineSplitter())\n .listen(addLog, onError: (_) {});\n process.stderr\n .transform(utf8.decoder)\n .transform(const LineSplitter())\n .listen(addLog, onError: (_) {});\n }\n\n Future<Directory> _stateRoot() async {\n final support = await getApplicationSupportDirectory();\n final dir = Directory(p.join(support.path, 'openclaw_embedded'));\n await dir.create(recursive: true);\n return dir;\n }\n\n Future<Directory> _runtimeDir(\n _OpenClawBundleSpec spec,\n String platformKey,\n ) async {\n final support = await getApplicationSupportDirectory();\n return Directory(\n p.join(\n support.path,\n 'openclaw_embedded',\n 'runtime',\n spec.version,\n platformKey,\n ),\n );\n }\n\n OpenClawRuntimeStatus _remember(OpenClawRuntimeStatus status) {\n _lastStatus = status;\n return status;\n }\n\n String? get _platformKey {\n final abi = Abi.current().toString().toLowerCase();\n if (Platform.isMacOS) {\n return abi.contains('arm64') ? 'macos-arm64' : 'macos-x64';\n }\n if (Platform.isWindows) {\n return abi.contains('arm64') ? 'windows-arm64' : 'windows-x64';\n }\n if (Platform.isLinux) {\n return abi.contains('arm64') ? 'linux-arm64' : 'linux-x64';\n }\n return null;\n }\n\n OpenClawRuntimeStatus? get lastStatus => _lastStatus;\n}\n\nint? _readInt(Object? value) {\n if (value is int) return value;\n if (value is num) return value.toInt();\n if (value is String) return int.tryParse(value);\n return null;\n}\n"}, "5fc26a6931e5eb3ad08e60c851fef8e5108e63b0": {"t": 1785757974.9934206, "n": 17995, "h": "f131f8775c2734c0", "c": "import 'dart:async';\nimport 'dart:convert';\nimport 'dart:ffi' show Abi;\nimport 'dart:io';\nimport 'package:flutter/foundation.dart';\nimport 'package:flutter/services.dart';\nimport 'package:http/http.dart' as http;\nimport 'package:path/path.dart' as p;\nimport 'package:path_provider/path_provider.dart';\n\nimport '../app_settings.dart';\n\nenum OpenClawRuntimeState {\n unsupported,\n notBundled,\n stopped,\n starting,\n running,\n failed,\n}\n\nclass OpenClawRuntimeStatus {\n final OpenClawRuntimeState state;\n final String message;\n final int? port;\n final String? platformKey;\n final String? runtimePath;\n final DateTime checkedAt;\n\n const OpenClawRuntimeStatus({\n required this.state,\n required this.message,\n this.port,\n this.platformKey,\n this.runtimePath,\n required this.checkedAt,\n });\n\n bool get isHealthy => state == OpenClawRuntimeState.running;\n\n String get label {\n switch (state) {\n case OpenClawRuntimeState.unsupported:\n return '不支持';\n case OpenClawRuntimeState.notBundled:\n return '未内置';\n case OpenClawRuntimeState.stopped:\n return '未启动';\n case OpenClawRuntimeState.starting:\n return '启动中';\n case OpenClawRuntimeState.running:\n return '运行中';\n case OpenClawRuntimeState.failed:\n return '异常';\n }\n }\n}\n\nclass OpenClawGatewayTarget {\n final Uri baseUri;\n final String token;\n final String model;\n final String? modelOverride;\n\n const OpenClawGatewayTarget({\n required this.baseUri,\n required this.token,\n required this.model,\n this.modelOverride,\n });\n}\n\nclass _OpenClawBundleSpec {\n final String version;\n final int defaultPort;\n final String defaultModel;\n final String? defaultModelOverride;\n final String nodeExecutable;\n final String cliEntrypoint;\n final List<String> gatewayArgs;\n\n const _OpenClawBundleSpec({\n required this.version,\n required this.defaultPort,\n required this.defaultModel,\n this.defaultModelOverride,\n required this.nodeExecutable,\n required this.cliEntrypoint,\n required this.gatewayArgs,\n });\n}\n\nclass OpenClawRuntimeException implements Exception {\n final String message;\n\n const OpenClawRuntimeException(this.message);\n\n @override\n String toString() => message;\n}\n\nclass OpenClawRuntime {\n OpenClawRuntime._();\n\n static final OpenClawRuntime instance = OpenClawRuntime._();\n\n static const String _manifestAsset = 'assets/openclaw/bundle_manifest.json';\n static const Duration _startupTimeout = Duration(seconds: 45);\n static const Duration _probeTimeout = Duration(seconds: 3);\n\n Process? _process;\n Future<OpenClawGatewayTarget>? _starting;\n OpenClawRuntimeStatus? _lastStatus;\n final List<String> _recentLogs = <String>[];\n\n Future<OpenClawRuntimeStatus> getStatus({bool probe = true}) async {\n final platformKey = _platformKey;\n final port = await AppSettings.getOpenClawGatewayPort();\n\n if (platformKey == null) {\n return _remember(\n OpenClawRuntimeStatus(\n state: OpenClawRuntimeState.unsupported,\n message: '当前平台不是 macOS / Windows / Linux 桌面端',\n port: port,\n checkedAt: DateTime.now(),\n ),\n );\n }\n\n try {\n final spec = await _loadSpec(platformKey);\n final runtimeDir = await _runtimeDir(spec, platformKey);\n final nodePath = p.join(runtimeDir.path, spec.nodeExecutable);\n final cliPath = p.join(runtimeDir.path, spec.cliEntrypoint);\n\n if (!await File(nodePath).exists() || !await File(cliPath).exists()) {\n final hasBundleAssets = await _hasRequiredBundleAssets(\n platformKey,\n spec,\n );\n return _remember(\n OpenClawRuntimeStatus(\n state: hasBundleAssets\n ? OpenClawRuntimeState.stopped\n : OpenClawRuntimeState.notBundled,\n message: hasBundleAssets\n ? '内置 OpenClaw 已随 App 打包,尚未释放或启动'\n : '当前 App 包未包含 $platformKey 的 OpenClaw runtime',\n port: port,\n platformKey: platformKey,\n runtimePath: runtimeDir.path,\n checkedAt: DateTime.now(),\n ),\n );\n }\n\n if (probe) {\n final healthy = await _probe(\n port,\n await AppSettings.getOpenClawGatewayToken(),\n );\n if (healthy) {\n return _remember(\n OpenClawRuntimeStatus(\n state: OpenClawRuntimeState.running,\n message: '本机 OpenClaw Gateway 正在运行',\n port: port,\n platformKey: platformKey,\n runtimePath: runtimeDir.path,\n checkedAt: DateTime.now(),\n ),\n );\n }\n }\n\n return _remember(\n OpenClawRuntimeStatus(\n state: _process == null\n ? OpenClawRuntimeState.stopped\n : OpenClawRuntimeState.starting,\n message: _process == null ? '本机 OpenClaw 未启动' : '本机 OpenClaw 正在启动',\n port: port,\n platformKey: platformKey,\n runtimePath: runtimeDir.path,\n checkedAt: DateTime.now(),\n ),\n );\n } catch (error) {\n return _remember(\n OpenClawRuntimeStatus(\n state: OpenClawRuntimeState.failed,\n message: error.toString(),\n port: port,\n platformKey: platformKey,\n checkedAt: DateTime.now(),\n ),\n );\n }\n }\n\n Future<OpenClawGatewayTarget> ensureStarted({\n String? authToken,\n String? username,\n bool isMember = false,\n }) {\n _starting ??= _ensureStartedInternal(\n authToken: authToken,\n username: username,\n isMember: isMember,\n ).whenComplete(() => _starting = null);\n return _starting!;\n }\n\n Future<OpenClawRuntimeStatus> restart() async {\n await stop();\n try {\n await ensureStarted();\n return getStatus();\n } catch (error) {\n return _remember(\n OpenClawRuntimeStatus(\n state: OpenClawRuntimeState.failed,\n message: error.toString(),\n port: await AppSettings.getOpenClawGatewayPort(),\n platformKey: _platformKey,\n checkedAt: DateTime.now(),\n ),\n );\n }\n }\n\n Future<void> stop() async {\n final process = _process;\n _process = null;\n if (process == null) return;\n process.kill();\n try {\n await process.exitCode.timeout(const Duration(seconds: 5));\n } catch (_) {\n process.kill(ProcessSignal.sigkill);\n }\n }\n\n Future<OpenClawGatewayTarget> _ensureStartedInternal({\n String? authToken,\n String? username,\n bool isMember = false,\n }) async {\n final platformKey = _platformKey;\n if (platformKey == null) {\n throw const OpenClawRuntimeException('当前平台不支持内置 OpenClaw Gateway');\n }\n\n final spec = await _loadSpec(platformKey);\n final port = await AppSettings.getOpenClawGatewayPort(\n defaultValue: spec.defaultPort,\n );\n final token = await AppSettings.getOpenClawGatewayToken();\n final model = await AppSettings.getOpenClawModel(\n defaultValue: spec.defaultModel,\n );\n final modelOverride = await AppSettings.getOpenClawModelOverride(\n defaultValue: spec.defaultModelOverride ?? '',\n );\n\n if (await _probe(port, token)) {\n return OpenClawGatewayTarget(\n baseUri: Uri.parse('http://127.0.0.1:$port'),\n token: token,\n model: model,\n modelOverride: modelOverride.trim().isEmpty\n ? null\n : modelOverride.trim(),\n );\n }\n\n final runtimeDir = await _prepareBundle(spec, platformKey);\n final stateRoot = await _stateRoot();\n final configPath = await _ensureConfigFile(\n stateRoot: stateRoot,\n port: port,\n );\n\n final nodePath = p.join(runtimeDir.path, spec.nodeExecutable);\n final cliPath = p.join(runtimeDir.path, spec.cliEntrypoint);\n if (!await File(nodePath).exists()) {\n throw OpenClawRuntimeException('OpenClaw 内置 Node 不存在: $nodePath');\n }\n if (!await File(cliPath).exists()) {\n throw OpenClawRuntimeException('OpenClaw CLI 入口不存在: $cliPath');\n }\n\n _remember(\n OpenClawRuntimeStatus(\n state: OpenClawRuntimeState.starting,\n message: '正在启动内置 OpenClaw Gateway',\n port: port,\n platformKey: platformKey,\n runtimePath: runtimeDir.path,\n checkedAt: DateTime.now(),\n ),\n );\n\n final args = <String>[\n cliPath,\n ...spec.gatewayArgs.map((arg) => arg.replaceAll('{port}', '$port')),\n ];\n\n final env = Map<String, String>.from(Platform.environment)\n ..addAll({\n 'OPENCLAW_GATEWAY_PORT': '$port',\n 'OPENCLAW_GATEWAY_BIND': 'loopback',\n 'OPENCLAW_GATEWAY_TOKEN': token,\n 'OPENCLAW_CONFIG_PATH': configPath.path,\n 'OPENCLAW_STATE_DIR': p.join(stateRoot.path, 'state'),\n 'OPENCLAW_AGENT_DIR': p.join(stateRoot.path, 'agents'),\n 'OPENCLAW_WORKSPACE': p.join(stateRoot.path, 'workspace'),\n 'DACHENG_APP_RUNTIME': '1',\n if (authToken != null && authToken.isNotEmpty)\n 'DACHENG_AUTH_TOKEN': authToken,\n if (username != null && username.isNotEmpty)\n 'DACHENG_USERNAME': username,\n 'DACHENG_IS_MEMBER': isMember ? '1' : '0',\n });\n\n _process = await Process.start(\n nodePath,\n args,\n workingDirectory: runtimeDir.path,\n environment: env,\n mode: ProcessStartMode.normal,\n runInShell: false,\n );\n _captureLogs(_process!);\n\n final deadline = DateTime.now().add(_startupTimeout);\n while (DateTime.now().isBefore(deadline)) {\n if (await _probe(port, token)) {\n _remember(\n OpenClawRuntimeStatus(\n state: OpenClawRuntimeState.running,\n message: '本机 OpenClaw Gateway 已启动',\n port: port,\n platformKey: platformKey,\n runtimePath: runtimeDir.path,\n checkedAt: DateTime.now(),\n ),\n );\n return OpenClawGatewayTarget(\n baseUri: Uri.parse('http://127.0.0.1:$port'),\n token: token,\n model: model,\n modelOverride: modelOverride.trim().isEmpty\n ? null\n : modelOverride.trim(),\n );\n }\n\n if (_process != null) {\n final exited = await _process!.exitCode.timeout(\n const Duration(milliseconds: 1),\n onTimeout: () => -999999,\n );\n if (exited != -999999) {\n final logs = _recentLogs.take(12).join('\\n');\n throw OpenClawRuntimeException(\n 'OpenClaw Gateway 提前退出,exitCode=$exited${logs.isEmpty ? '' : '\\n$logs'}',\n );\n }\n }\n await Future<void>.delayed(const Duration(milliseconds: 450));\n }\n\n final logs = _recentLogs.take(12).join('\\n');\n throw OpenClawRuntimeException(\n 'OpenClaw Gateway 启动超时${logs.isEmpty ? '' : '\\n$logs'}',\n );\n }\n\n Future<_OpenClawBundleSpec> _loadSpec(String platformKey) async {\n final raw = await rootBundle.loadString(_manifestAsset);\n final decoded = jsonDecode(raw) as Map<String, dynamic>;\n final platforms = Map<String, dynamic>.from(\n decoded['platforms'] as Map? ?? const {},\n );\n final platform = Map<String, dynamic>.from(\n platforms[platformKey] as Map? ?? const {},\n );\n if (platform.isEmpty) {\n throw OpenClawRuntimeException('bundle_manifest.json 缺少 $platformKey 配置');\n }\n\n final gatewayArgs =\n (platform['gatewayArgs'] as List? ??\n decoded['gatewayArgs'] as List? ??\n const ['gateway', '--port', '{port}'])\n .map((item) => item.toString())\n .toList();\n\n return _OpenClawBundleSpec(\n version: (decoded['version'] ?? 'dev').toString(),\n defaultPort: _readInt(decoded['defaultPort']) ?? 18789,\n defaultModel: (decoded['defaultModel'] ?? 'openclaw/default').toString(),\n defaultModelOverride: decoded['defaultModelOverride']?.toString(),\n nodeExecutable: (platform['nodeExecutable'] ?? 'node/bin/node')\n .toString(),\n cliEntrypoint: (platform['cliEntrypoint'] ?? 'openclaw/bin/openclaw.js')\n .toString(),\n gatewayArgs: gatewayArgs,\n );\n }\n\n Future<Directory> _prepareBundle(\n _OpenClawBundleSpec spec,\n String platformKey,\n ) async {\n final runtimeDir = await _runtimeDir(spec, platformKey);\n final marker = File(p.join(runtimeDir.path, '.bundle_ready'));\n final nodePath = File(p.join(runtimeDir.path, spec.nodeExecutable));\n final cliPath = File(p.join(runtimeDir.path, spec.cliEntrypoint));\n\n if (await marker.exists() &&\n await nodePath.exists() &&\n await cliPath.exists()) {\n return runtimeDir;\n }\n\n final prefix = 'assets/openclaw/$platformKey/';\n final assets = await _listAssets(prefix);\n if (!assets.contains('$prefix${spec.nodeExecutable}') ||\n !assets.contains('$prefix${spec.cliEntrypoint}')) {\n throw OpenClawRuntimeException(\n '当前安装包没有内置 $platformKey 的 OpenClaw runtime。请在 release 构建中运行 scripts/build_openclaw_desktop_bundle.sh 后再打包。',\n );\n }\n\n if (await runtimeDir.exists()) {\n await runtimeDir.delete(recursive: true);\n }\n await runtimeDir.create(recursive: true);\n\n for (final asset in assets) {\n final relative = asset.substring(prefix.length);\n if (relative.isEmpty || relative.endsWith('/')) continue;\n final bytes = await rootBundle.load(asset);\n final file = File(p.join(runtimeDir.path, relative));\n await file.parent.create(recursive: true);\n await file.writeAsBytes(\n bytes.buffer.asUint8List(bytes.offsetInBytes, bytes.lengthInBytes),\n flush: false,\n );\n }\n\n if (!Platform.isWindows) {\n await Process.run('chmod', ['+x', nodePath.path]);\n }\n await marker.writeAsString(DateTime.now().toIso8601String());\n return runtimeDir;\n }\n\n Future<bool> _hasRequiredBundleAssets(\n String platformKey,\n _OpenClawBundleSpec spec,\n ) async {\n final prefix = 'assets/openclaw/$platformKey/';\n final assets = await _listAssets(prefix);\n return assets.contains('$prefix${spec.nodeExecutable}') &&\n assets.contains('$prefix${spec.cliEntrypoint}');\n }\n\n Future<List<String>> _listAssets(String prefix) async {\n try {\n final raw = await rootBundle.loadString('AssetManifest.json');\n final decoded = jsonDecode(raw) as Map<String, dynamic>;\n return decoded.keys.where((key) => key.startsWith(prefix)).toList()\n ..sort();\n } catch (_) {\n return const [];\n }\n }\n\n Future<bool> _probe(int port, String token) async {\n try {\n final response = await http\n .get(\n Uri.parse('http://127.0.0.1:$port/v1/models'),\n headers: {'Authorization': 'Bearer $token'},\n )\n .timeout(_probeTimeout);\n return response.statusCode >= 200 && response.statusCode < 300;\n } catch (_) {\n return false;\n }\n }\n\n Future<File> _ensureConfigFile({\n required Directory stateRoot,\n required int port,\n }) async {\n final configDir = Directory(p.join(stateRoot.path, 'config'));\n await configDir.create(recursive: true);\n final workspace = Directory(p.join(stateRoot.path, 'workspace'));\n await workspace.create(recursive: true);\n final configPath = File(p.join(configDir.path, 'openclaw.json'));\n\n final config = <String, dynamic>{\n 'gateway': {\n 'port': port,\n 'bind': 'loopback',\n 'auth': {'mode': 'token', 'token': r'${OPENCLAW_GATEWAY_TOKEN}'},\n 'http': {\n 'endpoints': {\n 'chatCompletions': {'enabled': true},\n 'responses': {'enabled': true},\n },\n },\n },\n 'agents': {\n 'defaults': {'workspace': workspace.path},\n 'list': [\n {'id': 'dacheng', 'default': true, 'workspace': workspace.path},\n ],\n },\n };\n\n // 仅在缺失时创建默认配置,用户通过 OpenClaw Control UI 调整后的配置不被覆盖。\n if (!await configPath.exists()) {\n await configPath.writeAsString(\n const JsonEncoder.withIndent(' ').convert(config),\n );\n }\n return configPath;\n }\n\n void _captureLogs(Process process) {\n void addLog(String line) {\n final text = line.trim();\n if (text.isEmpty) return;\n _recentLogs.insert(0, text);\n if (_recentLogs.length > 80) {\n _recentLogs.removeRange(80, _recentLogs.length);\n }\n if (kDebugMode) {\n debugPrint('[OpenClaw] $text');\n }\n }\n\n process.stdout\n .transform(utf8.decoder)\n .transform(const LineSplitter())\n .listen(addLog, onError: (_) {});\n process.stderr\n .transform(utf8.decoder)\n .transform(const LineSplitter())\n .listen(addLog, onError: (_) {});\n }\n\n Future<Directory> _stateRoot() async {\n final support = await getApplicationSupportDirectory();\n final dir = Directory(p.join(support.path, 'openclaw_embedded'));\n await dir.create(recursive: true);\n return dir;\n }\n\n Future<Directory> _runtimeDir(\n _OpenClawBundleSpec spec,\n String platformKey,\n ) async {\n final support = await getApplicationSupportDirectory();\n return Directory(\n p.join(\n support.path,\n 'openclaw_embedded',\n 'runtime',\n spec.version,\n platformKey,\n ),\n );\n }\n\n OpenClawRuntimeStatus _remember(OpenClawRuntimeStatus status) {\n _lastStatus = status;\n return status;\n }\n\n String? get _platformKey {\n final abi = Abi.current().toString().toLowerCase();\n if (Platform.isMacOS) {\n return abi.contains('arm64') ? 'macos-arm64' : 'macos-x64';\n }\n if (Platform.isWindows) {\n return abi.contains('arm64') ? 'windows-arm64' : 'windows-x64';\n }\n if (Platform.isLinux) {\n return abi.contains('arm64') ? 'linux-arm64' : 'linux-x64';\n }\n return null;\n }\n\n OpenClawRuntimeStatus? get lastStatus => _lastStatus;\n}\n\nint? _readInt(Object? value) {\n if (value is int) return value;\n if (value is num) return value.toInt();\n if (value is String) return int.tryParse(value);\n return null;\n}\n"}, "33b5aa0d214c8013886ef1773c6ff1304af084f4": {"t": 1786067507.406462, "n": 97198, "h": "2aff8c1751728d25", "c": "import 'dart:async';\nimport 'dart:convert';\nimport 'dart:ffi' show Abi;\nimport 'dart:io';\nimport 'dart:math' as math;\nimport 'package:archive/archive_io.dart';\nimport 'package:convert/convert.dart';\nimport 'package:crypto/crypto.dart';\nimport 'package:flutter/foundation.dart';\nimport 'package:flutter/services.dart';\nimport 'package:http/http.dart' as http;\nimport 'package:path/path.dart' as p;\nimport 'package:path_provider/path_provider.dart';\n\nimport '../../core/config/app_config.dart';\nimport '../app_settings.dart';\nimport '../diagnostic_log_service.dart';\nimport '../desktop_control/desktop_control_bridge.dart';\nimport '../desktop_control/desktop_control_policy.dart';\nimport 'openclaw_port_resolver.dart';\n\nenum OpenClawRuntimeState {\n unsupported,\n notBundled,\n stopped,\n starting,\n running,\n failed,\n}\n\nclass OpenClawRuntimeStatus {\n final OpenClawRuntimeState state;\n final String message;\n final int? port;\n final String? platformKey;\n final String? runtimePath;\n final Map<String, dynamic>? desktopToolsStatus;\n final DateTime checkedAt;\n\n const OpenClawRuntimeStatus({\n required this.state,\n required this.message,\n this.port,\n this.platformKey,\n this.runtimePath,\n this.desktopToolsStatus,\n required this.checkedAt,\n });\n\n bool get isHealthy => state == OpenClawRuntimeState.running;\n\n String get label {\n switch (state) {\n case OpenClawRuntimeState.unsupported:\n return '不支持';\n case OpenClawRuntimeState.notBundled:\n return '未内置';\n case OpenClawRuntimeState.stopped:\n return '未启动';\n case OpenClawRuntimeState.starting:\n return '启动中';\n case OpenClawRuntimeState.running:\n return '运行中';\n case OpenClawRuntimeState.failed:\n return '异常';\n }\n }\n}\n\nclass OpenClawGatewayTarget {\n final Uri baseUri;\n final String token;\n final String model;\n final String? modelOverride;\n final Uri? desktopToolsUri;\n final String? desktopToolsToken;\n final Map<String, dynamic>? desktopToolsStatus;\n\n const OpenClawGatewayTarget({\n required this.baseUri,\n required this.token,\n required this.model,\n this.modelOverride,\n this.desktopToolsUri,\n this.desktopToolsToken,\n this.desktopToolsStatus,\n });\n}\n\nclass OpenClawCliResult {\n final List<String> args;\n final int exitCode;\n final String stdout;\n final String stderr;\n final bool timedOut;\n\n const OpenClawCliResult({\n required this.args,\n required this.exitCode,\n required this.stdout,\n required this.stderr,\n this.timedOut = false,\n });\n\n bool get succeeded => exitCode == 0 && !timedOut;\n\n String get command => 'openclaw ${args.join(' ')}';\n\n String get combinedOutput {\n final parts = <String>[\n if (stdout.trim().isNotEmpty) stdout.trimRight(),\n if (stderr.trim().isNotEmpty) stderr.trimRight(),\n ];\n return parts.join('\\n');\n }\n\n OpenClawCliResult append(OpenClawCliResult next) {\n return OpenClawCliResult(\n args: [...args, '&&', ...next.args],\n exitCode: next.exitCode,\n stdout: [\n if (stdout.trim().isNotEmpty) stdout.trimRight(),\n if (next.stdout.trim().isNotEmpty) next.stdout.trimRight(),\n ].join('\\n'),\n stderr: [\n if (stderr.trim().isNotEmpty) stderr.trimRight(),\n if (next.stderr.trim().isNotEmpty) next.stderr.trimRight(),\n ].join('\\n'),\n timedOut: timedOut || next.timedOut,\n );\n }\n}\n\nclass _OpenClawBundleSpec {\n final String version;\n final int defaultPort;\n final String defaultModel;\n final String? defaultModelOverride;\n final String nodeExecutable;\n final String cliEntrypoint;\n final List<String> gatewayArgs;\n final bool downloaded;\n\n const _OpenClawBundleSpec({\n required this.version,\n required this.defaultPort,\n required this.defaultModel,\n this.defaultModelOverride,\n required this.nodeExecutable,\n required this.cliEntrypoint,\n required this.gatewayArgs,\n this.downloaded = false,\n });\n\n Map<String, dynamic> toJson() => {\n 'schema': 1,\n 'version': version,\n 'defaultPort': defaultPort,\n 'defaultModel': defaultModel,\n if (defaultModelOverride != null)\n 'defaultModelOverride': defaultModelOverride,\n 'nodeExecutable': nodeExecutable,\n 'cliEntrypoint': cliEntrypoint,\n 'gatewayArgs': gatewayArgs,\n 'downloaded': downloaded,\n };\n}\n\nclass _OpenClawRuntimeRelease {\n final String version;\n final Uri archiveUri;\n final String sha256Hex;\n final int size;\n final int defaultPort;\n final String defaultModel;\n final String? defaultModelOverride;\n final String nodeExecutable;\n final String cliEntrypoint;\n final List<String> gatewayArgs;\n\n const _OpenClawRuntimeRelease({\n required this.version,\n required this.archiveUri,\n required this.sha256Hex,\n required this.size,\n required this.defaultPort,\n required this.defaultModel,\n this.defaultModelOverride,\n required this.nodeExecutable,\n required this.cliEntrypoint,\n required this.gatewayArgs,\n });\n\n _OpenClawBundleSpec toBundleSpec() => _OpenClawBundleSpec(\n version: version,\n defaultPort: defaultPort,\n defaultModel: defaultModel,\n defaultModelOverride: defaultModelOverride,\n nodeExecutable: nodeExecutable,\n cliEntrypoint: cliEntrypoint,\n gatewayArgs: gatewayArgs,\n downloaded: true,\n );\n}\n\nclass _DesktopToolsLaunch {\n final Uri? uri;\n final String? token;\n final Map<String, dynamic> statusJson;\n\n const _DesktopToolsLaunch({\n required this.uri,\n required this.token,\n required this.statusJson,\n });\n}\n\nclass _OpenClawCliLaunch {\n final _OpenClawBundleSpec spec;\n final Directory runtimeDir;\n final Directory stateRoot;\n final File configPath;\n final String nodePath;\n final String cliPath;\n final int port;\n final String token;\n\n const _OpenClawCliLaunch({\n required this.spec,\n required this.runtimeDir,\n required this.stateRoot,\n required this.configPath,\n required this.nodePath,\n required this.cliPath,\n required this.port,\n required this.token,\n });\n}\n\nclass OpenClawRuntimeException implements Exception {\n final String message;\n\n const OpenClawRuntimeException(this.message);\n\n @override\n String toString() => message;\n}\n\nclass OpenClawRuntime {\n OpenClawRuntime._();\n\n static final OpenClawRuntime instance = OpenClawRuntime._();\n\n static const String _manifestAsset = 'assets/openclaw/bundle_manifest.json';\n static const String _runtimeDirOverrideDefine = String.fromEnvironment(\n 'DACHENG_OPENCLAW_RUNTIME_DIR',\n );\n static const String _defaultGatewayModel =\n AppSettings.defaultOpenClawGatewayModel;\n static const String _defaultDeepSeekModel =\n AppSettings.defaultOpenClawDeepSeekModel;\n static const String _backendDeepSeekProviderId = 'dacheng-deepseek-proxy';\n static const String _desktopToolsPluginId = 'dacheng-desktop-tools';\n static const String _weChatPluginId = 'openclaw-weixin';\n static const String _weChatPluginPackage = '@tencent-weixin/openclaw-weixin';\n static const Duration _startupTimeout = Duration(seconds: 120);\n static const Duration _probeTimeout = Duration(seconds: 3);\n static const Duration _cliDefaultTimeout = Duration(seconds: 45);\n\n Process? _process;\n Future<OpenClawGatewayTarget>? _starting;\n String _processAuthToken = '';\n OpenClawRuntimeStatus? _lastStatus;\n final List<String> _recentLogs = <String>[];\n final OpenClawPortResolver _portResolver = const OpenClawPortResolver();\n\n Future<OpenClawRuntimeStatus> getStatus({bool probe = true}) async {\n final platformKey = _platformKey;\n final port = await AppSettings.getOpenClawGatewayPort();\n _diag(\n 'status.start',\n data: {'platformKey': platformKey, 'port': port, 'probe': probe},\n );\n\n if (platformKey == null) {\n return _remember(\n OpenClawRuntimeStatus(\n state: OpenClawRuntimeState.unsupported,\n message: '当前平台不是 macOS / Windows / Linux 桌面端',\n port: port,\n checkedAt: DateTime.now(),\n ),\n );\n }\n\n try {\n final spec = await _loadSpec(platformKey, checkUpdates: false);\n final overrideRuntimeDir = await _runtimeDirOverride(spec, platformKey);\n final bundledRuntimeDir = overrideRuntimeDir == null\n ? await _bundledRuntimeDir(spec, platformKey)\n : null;\n final runtimeDir =\n overrideRuntimeDir ??\n bundledRuntimeDir ??\n await _runtimeDir(spec, platformKey);\n final nodePath = p.join(runtimeDir.path, spec.nodeExecutable);\n final cliPath = p.join(runtimeDir.path, spec.cliEntrypoint);\n final nodeExists = await File(nodePath).exists();\n final cliExists = await File(cliPath).exists();\n _diag(\n 'status.paths',\n data: {\n 'platformKey': platformKey,\n 'runtimeDir': runtimeDir.path,\n 'nodePath': nodePath,\n 'nodeExists': nodeExists,\n 'cliPath': cliPath,\n 'cliExists': cliExists,\n 'runtimeSource': overrideRuntimeDir != null\n ? 'override'\n : (bundledRuntimeDir == null ? 'cache' : 'bundle'),\n },\n );\n\n if (!nodeExists || !cliExists) {\n final hasBundleAssets = await _hasRequiredBundleAssets(\n platformKey,\n spec,\n );\n _diag(\n 'status.bundle-assets',\n data: {\n 'platformKey': platformKey,\n 'hasBundleAssets': hasBundleAssets,\n 'nodeAsset': 'assets/openclaw/$platformKey/${spec.nodeExecutable}',\n 'cliAsset': 'assets/openclaw/$platformKey/${spec.cliEntrypoint}',\n },\n );\n return _remember(\n OpenClawRuntimeStatus(\n state: hasBundleAssets\n ? OpenClawRuntimeState.stopped\n : OpenClawRuntimeState.notBundled,\n message: hasBundleAssets\n ? '内置 OpenClaw 已随 App 打包,尚未释放或启动'\n : '当前 App 包未包含 $platformKey 的 OpenClaw runtime',\n port: port,\n platformKey: platformKey,\n runtimePath: runtimeDir.path,\n checkedAt: DateTime.now(),\n ),\n );\n }\n\n if (probe) {\n final healthy = await _probe(\n port,\n await AppSettings.getOpenClawGatewayToken(),\n );\n if (healthy) {\n return _remember(\n OpenClawRuntimeStatus(\n state: OpenClawRuntimeState.running,\n message: '本机 OpenClaw Gateway 正在运行',\n port: port,\n platformKey: platformKey,\n runtimePath: runtimeDir.path,\n checkedAt: DateTime.now(),\n ),\n );\n }\n }\n\n return _remember(\n OpenClawRuntimeStatus(\n state: _process == null\n ? OpenClawRuntimeState.stopped\n : OpenClawRuntimeState.starting,\n message: _process == null ? '本机 OpenClaw 未启动' : '本机 OpenClaw 正在启动',\n port: port,\n platformKey: platformKey,\n runtimePath: runtimeDir.path,\n checkedAt: DateTime.now(),\n ),\n );\n } catch (error) {\n _diag('status.error', error: error);\n return _remember(\n OpenClawRuntimeStatus(\n state: OpenClawRuntimeState.failed,\n message: error.toString(),\n port: port,\n platformKey: platformKey,\n checkedAt: DateTime.now(),\n ),\n );\n }\n }\n\n Future<OpenClawGatewayTarget> ensureStarted({\n String? authToken,\n String? username,\n bool isMember = false,\n }) {\n _starting ??= _ensureStartedInternal(\n authToken: authToken,\n username: username,\n isMember: isMember,\n ).whenComplete(() => _starting = null);\n return _starting!;\n }\n\n Future<OpenClawRuntimeStatus> restart() async {\n await stop();\n try {\n await ensureStarted();\n return getStatus();\n } catch (error) {\n return _remember(\n OpenClawRuntimeStatus(\n state: OpenClawRuntimeState.failed,\n message: error.toString(),\n port: await AppSettings.getOpenClawGatewayPort(),\n platformKey: _platformKey,\n checkedAt: DateTime.now(),\n ),\n );\n }\n }\n\n Future<void> stop() async {\n final process = _process;\n _process = null;\n _processAuthToken = '';\n if (process == null) return;\n process.kill();\n try {\n await process.exitCode.timeout(const Duration(seconds: 5));\n } catch (_) {\n process.kill();\n }\n }\n\n Future<OpenClawCliResult> createMobilePairingCode({bool remote = true}) {\n return runCli([\n 'qr',\n if (remote) '--remote',\n '--json',\n ], timeout: const Duration(seconds: 30));\n }\n\n Future<OpenClawCliResult> loginWeChat() {\n return runCli([\n 'channels',\n 'login',\n '--channel',\n _weChatPluginId,\n ], timeout: const Duration(minutes: 3));\n }\n\n Future<OpenClawCliResult> inspectChannels() {\n return runCli([\n 'channels',\n 'status',\n '--probe',\n ], timeout: const Duration(seconds: 40));\n }\n\n Future<OpenClawCliResult> installWeChatPlugin() async {\n final install = await runCli(\n ['plugins', 'install', _weChatPluginPackage, '--force'],\n timeout: const Duration(minutes: 3),\n ensureGateway: false,\n );\n if (!install.succeeded) return install;\n final enable = await runCli(\n ['config', 'set', 'plugins.entries.$_weChatPluginId.enabled', 'true'],\n timeout: const Duration(seconds: 30),\n ensureGateway: false,\n );\n return install.append(enable);\n }\n\n Future<OpenClawCliResult> runCli(\n List<String> args, {\n Duration timeout = _cliDefaultTimeout,\n bool ensureGateway = true,\n }) async {\n if (args.isEmpty) {\n throw const OpenClawRuntimeException('OpenClaw CLI 参数不能为空');\n }\n final launch = await _prepareCliLaunch(ensureGateway: ensureGateway);\n final processArgs = <String>[launch.cliPath, ...args];\n _diag(\n 'cli.start',\n data: {\n 'args': args,\n 'runtimeDir': launch.runtimeDir.path,\n 'configPath': launch.configPath.path,\n 'timeoutSeconds': timeout.inSeconds,\n },\n );\n\n final process = await Process.start(\n launch.nodePath,\n processArgs,\n workingDirectory: launch.runtimeDir.path,\n environment: _buildOpenClawEnvironment(\n runtimeDir: launch.runtimeDir,\n stateRoot: launch.stateRoot,\n configPath: launch.configPath,\n port: launch.port,\n token: launch.token,\n ),\n mode: ProcessStartMode.normal,\n runInShell: false,\n );\n final stdoutBuffer = StringBuffer();\n final stderrBuffer = StringBuffer();\n final stdoutSub = process.stdout\n .transform(utf8.decoder)\n .listen((chunk) => _appendBounded(stdoutBuffer, chunk));\n final stderrSub = process.stderr\n .transform(utf8.decoder)\n .listen((chunk) => _appendBounded(stderrBuffer, chunk));\n\n var timedOut = false;\n int exitCode;\n try {\n exitCode = await process.exitCode.timeout(timeout);\n } on TimeoutException {\n timedOut = true;\n process.kill();\n exitCode = await process.exitCode.timeout(\n const Duration(seconds: 5),\n onTimeout: () => -1,\n );\n } finally {\n await stdoutSub.cancel();\n await stderrSub.cancel();\n }\n\n final result = OpenClawCliResult(\n args: args,\n exitCode: exitCode,\n stdout: stdoutBuffer.toString(),\n stderr: stderrBuffer.toString(),\n timedOut: timedOut,\n );\n _diag(\n result.succeeded ? 'cli.complete' : 'cli.failed',\n data: {\n 'args': args,\n 'exitCode': exitCode,\n 'timedOut': timedOut,\n 'stdoutPreview': _previewProcessOutput(result.stdout),\n 'stderrPreview': _previewProcessOutput(result.stderr),\n },\n );\n return result;\n }\n\n Future<OpenClawGatewayTarget> _ensureStartedInternal({\n String? authToken,\n String? username,\n bool isMember = false,\n }) async {\n final platformKey = _platformKey;\n _diag('ensure-start.start', data: {'platformKey': platformKey});\n if (platformKey == null) {\n throw const OpenClawRuntimeException('当前平台不支持内置 OpenClaw Gateway');\n }\n\n final spec = await _loadSpec(platformKey, checkUpdates: true);\n final requestedPort = await AppSettings.getOpenClawGatewayPort(\n defaultValue: spec.defaultPort,\n );\n final token = await AppSettings.getOpenClawGatewayToken();\n final savedModel = await AppSettings.getOpenClawModel(\n defaultValue: spec.defaultModel,\n );\n final model = _gatewayChatModel(savedModel);\n final requestedAuthToken = authToken?.trim() ?? '';\n final backendDeepSeekModel = _backendDeepSeekModelRef(\n await AppSettings.getOpenClawDeepSeekModel(\n defaultValue: _defaultDeepSeekModelFor(spec.defaultModel),\n ),\n );\n final deepSeekProxyBaseUrl = _deepSeekProxyBaseUrl();\n final modelOverride = await AppSettings.getOpenClawModelOverride(\n defaultValue: spec.defaultModelOverride ?? '',\n );\n final remoteGatewayUrl = await AppSettings.getOpenClawRemoteGatewayUrl();\n final desktopTools = await _ensureDesktopTools();\n _diag(\n 'ensure-start.config',\n data: {\n 'platformKey': platformKey,\n 'requestedPort': requestedPort,\n 'model': model,\n 'rawModel': savedModel,\n 'backendDeepSeekModel': backendDeepSeekModel,\n 'deepSeekProxyBaseUrl': deepSeekProxyBaseUrl,\n 'hasAuthToken': requestedAuthToken.isNotEmpty,\n 'modelOverrideSet': modelOverride.trim().isNotEmpty,\n 'remoteGatewayUrlSet': remoteGatewayUrl.trim().isNotEmpty,\n 'desktopToolsUri': desktopTools.uri?.toString(),\n 'desktopToolsStatus': desktopTools.statusJson,\n },\n );\n\n final canReuseStartedProcess =\n _process != null && _processAuthToken == requestedAuthToken;\n if (canReuseStartedProcess &&\n await _probe(requestedPort, token, context: 'pre-start')) {\n _diag('ensure-start.reuse-existing', data: {'port': requestedPort});\n return OpenClawGatewayTarget(\n baseUri: Uri.parse('http://127.0.0.1:$requestedPort'),\n token: token,\n model: model,\n modelOverride: modelOverride.trim().isEmpty\n ? null\n : modelOverride.trim(),\n desktopToolsUri: desktopTools.uri,\n desktopToolsToken: desktopTools.token,\n desktopToolsStatus: desktopTools.statusJson,\n );\n } else if (_process != null) {\n _diag(\n 'ensure-start.restart-auth-changed',\n data: {\n 'hasPreviousAuthToken': _processAuthToken.isNotEmpty,\n 'hasRequestedAuthToken': requestedAuthToken.isNotEmpty,\n },\n );\n await stop();\n }\n\n final runtimeDir = await _prepareBundle(spec, platformKey);\n final stateRoot = await _stateRoot();\n final desktopToolsPluginDir = await _ensureDesktopToolsPlugin(\n stateRoot: stateRoot,\n );\n if (_process != null) {\n _diag(\n 'process.stop-stale-before-start',\n data: {'requestedPort': requestedPort},\n );\n await stop();\n }\n final portCandidate = await _portResolver.resolve(requestedPort);\n final port = portCandidate.port;\n if (portCandidate.isFallback) {\n await AppSettings.setOpenClawGatewayPort(port);\n _diag(\n 'ensure-start.fallback-port',\n data: {\n 'requestedPort': requestedPort,\n 'selectedPort': port,\n if (portCandidate.reason != null) 'reason': portCandidate.reason,\n },\n );\n } else {\n _diag(\n 'ensure-start.port-bindable',\n data: {'requestedPort': requestedPort, 'selectedPort': port},\n );\n }\n final configPath = await _ensureConfigFile(\n runtimeDir: runtimeDir,\n stateRoot: stateRoot,\n port: port,\n token: token,\n backendDeepSeekModel: backendDeepSeekModel,\n deepSeekProxyBaseUrl: deepSeekProxyBaseUrl,\n remoteGatewayUrl: remoteGatewayUrl,\n extraPluginLoadPaths: [desktopToolsPluginDir.path],\n );\n await _ensureAgentModelConfig(\n stateRoot: stateRoot,\n deepSeekProxyBaseUrl: deepSeekProxyBaseUrl,\n );\n final desktopToolsManifestPath = await _ensureDesktopToolsManifest(\n stateRoot: stateRoot,\n desktopTools: desktopTools,\n );\n\n final nodePath = p.join(runtimeDir.path, spec.nodeExecutable);\n final cliPath = p.join(runtimeDir.path, spec.cliEntrypoint);\n if (!await File(nodePath).exists()) {\n _diag('ensure-start.missing-node', data: {'nodePath': nodePath});\n throw OpenClawRuntimeException('OpenClaw 内置 Node 不存在: $nodePath');\n }\n if (!await File(cliPath).exists()) {\n _diag('ensure-start.missing-cli', data: {'cliPath': cliPath});\n throw OpenClawRuntimeException('OpenClaw CLI 入口不存在: $cliPath');\n }\n\n _remember(\n OpenClawRuntimeStatus(\n state: OpenClawRuntimeState.starting,\n message: '正在启动内置 OpenClaw Gateway',\n port: port,\n platformKey: platformKey,\n runtimePath: runtimeDir.path,\n checkedAt: DateTime.now(),\n ),\n );\n\n final args = <String>[\n cliPath,\n ...spec.gatewayArgs.map((arg) => arg.replaceAll('{port}', '$port')),\n ];\n _recentLogs.clear();\n _diag(\n 'process.starting',\n data: {\n 'nodePath': nodePath,\n 'args': args,\n 'workingDirectory': runtimeDir.path,\n 'configPath': configPath.path,\n 'desktopToolsPluginPath': desktopToolsPluginDir.path,\n 'desktopToolsManifestPath': desktopToolsManifestPath.path,\n 'requestedPort': requestedPort,\n 'selectedPort': port,\n },\n );\n\n final env =\n _buildOpenClawEnvironment(\n runtimeDir: runtimeDir,\n stateRoot: stateRoot,\n configPath: configPath,\n port: port,\n token: token,\n )..addAll({\n 'DACHENG_DESKTOP_TOOLS_ENABLED': desktopTools.uri == null ? '0' : '1',\n 'DACHENG_DESKTOP_TOOLS_MANIFEST': desktopToolsManifestPath.path,\n if (desktopTools.uri != null)\n 'DACHENG_DESKTOP_TOOLS_URL': desktopTools.uri.toString(),\n if (desktopTools.token != null)\n 'DACHENG_DESKTOP_TOOLS_TOKEN': desktopTools.token!,\n 'DACHENG_APP_RUNTIME': '1',\n // This is a process-local OpenClaw capability, never a Mahayana\n // account credential. Account HTTP stays inside the Rust runtime.\n 'DACHENG_AUTH_TOKEN': token,\n if (username != null && username.isNotEmpty)\n 'DACHENG_USERNAME': username,\n 'DACHENG_IS_MEMBER': isMember ? '1' : '0',\n 'DACHENG_OPENCLAW_PROXY_TOKEN': 'dacheng-openclaw-proxy',\n });\n\n _process = await Process.start(\n nodePath,\n args,\n workingDirectory: runtimeDir.path,\n environment: env,\n mode: ProcessStartMode.normal,\n runInShell: false,\n );\n _processAuthToken = requestedAuthToken;\n _captureLogs(_process!);\n _diag('process.started', data: {'pid': _process!.pid, 'port': port});\n\n final deadline = DateTime.now().add(_startupTimeout);\n while (DateTime.now().isBefore(deadline)) {\n if (await _probe(port, token, context: 'startup')) {\n _diag('process.ready', data: {'pid': _process?.pid, 'port': port});\n _remember(\n OpenClawRuntimeStatus(\n state: OpenClawRuntimeState.running,\n message: '本机 OpenClaw Gateway 已启动',\n port: port,\n platformKey: platformKey,\n runtimePath: runtimeDir.path,\n desktopToolsStatus: desktopTools.statusJson,\n checkedAt: DateTime.now(),\n ),\n );\n return OpenClawGatewayTarget(\n baseUri: Uri.parse('http://127.0.0.1:$port'),\n token: token,\n model: model,\n modelOverride: modelOverride.trim().isEmpty\n ? null\n : modelOverride.trim(),\n desktopToolsUri: desktopTools.uri,\n desktopToolsToken: desktopTools.token,\n desktopToolsStatus: desktopTools.statusJson,\n );\n }\n\n if (_process != null) {\n final exited = await _process!.exitCode.timeout(\n const Duration(milliseconds: 1),\n onTimeout: () => -999999,\n );\n if (exited != -999999) {\n final logs = _recentLogs.take(12).join('\\n');\n _diag(\n 'process.exited-early',\n data: {\n 'exitCode': exited,\n 'recentLogs': _recentLogs.take(12).toList(),\n },\n );\n throw OpenClawRuntimeException(\n 'OpenClaw Gateway 提前退出,exitCode=$exited${logs.isEmpty ? '' : '\\n$logs'}',\n );\n }\n }\n if (_recentLogsContainRuntimeModuleFailure()) {\n final logs = _recentLogs.take(12).join('\\n');\n _diag(\n 'process.runtime-module-failure',\n data: {'recentLogs': _recentLogs.take(12).toList()},\n );\n await stop();\n throw OpenClawRuntimeException(\n 'OpenClaw runtime 模块缓存异常${logs.isEmpty ? '' : '\\n$logs'}',\n );\n }\n await Future<void>.delayed(const Duration(milliseconds: 450));\n }\n\n final logs = _recentLogs.take(12).join('\\n');\n _diag(\n 'process.startup-timeout',\n data: {\n 'timeoutSeconds': _startupTimeout.inSeconds,\n 'recentLogs': _recentLogs.take(12).toList(),\n },\n );\n throw OpenClawRuntimeException(\n 'OpenClaw Gateway 启动超时${logs.isEmpty ? '' : '\\n$logs'}',\n );\n }\n\n Future<_OpenClawBundleSpec> _loadSpec(\n String platformKey, {\n bool checkUpdates = false,\n }) async {\n final raw = await rootBundle.loadString(_manifestAsset);\n final decoded = jsonDecode(raw) as Map<String, dynamic>;\n final platforms = Map<String, dynamic>.from(\n decoded['platforms'] as Map? ?? const {},\n );\n final platform = Map<String, dynamic>.from(\n platforms[platformKey] as Map? ?? const {},\n );\n if (platform.isEmpty) {\n throw OpenClawRuntimeException('bundle_manifest.json 缺少 $platformKey 配置');\n }\n\n final gatewayArgs =\n (platform['gatewayArgs'] as List? ??\n decoded['gatewayArgs'] as List? ??\n const ['gateway', '--port', '{port}'])\n .map((item) => item.toString())\n .toList();\n\n final spec = _OpenClawBundleSpec(\n version: (decoded['version'] ?? 'dev').toString(),\n defaultPort: _readInt(decoded['defaultPort']) ?? 18789,\n defaultModel: (decoded['defaultModel'] ?? 'openclaw/default').toString(),\n defaultModelOverride: decoded['defaultModelOverride']?.toString(),\n nodeExecutable: (platform['nodeExecutable'] ?? 'node/bin/node')\n .toString(),\n cliEntrypoint: (platform['cliEntrypoint'] ?? 'openclaw/bin/openclaw.js')\n .toString(),\n gatewayArgs: gatewayArgs,\n );\n _diag(\n 'manifest.loaded',\n data: {\n 'platformKey': platformKey,\n 'version': spec.version,\n 'defaultPort': spec.defaultPort,\n 'defaultModel': spec.defaultModel,\n 'nodeExecutable': spec.nodeExecutable,\n 'cliEntrypoint': spec.cliEntrypoint,\n 'gatewayArgs': spec.gatewayArgs,\n },\n );\n final allowDownloadedRuntime = await _allowDownloadedRuntime(platformKey);\n var effectiveSpec = spec;\n if (allowDownloadedRuntime) {\n effectiveSpec =\n await _loadActiveDownloadedSpec(platformKey, fallbackSpec: spec) ??\n spec;\n } else {\n await _clearActiveDownloadedRuntimeSpec(\n platformKey: platformKey,\n reason: 'macos-sandbox',\n );\n }\n if (checkUpdates && allowDownloadedRuntime) {\n effectiveSpec =\n await _maybeInstallRuntimeUpdate(\n platformKey: platformKey,\n currentSpec: effectiveSpec,\n ) ??\n effectiveSpec;\n } else if (checkUpdates) {\n _diag(\n 'runtime-update.skip-disabled',\n data: {'platformKey': platformKey, 'reason': 'macos-sandbox'},\n );\n }\n return effectiveSpec;\n }\n\n Future<bool> _allowDownloadedRuntime(String platformKey) async {\n if (!Platform.isMacOS) return true;\n\n final support = await getApplicationSupportDirectory();\n final sandboxContainerId =\n Platform.environment['APP_SANDBOX_CONTAINER_ID']?.trim() ?? '';\n final pathLooksSandboxed = p\n .split(p.normalize(support.path))\n .contains('Containers');\n final sandboxed = sandboxContainerId.isNotEmpty || pathLooksSandboxed;\n if (!sandboxed) return true;\n\n // App Store/TestFlight sandboxed apps cannot reliably execute downloaded\n // Mach-O payloads. Use the bundled runtime that was signed with inherited\n // sandbox entitlements during packaging.\n _diag(\n 'runtime-update.disabled-macos-sandbox',\n data: {\n 'platformKey': platformKey,\n 'supportPath': support.path,\n 'hasSandboxContainerId': sandboxContainerId.isNotEmpty,\n },\n );\n return false;\n }\n\n Future<void> _clearActiveDownloadedRuntimeSpec({\n required String platformKey,\n required String reason,\n }) async {\n final saved = await AppSettings.getOpenClawActiveRuntimeSpec();\n if (saved == null) return;\n await AppSettings.clearOpenClawActiveRuntimeSpec();\n _diag(\n 'runtime-update.active-cleared',\n data: {\n 'platformKey': platformKey,\n 'reason': reason,\n 'version': saved['version']?.toString(),\n 'downloaded': saved['downloaded'] == true,\n },\n );\n }\n\n Future<_OpenClawBundleSpec?> _loadActiveDownloadedSpec(\n String platformKey, {\n required _OpenClawBundleSpec fallbackSpec,\n }) async {\n final saved = await AppSettings.getOpenClawActiveRuntimeSpec();\n if (saved == null) return null;\n final spec = _specFromJson(saved, downloaded: true);\n if (spec == null) {\n await AppSettings.clearOpenClawActiveRuntimeSpec();\n return null;\n }\n final runtimeDir = await _cachedRuntimeDirIfComplete(spec, platformKey);\n if (runtimeDir == null) {\n await AppSettings.clearOpenClawActiveRuntimeSpec();\n _diag(\n 'runtime-update.active-missing',\n data: {'platformKey': platformKey, 'version': spec.version},\n );\n return null;\n }\n if (_isRuntimeVersionNewer(fallbackSpec.version, spec.version)) {\n await AppSettings.clearOpenClawActiveRuntimeSpec();\n _diag(\n 'runtime-update.active-cleared',\n data: {\n 'platformKey': platformKey,\n 'reason': 'bundled-newer',\n 'version': spec.version,\n 'bundledVersion': fallbackSpec.version,\n },\n );\n return null;\n }\n _diag(\n 'runtime-update.active-loaded',\n data: {\n 'platformKey': platformKey,\n 'version': spec.version,\n 'runtimeDir': runtimeDir.path,\n 'bundledVersion': fallbackSpec.version,\n },\n );\n return spec;\n }\n\n bool _isRuntimeVersionNewer(String candidate, String current) {\n final candidateRank = _runtimeVersionRank(candidate);\n final currentRank = _runtimeVersionRank(current);\n final length = math.max(candidateRank.length, currentRank.length);\n for (var i = 0; i < length; i++) {\n final a = i < candidateRank.length ? candidateRank[i] : 0;\n final b = i < currentRank.length ? currentRank[i] : 0;\n if (a != b) return a > b;\n }\n return false;\n }\n\n List<int> _runtimeVersionRank(String version) {\n return RegExp(r'\\d+')\n .allMatches(version)\n .map((match) => int.tryParse(match.group(0) ?? '') ?? 0)\n .toList(growable: false);\n }\n\n _OpenClawBundleSpec? _specFromJson(\n Map<String, dynamic> json, {\n required bool downloaded,\n }) {\n final version = json['version']?.toString().trim() ?? '';\n final nodeExecutable = json['nodeExecutable']?.toString().trim() ?? '';\n final cliEntrypoint = json['cliEntrypoint']?.toString().trim() ?? '';\n if (version.isEmpty || nodeExecutable.isEmpty || cliEntrypoint.isEmpty) {\n return null;\n }\n return _OpenClawBundleSpec(\n version: version,\n defaultPort: _readInt(json['defaultPort']) ?? 18789,\n defaultModel: (json['defaultModel'] ?? _defaultGatewayModel).toString(),\n defaultModelOverride: json['defaultModelOverride']?.toString(),\n nodeExecutable: nodeExecutable,\n cliEntrypoint: cliEntrypoint,\n gatewayArgs:\n (json['gatewayArgs'] as List? ??\n const ['gateway', '--port', '{port}', '--force'])\n .map((item) => item.toString())\n .toList(),\n downloaded: downloaded,\n );\n }\n\n Future<_OpenClawBundleSpec?> _maybeInstallRuntimeUpdate({\n required String platformKey,\n required _OpenClawBundleSpec currentSpec,\n }) async {\n if (_configuredRuntimeDirOverride(platformKey) != null) {\n _diag('runtime-update.skip-override', data: {'platformKey': platformKey});\n return null;\n }\n\n try {\n final release = await _fetchRuntimeRelease(\n platformKey: platformKey,\n currentVersion: currentSpec.version,\n );\n if (release == null || release.version == currentSpec.version) {\n return null;\n }\n\n final releaseSpec = release.toBundleSpec();\n final cached = await _cachedRuntimeDirIfComplete(\n releaseSpec,\n platformKey,\n );\n if (cached != null) {\n await AppSettings.setOpenClawActiveRuntimeSpec(releaseSpec.toJson());\n _diag(\n 'runtime-update.use-cached',\n data: {\n 'platformKey': platformKey,\n 'version': release.version,\n 'runtimeDir': cached.path,\n },\n );\n return releaseSpec;\n }\n\n return await _downloadAndInstallRuntimeUpdate(\n release: release,\n platformKey: platformKey,\n );\n } catch (error, stackTrace) {\n _diag(\n 'runtime-update.failed',\n data: {\n 'platformKey': platformKey,\n 'currentVersion': currentSpec.version,\n 'error': error.toString(),\n },\n error: error,\n stackTrace: stackTrace,\n );\n return null;\n }\n }\n\n Future<_OpenClawRuntimeRelease?> _fetchRuntimeRelease({\n required String platformKey,\n required String currentVersion,\n }) async {\n final baseUrl = AppConfig.currentAiBackendUrl.replaceFirst(\n RegExp(r'/+$'),\n '',\n );\n final uri = Uri.parse('$baseUrl/api/openclaw/runtime/manifest').replace(\n queryParameters: {\n 'platform': platformKey,\n 'currentVersion': currentVersion,\n 'appVersion': AppConfig.appVersion,\n },\n );\n final response = await http\n .get(uri, headers: {'Accept': 'application/json'})\n .timeout(const Duration(seconds: 20));\n if (response.statusCode < 200 || response.statusCode >= 300) {\n _diag(\n 'runtime-update.manifest-http',\n data: {'statusCode': response.statusCode, 'uri': uri.toString()},\n );\n return null;\n }\n final decoded = _decodeJsonOrNull(response.body);\n if (decoded is! Map) return null;\n final payload = Map<String, dynamic>.from(decoded);\n if (payload['updateAvailable'] != true) {\n _diag(\n 'runtime-update.none',\n data: {\n 'platformKey': platformKey,\n 'reason': payload['reason']?.toString(),\n 'currentVersion': currentVersion,\n 'latestVersion': payload['latestVersion']?.toString(),\n },\n );\n return null;\n }\n final latest = payload['latest'];\n if (latest is! Map) return null;\n return _releaseFromJson(Map<String, dynamic>.from(latest));\n }\n\n _OpenClawRuntimeRelease? _releaseFromJson(Map<String, dynamic> json) {\n final version = json['version']?.toString().trim() ?? '';\n final archiveUrl = json['archiveUrl']?.toString().trim() ?? '';\n final sha256Hex = json['sha256']?.toString().trim().toLowerCase() ?? '';\n final archiveUri = Uri.tryParse(archiveUrl);\n final gatewayArgs =\n (json['gatewayArgs'] as List? ??\n const ['gateway', '--port', '{port}', '--force'])\n .map((item) => item.toString())\n .toList();\n if (version.isEmpty ||\n archiveUri == null ||\n !archiveUri.hasScheme ||\n !RegExp(r'^[a-f0-9]{64}$').hasMatch(sha256Hex)) {\n _diag(\n 'runtime-update.invalid-release',\n data: {\n 'version': version,\n 'archiveUrlSet': archiveUrl.isNotEmpty,\n 'sha256Length': sha256Hex.length,\n },\n );\n return null;\n }\n return _OpenClawRuntimeRelease(\n version: version,\n archiveUri: archiveUri,\n sha256Hex: sha256Hex,\n size: _readInt(json['size']) ?? 0,\n defaultPort: _readInt(json['defaultPort']) ?? 18789,\n defaultModel: (json['defaultModel'] ?? _defaultGatewayModel).toString(),\n defaultModelOverride: json['defaultModelOverride']?.toString(),\n nodeExecutable: (json['nodeExecutable'] ?? 'node/bin/node').toString(),\n cliEntrypoint: (json['cliEntrypoint'] ?? 'openclaw/openclaw.mjs')\n .toString(),\n gatewayArgs: gatewayArgs,\n );\n }\n\n Future<_OpenClawBundleSpec> _downloadAndInstallRuntimeUpdate({\n required _OpenClawRuntimeRelease release,\n required String platformKey,\n }) async {\n final spec = release.toBundleSpec();\n final runtimeDir = await _runtimeDir(spec, platformKey);\n final support = await getApplicationSupportDirectory();\n final downloadDir = Directory(p.join(support.path, 'openclaw_downloads'));\n await downloadDir.create(recursive: true);\n final archivePath = p.join(\n downloadDir.path,\n '${_safePathPart(release.version)}-$platformKey${_archiveExtension(release.archiveUri)}',\n );\n final archiveFile = File(archivePath);\n final digest = await _downloadRuntimeArchive(release, archiveFile);\n if (digest != release.sha256Hex) {\n await _deleteFileIfExists(archiveFile);\n throw OpenClawRuntimeException(\n 'OpenClaw runtime 校验失败: expected=${release.sha256Hex}, actual=$digest',\n );\n }\n\n final extractDir = Directory(\n p.join(\n downloadDir.path,\n '${_safePathPart(release.version)}-$platformKey-extract',\n ),\n );\n final stagingDir = Directory(\n p.join(\n downloadDir.path,\n '${_safePathPart(release.version)}-$platformKey-stage',\n ),\n );\n if (await extractDir.exists()) await extractDir.delete(recursive: true);\n if (await stagingDir.exists()) await stagingDir.delete(recursive: true);\n await extractDir.create(recursive: true);\n await _extractRuntimeArchive(archiveFile, extractDir);\n\n final extractedRoot = await _findExtractedRuntimeRoot(\n extractDir: extractDir,\n spec: spec,\n );\n if (extractedRoot == null) {\n throw OpenClawRuntimeException('下载的 OpenClaw runtime 不包含必要的 node/cli 文件');\n }\n\n await runtimeDir.parent.create(recursive: true);\n if (await runtimeDir.exists()) await runtimeDir.delete(recursive: true);\n await _moveOrCopyRuntimeDirectory(extractedRoot, stagingDir);\n await stagingDir.rename(runtimeDir.path);\n final marker = File(p.join(runtimeDir.path, '.bundle_ready'));\n await marker.writeAsString(\n const JsonEncoder.withIndent(' ').convert({\n 'installedAt': DateTime.now().toIso8601String(),\n 'version': release.version,\n 'platform': platformKey,\n 'sha256': digest,\n 'source': release.archiveUri.toString(),\n }),\n );\n await _repairRuntimeLaunchMetadata(\n runtimeDir: runtimeDir,\n nodePath: File(p.join(runtimeDir.path, spec.nodeExecutable)),\n platformKey: platformKey,\n cached: false,\n );\n await AppSettings.setOpenClawActiveRuntimeSpec(spec.toJson());\n await _deleteDirectoryIfExists(extractDir);\n await _deleteFileIfExists(archiveFile);\n _diag(\n 'runtime-update.installed',\n data: {\n 'platformKey': platformKey,\n 'version': release.version,\n 'runtimeDir': runtimeDir.path,\n 'size': release.size,\n },\n );\n return spec;\n }\n\n Future<String> _downloadRuntimeArchive(\n _OpenClawRuntimeRelease release,\n File destination,\n ) async {\n final client = http.Client();\n IOSink? output;\n try {\n final request = http.Request('GET', release.archiveUri)\n ..headers['Accept'] = 'application/octet-stream';\n final response = await client\n .send(request)\n .timeout(const Duration(seconds: 20));\n if (response.statusCode < 200 || response.statusCode >= 300) {\n throw OpenClawRuntimeException(\n 'OpenClaw runtime 下载失败 (${response.statusCode})',\n );\n }\n await destination.parent.create(recursive: true);\n output = destination.openWrite();\n final digestSink = AccumulatorSink<Digest>();\n final hashSink = sha256.startChunkedConversion(digestSink);\n var bytes = 0;\n await for (final chunk in response.stream.timeout(\n const Duration(minutes: 5),\n )) {\n bytes += chunk.length;\n hashSink.add(chunk);\n output.add(chunk);\n }\n await output.flush();\n await output.close();\n output = null;\n hashSink.close();\n final digest = digestSink.events.single.toString();\n _diag(\n 'runtime-update.downloaded',\n data: {\n 'archiveUri': release.archiveUri.toString(),\n 'bytes': bytes,\n 'sha256': digest,\n },\n );\n return digest;\n } finally {\n await output?.close().catchError((_) {});\n client.close();\n }\n }\n\n Future<void> _extractRuntimeArchive(\n File archiveFile,\n Directory destination,\n ) async {\n final lowerPath = archiveFile.path.toLowerCase();\n if (Platform.isMacOS && lowerPath.endsWith('.zip')) {\n try {\n final result = await Process.run('/usr/bin/ditto', [\n '-x',\n '-k',\n archiveFile.path,\n destination.path,\n ]).timeout(const Duration(minutes: 5));\n _diag(\n 'runtime-update.extract-native',\n data: {\n 'archivePath': archiveFile.path,\n 'destination': destination.path,\n 'exitCode': result.exitCode,\n if ((result.stderr as Object).toString().isNotEmpty)\n 'stderr': _previewProcessOutput(result.stderr),\n },\n );\n if (result.exitCode == 0) return;\n } catch (error, stackTrace) {\n _diag(\n 'runtime-update.extract-native-failed',\n data: {\n 'archivePath': archiveFile.path,\n 'destination': destination.path,\n 'error': error.toString(),\n },\n error: error,\n stackTrace: stackTrace,\n );\n }\n }\n\n await extractFileToDisk(archiveFile.path, destination.path);\n _diag(\n 'runtime-update.extract-dart',\n data: {'archivePath': archiveFile.path, 'destination': destination.path},\n );\n }\n\n Future<Directory?> _findExtractedRuntimeRoot({\n required Directory extractDir,\n required _OpenClawBundleSpec spec,\n }) async {\n final direct = await _runtimeRootIfComplete(extractDir, spec);\n if (direct != null) return direct;\n await for (final entity in extractDir.list(followLinks: false)) {\n if (entity is! Directory) continue;\n final nested = await _runtimeRootIfComplete(entity, spec);\n if (nested != null) return nested;\n }\n return null;\n }\n\n Future<Directory?> _runtimeRootIfComplete(\n Directory dir,\n _OpenClawBundleSpec spec,\n ) async {\n final nodePath = File(p.join(dir.path, spec.nodeExecutable));\n final cliPath = File(p.join(dir.path, spec.cliEntrypoint));\n if (await nodePath.exists() && await cliPath.exists()) return dir;\n return null;\n }\n\n Future<Directory?> _cachedRuntimeDirIfComplete(\n _OpenClawBundleSpec spec,\n String platformKey,\n ) async {\n final runtimeDir = await _runtimeDir(spec, platformKey);\n final marker = File(p.join(runtimeDir.path, '.bundle_ready'));\n final nodePath = File(p.join(runtimeDir.path, spec.nodeExecutable));\n final cliPath = File(p.join(runtimeDir.path, spec.cliEntrypoint));\n if (await marker.exists() &&\n await nodePath.exists() &&\n await cliPath.exists()) {\n return runtimeDir;\n }\n return null;\n }\n\n String _safePathPart(String value) {\n final safe = value.replaceAll(RegExp(r'[^A-Za-z0-9_.-]+'), '-');\n return safe.isEmpty ? 'runtime' : safe;\n }\n\n String _archiveExtension(Uri uri) {\n final lower = uri.path.toLowerCase();\n if (lower.endsWith('.tar.gz')) return '.tar.gz';\n if (lower.endsWith('.tgz')) return '.tgz';\n if (lower.endsWith('.tar')) return '.tar';\n return '.zip';\n }\n\n Future<void> _deleteFileIfExists(File file) async {\n try {\n if (await file.exists()) await file.delete();\n } catch (_) {}\n }\n\n Future<void> _deleteDirectoryIfExists(Directory dir) async {\n try {\n if (await dir.exists()) await dir.delete(recursive: true);\n } catch (_) {}\n }\n\n Future<Directory> _prepareBundle(\n _OpenClawBundleSpec spec,\n String platformKey,\n ) async {\n final overrideRuntimeDir = await _runtimeDirOverride(spec, platformKey);\n if (overrideRuntimeDir != null) {\n final nodePath = File(\n p.join(overrideRuntimeDir.path, spec.nodeExecutable),\n );\n await _repairRuntimeLaunchMetadata(\n runtimeDir: overrideRuntimeDir,\n nodePath: nodePath,\n platformKey: platformKey,\n cached: true,\n );\n _diag(\n 'bundle.launch.override-runtime',\n data: {\n 'platformKey': platformKey,\n 'runtimeDir': overrideRuntimeDir.path,\n },\n );\n return overrideRuntimeDir;\n }\n\n if (spec.downloaded) {\n final downloadedRuntimeDir = await _cachedRuntimeDirIfComplete(\n spec,\n platformKey,\n );\n if (downloadedRuntimeDir != null) {\n await _repairRuntimeLaunchMetadata(\n runtimeDir: downloadedRuntimeDir,\n nodePath: File(\n p.join(downloadedRuntimeDir.path, spec.nodeExecutable),\n ),\n platformKey: platformKey,\n cached: true,\n );\n _diag(\n 'bundle.launch.downloaded-runtime',\n data: {\n 'platformKey': platformKey,\n 'version': spec.version,\n 'runtimeDir': downloadedRuntimeDir.path,\n },\n );\n return downloadedRuntimeDir;\n }\n await AppSettings.clearOpenClawActiveRuntimeSpec();\n throw OpenClawRuntimeException(\n '已下载的 OpenClaw runtime 不完整,已回退到内置版本,请重新启动。',\n );\n }\n\n final bundledRuntimeDir = await _bundledRuntimeDir(spec, platformKey);\n if (bundledRuntimeDir != null) {\n _diag(\n 'bundle.launch.bundled-runtime',\n data: {\n 'platformKey': platformKey,\n 'runtimeDir': bundledRuntimeDir.path,\n },\n );\n return bundledRuntimeDir;\n }\n\n if (Platform.isMacOS) {\n throw OpenClawRuntimeException(\n '当前安装包没有可执行的 $platformKey OpenClaw runtime。请先运行 scripts/build_openclaw_desktop_bundle.sh $platformKey,再通过 flutter run 启动。',\n );\n }\n\n final runtimeDir = await _runtimeDir(spec, platformKey);\n final marker = File(p.join(runtimeDir.path, '.bundle_ready'));\n final nodePath = File(p.join(runtimeDir.path, spec.nodeExecutable));\n final cliPath = File(p.join(runtimeDir.path, spec.cliEntrypoint));\n\n if (await marker.exists() &&\n await nodePath.exists() &&\n await cliPath.exists()) {\n await _repairRuntimeLaunchMetadata(\n runtimeDir: runtimeDir,\n nodePath: nodePath,\n platformKey: platformKey,\n cached: true,\n );\n _diag(\n 'bundle.prepare.cached',\n data: {\n 'platformKey': platformKey,\n 'runtimeDir': runtimeDir.path,\n 'marker': marker.path,\n },\n );\n return runtimeDir;\n }\n\n final legacyRuntimeDir = await _legacyRuntimeDir(spec, platformKey);\n if (legacyRuntimeDir.path != runtimeDir.path &&\n await File(\n p.join(legacyRuntimeDir.path, spec.nodeExecutable),\n ).exists() &&\n await File(\n p.join(legacyRuntimeDir.path, spec.cliEntrypoint),\n ).exists()) {\n _diag(\n 'bundle.prepare.migrate-legacy',\n data: {\n 'platformKey': platformKey,\n 'from': legacyRuntimeDir.path,\n 'to': runtimeDir.path,\n },\n );\n if (await runtimeDir.exists()) {\n await runtimeDir.delete(recursive: true);\n }\n await _copyRuntimeDirectory(legacyRuntimeDir, runtimeDir);\n await _repairRuntimeLaunchMetadata(\n runtimeDir: runtimeDir,\n nodePath: nodePath,\n platformKey: platformKey,\n cached: false,\n );\n await marker.writeAsString(DateTime.now().toIso8601String());\n _diag(\n 'bundle.prepare.migrate-complete',\n data: {\n 'platformKey': platformKey,\n 'runtimeDir': runtimeDir.path,\n 'nodeExists': await nodePath.exists(),\n 'cliExists': await cliPath.exists(),\n },\n );\n return runtimeDir;\n }\n\n final prefix = 'assets/openclaw/$platformKey/';\n final assets = await _listAssets(prefix);\n final hasNodeAsset = assets.contains('$prefix${spec.nodeExecutable}');\n final hasCliAsset = assets.contains('$prefix${spec.cliEntrypoint}');\n _diag(\n 'bundle.prepare.assets',\n data: {\n 'platformKey': platformKey,\n 'prefix': prefix,\n 'assetCount': assets.length,\n 'hasNodeAsset': hasNodeAsset,\n 'hasCliAsset': hasCliAsset,\n 'nodeAsset': '$prefix${spec.nodeExecutable}',\n 'cliAsset': '$prefix${spec.cliEntrypoint}',\n 'sample': assets.take(8).toList(),\n },\n );\n if (!hasNodeAsset || !hasCliAsset) {\n throw OpenClawRuntimeException(\n '当前安装包没有内置 $platformKey 的 OpenClaw runtime。请在 release 构建中运行 scripts/build_openclaw_desktop_bundle.sh 后再打包。',\n );\n }\n\n if (await runtimeDir.exists()) {\n _diag('bundle.prepare.remove-old', data: {'runtimeDir': runtimeDir.path});\n await runtimeDir.delete(recursive: true);\n }\n await runtimeDir.create(recursive: true);\n\n for (final asset in assets) {\n final relative = asset.substring(prefix.length);\n if (relative.isEmpty || relative.endsWith('/')) continue;\n final bytes = await rootBundle.load(asset);\n final file = File(p.join(runtimeDir.path, relative));\n await file.parent.create(recursive: true);\n await file.writeAsBytes(\n bytes.buffer.asUint8List(bytes.offsetInBytes, bytes.lengthInBytes),\n flush: false,\n );\n }\n\n await _repairRuntimeLaunchMetadata(\n runtimeDir: runtimeDir,\n nodePath: nodePath,\n platformKey: platformKey,\n cached: false,\n );\n await marker.writeAsString(DateTime.now().toIso8601String());\n _diag(\n 'bundle.prepare.complete',\n data: {\n 'platformKey': platformKey,\n 'runtimeDir': runtimeDir.path,\n 'assetCount': assets.length,\n 'nodeExists': await nodePath.exists(),\n 'nodeSize': await nodePath.exists() ? await nodePath.length() : 0,\n 'cliExists': await cliPath.exists(),\n 'cliSize': await cliPath.exists() ? await cliPath.length() : 0,\n },\n );\n return runtimeDir;\n }\n\n Future<void> _copyRuntimeDirectory(\n Directory source,\n Directory destination,\n ) async {\n await destination.create(recursive: true);\n await for (final entity in source.list(\n recursive: true,\n followLinks: false,\n )) {\n final relative = p.relative(entity.path, from: source.path);\n if (relative == '.') continue;\n final target = p.join(destination.path, relative);\n if (entity is Directory) {\n await Directory(target).create(recursive: true);\n } else if (entity is File) {\n final targetFile = File(target);\n await targetFile.parent.create(recursive: true);\n await entity.openRead().pipe(targetFile.openWrite());\n }\n }\n }\n\n Future<void> _moveOrCopyRuntimeDirectory(\n Directory source,\n Directory destination,\n ) async {\n if (await destination.exists()) {\n await destination.delete(recursive: true);\n }\n await destination.parent.create(recursive: true);\n try {\n await source.rename(destination.path);\n return;\n } on FileSystemException {\n await _copyRuntimeDirectory(source, destination);\n }\n }\n\n Future<void> _repairRuntimeLaunchMetadata({\n required Directory runtimeDir,\n required File nodePath,\n required String platformKey,\n required bool cached,\n }) async {\n if (Platform.isWindows) return;\n\n final chmodResult = await Process.run('/bin/chmod', ['+x', nodePath.path]);\n ProcessResult? xattrResult;\n if (Platform.isMacOS) {\n // Files created by a quarantined app can inherit the quarantine xattr.\n // macOS then rejects Process.start with \"Operation not permitted\".\n xattrResult = await Process.run('/usr/bin/xattr', [\n '-dr',\n 'com.apple.quarantine',\n runtimeDir.path,\n ]);\n await Process.run('/usr/bin/xattr', [\n '-dr',\n 'com.apple.provenance',\n runtimeDir.path,\n ]);\n }\n\n _diag(\n 'bundle.prepare.launch-metadata',\n data: {\n 'platformKey': platformKey,\n 'runtimeDir': runtimeDir.path,\n 'nodePath': nodePath.path,\n 'cached': cached,\n 'chmodExitCode': chmodResult.exitCode,\n if ((chmodResult.stderr as Object).toString().isNotEmpty)\n 'chmodStderr': _previewProcessOutput(chmodResult.stderr),\n if (xattrResult != null) 'xattrExitCode': xattrResult.exitCode,\n if (xattrResult != null &&\n (xattrResult.stderr as Object).toString().isNotEmpty)\n 'xattrStderr': _previewProcessOutput(xattrResult.stderr),\n },\n );\n }\n\n Future<bool> _hasRequiredBundleAssets(\n String platformKey,\n _OpenClawBundleSpec spec,\n ) async {\n final prefix = 'assets/openclaw/$platformKey/';\n final assets = await _listAssets(prefix);\n final hasRequired =\n assets.contains('$prefix${spec.nodeExecutable}') &&\n assets.contains('$prefix${spec.cliEntrypoint}');\n _diag(\n 'bundle.has-required-assets',\n data: {\n 'platformKey': platformKey,\n 'assetCount': assets.length,\n 'hasRequired': hasRequired,\n },\n );\n return hasRequired;\n }\n\n Future<List<String>> _listAssets(String prefix) async {\n final assets = <String>{};\n var jsonCount = 0;\n var binCount = 0;\n var indexCount = 0;\n final failures = <String>[];\n\n try {\n final raw = await rootBundle.loadString('AssetManifest.json');\n final decoded = jsonDecode(raw) as Map<String, dynamic>;\n final items = decoded.keys\n .where((key) => key.startsWith(prefix))\n .toList();\n jsonCount = items.length;\n assets.addAll(items);\n } catch (error) {\n failures.add('AssetManifest.json: $error');\n // Newer Flutter release builds may only include AssetManifest.bin.\n }\n\n try {\n final manifest = await AssetManifest.loadFromAssetBundle(rootBundle);\n final items = manifest\n .listAssets()\n .where((key) => key.startsWith(prefix))\n .toList();\n binCount = items.length;\n assets.addAll(items);\n } catch (error) {\n failures.add('AssetManifest.bin: $error');\n // App Store archives patched after flutter build carry a JSON/index file.\n }\n\n try {\n final raw = await rootBundle.loadString(\n 'assets/openclaw/asset_index.json',\n );\n final decoded = jsonDecode(raw);\n final indexedAssets = decoded is Map\n ? decoded['assets']\n : decoded is List\n ? decoded\n : const [];\n if (indexedAssets is List) {\n final items = indexedAssets\n .map((item) => item.toString())\n .where((key) => key.startsWith(prefix))\n .toList();\n indexCount = items.length;\n assets.addAll(items);\n }\n } catch (error) {\n failures.add('asset_index.json: $error');\n // Older builds do not have an OpenClaw asset index.\n }\n\n final sortedAssets = assets.toList()..sort();\n _diag(\n 'asset.list',\n data: {\n 'prefix': prefix,\n 'total': sortedAssets.length,\n 'jsonCount': jsonCount,\n 'binCount': binCount,\n 'indexCount': indexCount,\n if (failures.isNotEmpty) 'failures': failures,\n 'sample': sortedAssets.take(8).toList(),\n },\n );\n return sortedAssets;\n }\n\n Future<bool> _probe(\n int port,\n String token, {\n String context = 'probe',\n }) async {\n try {\n final healthResponse = await http\n .get(\n Uri.parse('http://127.0.0.1:$port/health'),\n headers: {'Authorization': 'Bearer $token'},\n )\n .timeout(_probeTimeout);\n final healthPayload = _decodeJsonOrNull(healthResponse.body);\n if (healthResponse.statusCode < 200 ||\n healthResponse.statusCode >= 300 ||\n !_looksLikeHealthPayload(healthPayload)) {\n _diag(\n 'probe.health-unhealthy',\n data: {\n 'context': context,\n 'port': port,\n 'statusCode': healthResponse.statusCode,\n 'contentType': healthResponse.headers['content-type'],\n 'bodyPreview': _previewBody(healthResponse.body),\n },\n );\n return false;\n }\n\n final modelsResponse = await http\n .get(\n Uri.parse('http://127.0.0.1:$port/v1/models'),\n headers: {'Authorization': 'Bearer $token'},\n )\n .timeout(_probeTimeout);\n final modelsPayload = _decodeJsonOrNull(modelsResponse.body);\n if (modelsResponse.statusCode < 200 ||\n modelsResponse.statusCode >= 300 ||\n !_looksLikeModelsPayload(modelsPayload)) {\n _diag(\n 'probe.models-unhealthy',\n data: {\n 'context': context,\n 'port': port,\n 'statusCode': modelsResponse.statusCode,\n 'contentType': modelsResponse.headers['content-type'],\n 'bodyPreview': _previewBody(modelsResponse.body),\n },\n );\n return false;\n }\n\n // `/v1/models` can be healthy while `/v1/chat/completions` is disabled\n // by an old config. Use GET so the probe only checks route wiring and\n // never starts a real model request.\n final chatResponse = await http\n .get(\n Uri.parse('http://127.0.0.1:$port/v1/chat/completions'),\n headers: {'Authorization': 'Bearer $token'},\n )\n .timeout(_probeTimeout);\n final healthy =\n chatResponse.statusCode < 500 &&\n chatResponse.statusCode != 401 &&\n chatResponse.statusCode != 403 &&\n chatResponse.statusCode != 404;\n _diag(\n healthy ? 'probe.healthy' : 'probe.chat-unhealthy',\n data: {\n 'context': context,\n 'port': port,\n 'chatStatusCode': chatResponse.statusCode,\n },\n );\n return healthy;\n } catch (error) {\n if (context != 'startup') {\n _diag(\n 'probe.error',\n data: {'context': context, 'port': port},\n error: error,\n );\n }\n return false;\n }\n }\n\n Future<File> _ensureConfigFile({\n required Directory runtimeDir,\n required Directory stateRoot,\n required int port,\n required String token,\n required String backendDeepSeekModel,\n required String deepSeekProxyBaseUrl,\n required String remoteGatewayUrl,\n List<String> extraPluginLoadPaths = const [],\n }) async {\n final configDir = Directory(p.join(stateRoot.path, 'config'));\n await configDir.create(recursive: true);\n final workspace = Directory(p.join(stateRoot.path, 'workspace'));\n await workspace.create(recursive: true);\n final configPath = File(p.join(configDir.path, 'openclaw.json'));\n final bundledPluginLoadPaths = await _bundledPluginLoadPaths(runtimeDir);\n final pluginLoadPaths = _mergeStringLists(\n extraPluginLoadPaths,\n bundledPluginLoadPaths,\n );\n final hasWeChatPlugin = pluginLoadPaths.any(\n (path) => p.basename(path) == _weChatPluginId,\n );\n final remoteUrl = remoteGatewayUrl.trim();\n\n final config = _buildEmbeddedConfig(\n stateRoot: stateRoot,\n port: port,\n token: token,\n backendDeepSeekModel: backendDeepSeekModel,\n deepSeekProxyBaseUrl: deepSeekProxyBaseUrl,\n remoteGatewayUrl: remoteUrl,\n pluginLoadPaths: pluginLoadPaths,\n hasWeChatPlugin: hasWeChatPlugin,\n );\n\n // Repair older embedded configs in-place. OpenClaw 2026.6 requires\n // gateway.mode=local, and the home screen requires the OpenAI-compatible\n // chat endpoint. Preserve unrelated user edits while fixing the fields\n // needed for the app-owned embedded gateway to start reliably.\n try {\n final mcpConfigPath = File(\n p.join(\n Platform.environment['HOME'] ?? '',\n '.gemini',\n 'config',\n 'mcp_config.json',\n ),\n );\n if (await mcpConfigPath.exists()) {\n final mcpConfig = jsonDecode(await mcpConfigPath.readAsString());\n if (mcpConfig is Map && mcpConfig.containsKey('mcpServers')) {\n config['mcpServers'] = mcpConfig['mcpServers'];\n }\n }\n } catch (e) {\n _diag('mcp.config-error', data: {'error': e.toString()});\n }\n\n final merged = await _mergeEmbeddedConfig(configPath, config);\n await configPath.writeAsString(\n const JsonEncoder.withIndent(' ').convert(merged),\n );\n _diag(\n 'config.written',\n data: {\n 'configPath': configPath.path,\n 'port': port,\n 'gatewayMode': _mutableMap(merged['gateway'])['mode'],\n 'backendDeepSeekModel': backendDeepSeekModel,\n 'deepSeekProxyBaseUrl': deepSeekProxyBaseUrl,\n 'remoteGatewayUrlSet': remoteUrl.isNotEmpty,\n 'pluginLoadPaths': pluginLoadPaths,\n 'hasWeChatPlugin': hasWeChatPlugin,\n },\n );\n return configPath;\n }\n\n @visibleForTesting\n Map<String, dynamic> buildEmbeddedConfigForTest({\n required Directory stateRoot,\n required int port,\n required String token,\n required String backendDeepSeekModel,\n required String deepSeekProxyBaseUrl,\n String remoteGatewayUrl = '',\n List<String> pluginLoadPaths = const [],\n bool hasWeChatPlugin = false,\n }) {\n return _buildEmbeddedConfig(\n stateRoot: stateRoot,\n port: port,\n token: token,\n backendDeepSeekModel: backendDeepSeekModel,\n deepSeekProxyBaseUrl: deepSeekProxyBaseUrl,\n remoteGatewayUrl: remoteGatewayUrl,\n pluginLoadPaths: pluginLoadPaths,\n hasWeChatPlugin: hasWeChatPlugin,\n );\n }\n\n Map<String, dynamic> _buildEmbeddedConfig({\n required Directory stateRoot,\n required int port,\n required String token,\n required String backendDeepSeekModel,\n required String deepSeekProxyBaseUrl,\n required String remoteGatewayUrl,\n required List<String> pluginLoadPaths,\n required bool hasWeChatPlugin,\n }) {\n final workspace = p.join(stateRoot.path, 'workspace');\n final remoteUrl = remoteGatewayUrl.trim();\n final desktopToolAllowList = DesktopControlPolicy.supportedTools.toList()\n ..sort();\n final canvasHostConfig = <String, dynamic>{\n 'enabled': true,\n 'root': p.join(stateRoot.path, 'canvas'),\n };\n\n return <String, dynamic>{\n 'gateway': {\n 'mode': 'local',\n 'port': port,\n 'bind': 'loopback',\n 'auth': {'mode': 'token', 'token': token},\n if (remoteUrl.isNotEmpty) 'remote': {'enabled': true, 'url': remoteUrl},\n 'http': {\n 'endpoints': {\n 'chatCompletions': {'enabled': true},\n 'responses': {'enabled': true},\n },\n },\n },\n 'models': {\n 'mode': 'merge',\n 'providers': {\n _backendDeepSeekProviderId: {\n 'baseUrl': deepSeekProxyBaseUrl,\n 'api': 'openai-completions',\n 'apiKey': 'DACHENG_OPENCLAW_PROXY_TOKEN',\n 'authHeader': true,\n 'headers': {'x-dacheng-auth-token': 'DACHENG_AUTH_TOKEN'},\n 'models': [\n {\n 'id': 'deepseek-chat',\n 'name': 'DeepSeek Chat',\n 'contextWindow': 131072,\n 'maxTokens': 8192,\n 'input': ['text'],\n 'compat': {\n 'requiresStringContent': true,\n 'strictMessageKeys': true,\n },\n },\n {\n 'id': 'deepseek-reasoner',\n 'name': 'DeepSeek Reasoner',\n 'contextWindow': 131072,\n 'maxTokens': 8192,\n 'reasoning': true,\n 'input': ['text'],\n 'compat': {\n 'requiresStringContent': true,\n 'strictMessageKeys': true,\n },\n },\n ],\n },\n },\n },\n 'browser': {\n 'enabled': true,\n 'defaultProfile': 'openclaw',\n 'headless': false,\n 'ssrfPolicy': {'dangerouslyAllowPrivateNetwork': true},\n },\n 'tools': {\n 'profile': 'full',\n 'alsoAllow': desktopToolAllowList,\n 'exec': {'host': 'gateway', 'security': 'full', 'ask': 'off'},\n },\n 'plugins': {\n 'enabled': true,\n 'deny': <String>[],\n 'load': {'paths': pluginLoadPaths},\n 'slots': {'memory': 'memory-core'},\n 'entries': <String, dynamic>{\n 'memory-core': {'enabled': true},\n 'bonjour': {'enabled': false},\n 'browser': {'enabled': true},\n 'canvas': {\n 'enabled': true,\n 'config': {'host': canvasHostConfig},\n },\n 'device-pair': {'enabled': true},\n 'file-transfer': {'enabled': true},\n 'phone-control': {'enabled': true},\n 'talk-voice': {'enabled': true},\n _desktopToolsPluginId: {'enabled': true},\n if (hasWeChatPlugin) _weChatPluginId: {'enabled': true},\n },\n },\n 'channels': {\n 'defaults': {'groupPolicy': 'allowlist'},\n if (hasWeChatPlugin)\n _weChatPluginId: {\n 'enabled': true,\n 'dmPolicy': 'pairing',\n 'allowFrom': <String>[],\n 'accounts': <String, dynamic>{},\n },\n },\n 'agents': {\n 'defaults': {\n 'workspace': workspace,\n 'model': {'primary': backendDeepSeekModel},\n },\n 'list': [\n {\n 'id': 'dacheng',\n 'default': true,\n 'workspace': workspace,\n 'model': {'primary': backendDeepSeekModel},\n },\n ],\n },\n };\n }\n\n Future<File> _ensureDesktopToolsManifest({\n required Directory stateRoot,\n required _DesktopToolsLaunch desktopTools,\n }) async {\n final configDir = Directory(p.join(stateRoot.path, 'config'));\n await configDir.create(recursive: true);\n final manifestPath = File(p.join(configDir.path, 'desktop_tools.json'));\n final tools =\n DesktopControlPolicy.supportedTools\n .map(\n (name) => {\n 'name': name,\n 'endpoint': '/v1/tools/execute',\n 'readOnly': DesktopControlPolicy.isReadOnly(name),\n 'requiresConfirmation':\n DesktopControlPolicy.requiresConfirmation(name),\n },\n )\n .toList()\n ..sort(\n (a, b) => a['name'].toString().compareTo(b['name'].toString()),\n );\n\n await manifestPath.writeAsString(\n const JsonEncoder.withIndent(' ').convert({\n 'version': 1,\n 'transport': 'http-loopback',\n 'baseUrl': desktopTools.uri?.toString(),\n 'auth': {'type': 'bearer', 'env': 'DACHENG_DESKTOP_TOOLS_TOKEN'},\n 'status': desktopTools.statusJson,\n 'tools': tools,\n }),\n );\n _diag(\n 'desktop-tools.manifest-written',\n data: {\n 'manifestPath': manifestPath.path,\n 'hasBaseUrl': desktopTools.uri != null,\n },\n );\n return manifestPath;\n }\n\n Future<File> _ensureAgentModelConfig({\n required Directory stateRoot,\n required String deepSeekProxyBaseUrl,\n }) async {\n final agentDir = Directory(\n p.join(stateRoot.path, 'state', 'agents', 'dacheng', 'agent'),\n );\n await agentDir.create(recursive: true);\n final modelsPath = File(p.join(agentDir.path, 'models.json'));\n final providerConfig = <String, dynamic>{\n 'baseUrl': deepSeekProxyBaseUrl,\n 'api': 'openai-completions',\n 'apiKey': 'DACHENG_OPENCLAW_PROXY_TOKEN',\n 'authHeader': true,\n 'headers': {'x-dacheng-auth-token': 'DACHENG_AUTH_TOKEN'},\n 'models': [\n {\n 'id': 'deepseek-chat',\n 'name': 'DeepSeek Chat',\n 'contextWindow': 131072,\n 'maxTokens': 8192,\n 'input': ['text'],\n 'compat': {'requiresStringContent': true, 'strictMessageKeys': true},\n },\n {\n 'id': 'deepseek-reasoner',\n 'name': 'DeepSeek Reasoner',\n 'contextWindow': 131072,\n 'maxTokens': 8192,\n 'reasoning': true,\n 'input': ['text'],\n 'compat': {'requiresStringContent': true, 'strictMessageKeys': true},\n },\n ],\n };\n await modelsPath.writeAsString(\n const JsonEncoder.withIndent(' ').convert({\n 'providers': {_backendDeepSeekProviderId: providerConfig},\n }),\n );\n _diag(\n 'agent-models.written',\n data: {\n 'modelsPath': modelsPath.path,\n 'provider': _backendDeepSeekProviderId,\n 'deepSeekProxyBaseUrl': deepSeekProxyBaseUrl,\n },\n );\n return modelsPath;\n }\n\n Future<_OpenClawCliLaunch> _prepareCliLaunch({\n required bool ensureGateway,\n }) async {\n if (ensureGateway) {\n await ensureStarted();\n }\n final platformKey = _platformKey;\n if (platformKey == null) {\n throw const OpenClawRuntimeException('当前平台不支持内置 OpenClaw Gateway');\n }\n final spec = await _loadSpec(platformKey, checkUpdates: false);\n final runtimeDir = await _prepareBundle(spec, platformKey);\n final stateRoot = await _stateRoot();\n final desktopToolsPluginDir = await _ensureDesktopToolsPlugin(\n stateRoot: stateRoot,\n );\n final port = await AppSettings.getOpenClawGatewayPort(\n defaultValue: spec.defaultPort,\n );\n final token = await AppSettings.getOpenClawGatewayToken();\n final backendDeepSeekModel = _backendDeepSeekModelRef(\n await AppSettings.getOpenClawDeepSeekModel(\n defaultValue: _defaultDeepSeekModelFor(spec.defaultModel),\n ),\n );\n final configPath = await _ensureConfigFile(\n runtimeDir: runtimeDir,\n stateRoot: stateRoot,\n port: port,\n token: token,\n backendDeepSeekModel: backendDeepSeekModel,\n deepSeekProxyBaseUrl: _deepSeekProxyBaseUrl(),\n remoteGatewayUrl: await AppSettings.getOpenClawRemoteGatewayUrl(),\n extraPluginLoadPaths: [desktopToolsPluginDir.path],\n );\n final nodePath = p.join(runtimeDir.path, spec.nodeExecutable);\n final cliPath = p.join(runtimeDir.path, spec.cliEntrypoint);\n if (!await File(nodePath).exists()) {\n throw OpenClawRuntimeException('OpenClaw 内置 Node 不存在: $nodePath');\n }\n if (!await File(cliPath).exists()) {\n throw OpenClawRuntimeException('OpenClaw CLI 入口不存在: $cliPath');\n }\n return _OpenClawCliLaunch(\n spec: spec,\n runtimeDir: runtimeDir,\n stateRoot: stateRoot,\n configPath: configPath,\n nodePath: nodePath,\n cliPath: cliPath,\n port: port,\n token: token,\n );\n }\n\n Map<String, String> _buildOpenClawEnvironment({\n required Directory runtimeDir,\n required Directory stateRoot,\n required File configPath,\n required int port,\n required String token,\n }) {\n final nodeBinDir = Platform.isWindows\n ? p.join(runtimeDir.path, 'node')\n : p.join(runtimeDir.path, 'node', 'bin');\n final pathKey = Platform.isWindows ? 'Path' : 'PATH';\n final currentPath =\n Platform.environment[pathKey] ?? Platform.environment['PATH'] ?? '';\n final separator = Platform.isWindows ? ';' : ':';\n final pathValue = currentPath.isEmpty\n ? nodeBinDir\n : '$nodeBinDir$separator$currentPath';\n final env = Map<String, String>.from(Platform.environment)\n ..remove('NODE_COMPILE_CACHE')\n ..addAll({\n 'OPENCLAW_GATEWAY_PORT': '$port',\n 'OPENCLAW_GATEWAY_BIND': 'loopback',\n 'OPENCLAW_GATEWAY_TOKEN': token,\n 'OPENCLAW_CONFIG_PATH': configPath.path,\n 'OPENCLAW_STATE_DIR': p.join(stateRoot.path, 'state'),\n 'OPENCLAW_AGENT_DIR': p.join(stateRoot.path, 'agents'),\n 'OPENCLAW_WORKSPACE': p.join(stateRoot.path, 'workspace'),\n 'OPENCLAW_DISABLE_BONJOUR': '1',\n 'NODE_DISABLE_COMPILE_CACHE': '1',\n pathKey: pathValue,\n if (pathKey != 'PATH') 'PATH': pathValue,\n });\n return env;\n }\n\n @visibleForTesting\n Map<String, String> buildOpenClawEnvironmentForTest({\n required Directory runtimeDir,\n required Directory stateRoot,\n required File configPath,\n required int port,\n required String token,\n }) {\n return _buildOpenClawEnvironment(\n runtimeDir: runtimeDir,\n stateRoot: stateRoot,\n configPath: configPath,\n port: port,\n token: token,\n );\n }\n\n Future<List<String>> _bundledPluginLoadPaths(Directory runtimeDir) async {\n final paths = <String>[];\n final weChatPluginDir = Directory(\n p.join(runtimeDir.path, 'plugins', _weChatPluginId),\n );\n if (await File(\n p.join(weChatPluginDir.path, 'openclaw.plugin.json'),\n ).exists()) {\n paths.add(weChatPluginDir.path);\n }\n return paths;\n }\n\n Future<Directory> _ensureDesktopToolsPlugin({\n required Directory stateRoot,\n }) async {\n final dir = Directory(\n p.join(stateRoot.path, 'plugins', _desktopToolsPluginId),\n );\n await dir.create(recursive: true);\n final tools = DesktopControlPolicy.supportedTools.toList()..sort();\n await File(p.join(dir.path, 'package.json')).writeAsString(\n const JsonEncoder.withIndent(' ').convert({\n 'name': '@dacheng/openclaw-desktop-tools',\n 'version': '1.0.0',\n 'type': 'module',\n 'openclaw': {\n 'extensions': ['./index.mjs'],\n },\n }),\n );\n await File(p.join(dir.path, 'openclaw.plugin.json')).writeAsString(\n const JsonEncoder.withIndent(' ').convert({\n 'id': _desktopToolsPluginId,\n 'name': 'Dacheng Desktop Tools',\n 'description':\n 'Expose Fabushi desktop and Chrome control bridge tools to embedded OpenClaw.',\n 'activation': {'onStartup': true},\n 'contracts': {'tools': tools},\n 'configSchema': {\n 'type': 'object',\n 'additionalProperties': false,\n 'properties': <String, dynamic>{},\n },\n }),\n );\n await File(\n p.join(dir.path, 'index.mjs'),\n ).writeAsString(_desktopToolsPluginSource(tools));\n _diag(\n 'desktop-tools.plugin-written',\n data: {'pluginDir': dir.path, 'tools': tools},\n );\n return dir;\n }\n\n String _desktopToolsPluginSource(List<String> tools) {\n final toolsJson = jsonEncode(tools);\n return '''\nconst tools = $toolsJson;\n\nconst descriptions = {\n \"desktop.observe\": \"Observe the active desktop application and visible windows.\",\n \"desktop.screenshot\": \"Capture a screenshot of the local desktop.\",\n \"desktop.windows\": \"List visible desktop windows.\",\n \"desktop.click\": \"Click a local desktop coordinate after user confirmation.\",\n \"desktop.type\": \"Type text into the focused local desktop app after user confirmation.\",\n \"desktop.hotkey\": \"Send a keyboard shortcut to the local desktop after user confirmation.\",\n \"desktop.scroll\": \"Scroll the local desktop after user confirmation.\",\n \"chrome.tabs\": \"List tabs from the paired Chrome connector.\",\n \"chrome.navigate\": \"Navigate the active or selected Chrome tab after user confirmation.\",\n \"chrome.dom_snapshot\": \"Read a structured DOM snapshot from the paired Chrome connector.\",\n \"chrome.screenshot\": \"Capture a screenshot from the paired Chrome connector.\",\n \"chrome.click\": \"Click an element or point in the paired Chrome tab after user confirmation.\",\n \"chrome.type\": \"Type into an element in the paired Chrome tab after user confirmation.\"\n};\n\nconst parameterSchemas = {\n \"desktop.click\": {\n type: \"object\",\n additionalProperties: true,\n properties: {\n x: { type: \"number\" },\n y: { type: \"number\" },\n button: { type: \"string\", enum: [\"left\", \"right\"] }\n },\n required: [\"x\", \"y\"]\n },\n \"desktop.type\": {\n type: \"object\",\n additionalProperties: true,\n properties: { text: { type: \"string\" } },\n required: [\"text\"]\n },\n \"desktop.hotkey\": {\n type: \"object\",\n additionalProperties: true,\n properties: {\n key: { type: \"string\" },\n keys: { type: \"array\", items: { type: \"string\" } }\n }\n },\n \"chrome.navigate\": {\n type: \"object\",\n additionalProperties: true,\n properties: { url: { type: \"string\" }, tabId: { type: \"integer\" } },\n required: [\"url\"]\n },\n \"chrome.click\": {\n type: \"object\",\n additionalProperties: true,\n properties: {\n selector: { type: \"string\" },\n x: { type: \"number\" },\n y: { type: \"number\" },\n tabId: { type: \"integer\" }\n }\n },\n \"chrome.type\": {\n type: \"object\",\n additionalProperties: true,\n properties: {\n selector: { type: \"string\" },\n x: { type: \"number\" },\n y: { type: \"number\" },\n text: { type: \"string\" },\n tabId: { type: \"integer\" }\n },\n required: [\"text\"]\n }\n};\n\nconst defaultSchema = {\n type: \"object\",\n additionalProperties: true,\n properties: {}\n};\n\nfunction toolResultText(value) {\n try {\n return JSON.stringify(value, null, 2);\n } catch {\n return String(value);\n }\n}\n\nasync function callBridge(toolName, args) {\n if (process.env.DACHENG_DESKTOP_TOOLS_ENABLED !== \"1\") {\n return {\n isError: true,\n content: [{ type: \"text\", text: \"Fabushi desktop tools bridge is not running.\" }]\n };\n }\n const rawBaseUrl = process.env.DACHENG_DESKTOP_TOOLS_URL || \"\";\n const baseUrl = rawBaseUrl.endsWith(\"/\") ? rawBaseUrl.slice(0, -1) : rawBaseUrl;\n const token = process.env.DACHENG_DESKTOP_TOOLS_TOKEN || \"\";\n if (!baseUrl || !token) {\n return {\n isError: true,\n content: [{ type: \"text\", text: \"Fabushi desktop tools bridge URL/token is missing.\" }]\n };\n }\n const response = await fetch(`\\${baseUrl}/v1/tools/execute`, {\n method: \"POST\",\n headers: {\n \"Authorization\": `Bearer \\${token}`,\n \"Content-Type\": \"application/json\"\n },\n body: JSON.stringify({ tool: toolName, arguments: args || {} })\n });\n const payload = await response.json().catch(() => ({}));\n const ok = response.ok && payload && payload.ok !== false;\n const data = payload && Object.prototype.hasOwnProperty.call(payload, \"data\")\n ? payload.data\n : payload;\n return {\n isError: !ok,\n content: [{ type: \"text\", text: toolResultText(data) }],\n details: data\n };\n}\n\nexport default {\n id: \"dacheng-desktop-tools\",\n name: \"Dacheng Desktop Tools\",\n description: \"Expose Fabushi desktop and Chrome control tools to embedded OpenClaw.\",\n configSchema: { type: \"object\", additionalProperties: false, properties: {} },\n register(api) {\n for (const name of tools) {\n api.registerTool({\n name,\n label: name,\n description: descriptions[name] || `Run \\${name} through the Fabushi desktop bridge.`,\n parameters: parameterSchemas[name] || defaultSchema,\n async execute(_toolCallId, args) {\n return await callBridge(name, args);\n }\n });\n }\n }\n};\n''';\n }\n\n Future<Map<String, dynamic>> _mergeEmbeddedConfig(\n File configPath,\n Map<String, dynamic> defaults,\n ) async {\n Map<String, dynamic> current = <String, dynamic>{};\n if (await configPath.exists()) {\n try {\n final decoded = jsonDecode(await configPath.readAsString());\n if (decoded is Map<String, dynamic>) {\n current = decoded;\n } else if (decoded is Map) {\n current = Map<String, dynamic>.from(decoded);\n }\n } catch (_) {\n current = <String, dynamic>{};\n }\n }\n\n final gateway = _mutableMap(current['gateway']);\n final defaultGateway = _mutableMap(defaults['gateway']);\n gateway['mode'] = 'local';\n gateway['port'] = defaultGateway['port'];\n gateway['bind'] = defaultGateway['bind'];\n gateway['auth'] = Map<String, dynamic>.from(defaultGateway['auth'] as Map);\n if (defaultGateway.containsKey('remote')) {\n gateway['remote'] = {\n ..._mutableMap(gateway['remote']),\n ..._mutableMap(defaultGateway['remote']),\n };\n } else {\n final remote = _mutableMap(gateway['remote'])..remove('url');\n if (remote.isEmpty) {\n gateway.remove('remote');\n } else {\n gateway['remote'] = remote;\n }\n }\n\n final gatewayHttp = _mutableMap(gateway['http']);\n final endpoints = _mutableMap(gatewayHttp['endpoints']);\n endpoints['chatCompletions'] = {'enabled': true};\n endpoints['responses'] = {'enabled': true};\n gatewayHttp['endpoints'] = endpoints;\n gateway['http'] = gatewayHttp;\n current['gateway'] = gateway;\n\n final env = _mutableMap(current['env']);\n env.remove('DEEPSEEK_API_KEY');\n if (env.isEmpty) {\n current.remove('env');\n } else {\n current['env'] = env;\n }\n\n final defaultModels = _mutableMap(defaults['models']);\n final models = _mutableMap(current['models']);\n models['mode'] ??= defaultModels['mode'];\n final providers = _mutableMap(models['providers']);\n final defaultProviders = _mutableMap(defaultModels['providers']);\n providers.remove('deepseek');\n providers.remove('dacheng-deepseek');\n providers.remove(_backendDeepSeekProviderId);\n for (final entry in defaultProviders.entries) {\n providers[entry.key] = entry.value is Map\n ? Map<String, dynamic>.from(entry.value as Map)\n : entry.value;\n }\n models['providers'] = providers;\n current['models'] = models;\n\n final defaultBrowser = _mutableMap(defaults['browser']);\n final browser = _mutableMap(current['browser']);\n browser.addAll(defaultBrowser);\n current['browser'] = browser;\n\n // OpenClaw 2026.6 rejects root-level canvas/canvasHost. Canvas is now\n // configured through plugins.entries.canvas.config.host.\n current.remove('canvas');\n current.remove('canvasHost');\n\n final defaultTools = _mutableMap(defaults['tools']);\n final tools = _mutableMap(current['tools']);\n tools.remove('toolSearch');\n tools.remove('fs');\n final alsoAllow = _mergeStringLists(\n tools['alsoAllow'],\n defaultTools['alsoAllow'],\n );\n final execTools = _mutableMap(tools['exec']);\n execTools.remove('mode');\n if (execTools.isNotEmpty) {\n tools['exec'] = execTools;\n }\n tools.addAll(defaultTools);\n if (alsoAllow.isEmpty) {\n tools.remove('alsoAllow');\n } else {\n tools['alsoAllow'] = alsoAllow;\n }\n current['tools'] = tools;\n\n final defaultPlugins = _mutableMap(defaults['plugins']);\n final plugins = _mutableMap(current['plugins']);\n plugins['enabled'] = true;\n final defaultEntries = _mutableMap(defaultPlugins['entries']);\n final requiredPluginIds = defaultEntries.keys.toSet();\n final allowList = _stringList(plugins['allow']);\n if (allowList.isEmpty) {\n plugins.remove('allow');\n } else {\n plugins['allow'] = _mergeStringLists(allowList, requiredPluginIds);\n }\n final denyList = _stringList(\n plugins['deny'],\n ).where((item) => !requiredPluginIds.contains(item)).toList();\n if (denyList.isEmpty) {\n plugins.remove('deny');\n } else {\n plugins['deny'] = denyList;\n }\n final defaultLoad = _mutableMap(defaultPlugins['load']);\n final load = _mutableMap(plugins['load']);\n load['paths'] = _mergeStringLists(load['paths'], defaultLoad['paths']);\n plugins['load'] = load;\n final slots = _mutableMap(plugins['slots']);\n slots.addAll(_mutableMap(defaultPlugins['slots']));\n plugins['slots'] = slots;\n final entries = _mutableMap(plugins['entries']);\n for (final entry in defaultEntries.entries) {\n final currentEntry = _mutableMap(entries[entry.key]);\n currentEntry.addAll(\n entry.value is Map\n ? Map<String, dynamic>.from(entry.value as Map)\n : {'enabled': true},\n );\n entries[entry.key] = currentEntry;\n }\n plugins['entries'] = entries;\n current['plugins'] = plugins;\n\n final defaultChannels = _mutableMap(defaults['channels']);\n final channels = _mutableMap(current['channels']);\n final defaultChannelDefaults = _mutableMap(defaultChannels['defaults']);\n if (defaultChannelDefaults.isNotEmpty) {\n channels['defaults'] = {\n ..._mutableMap(channels['defaults']),\n ...defaultChannelDefaults,\n };\n }\n for (final entry in defaultChannels.entries) {\n if (entry.key == 'defaults') continue;\n final currentChannel = _mutableMap(channels[entry.key]);\n currentChannel.addAll(\n entry.value is Map\n ? Map<String, dynamic>.from(entry.value as Map)\n : <String, dynamic>{},\n );\n channels[entry.key] = currentChannel;\n }\n current['channels'] = channels;\n\n final agents = _mutableMap(current['agents']);\n final defaultAgents = _mutableMap(defaults['agents']);\n final defaultAgentDefaults = _mutableMap(defaultAgents['defaults']);\n final agentDefaults = _mutableMap(agents['defaults']);\n agentDefaults['workspace'] = defaultAgentDefaults['workspace'];\n agentDefaults['model'] = defaultAgentDefaults['model'];\n agents['defaults'] = agentDefaults;\n\n final defaultList = (defaultAgents['list'] as List? ?? const [])\n .whereType<Map>()\n .map((item) => Map<String, dynamic>.from(item))\n .toList();\n final defaultDacheng = defaultList.isNotEmpty\n ? defaultList.first\n : <String, dynamic>{\n 'id': 'dacheng',\n 'default': true,\n 'workspace': agentDefaults['workspace'],\n 'model': agentDefaults['model'],\n };\n final list = (agents['list'] as List? ?? const [])\n .map((item) => item is Map ? Map<String, dynamic>.from(item) : item)\n .toList();\n var foundDacheng = false;\n for (var i = 0; i < list.length; i++) {\n final item = list[i];\n if (item is! Map<String, dynamic>) continue;\n if (item['id'] != 'dacheng') continue;\n foundDacheng = true;\n item['default'] = true;\n item['workspace'] = defaultDacheng['workspace'];\n item['model'] = defaultDacheng['model'];\n list[i] = item;\n }\n if (!foundDacheng) {\n list.add(defaultDacheng);\n }\n agents['list'] = list.isEmpty ? defaultList : list;\n current['agents'] = agents;\n\n return current;\n }\n\n @visibleForTesting\n Future<Map<String, dynamic>> mergeEmbeddedConfigForTest(\n File configPath,\n Map<String, dynamic> defaults,\n ) {\n return _mergeEmbeddedConfig(configPath, defaults);\n }\n\n String _gatewayChatModel(String model) {\n final trimmed = model.trim();\n if (trimmed == 'openclaw' || trimmed.startsWith('openclaw/')) {\n return trimmed;\n }\n return _defaultGatewayModel;\n }\n\n String _defaultDeepSeekModelFor(String model) {\n final trimmed = model.trim();\n if (trimmed.startsWith('deepseek/')) return trimmed;\n return _defaultDeepSeekModel;\n }\n\n String _backendDeepSeekModelRef(String model) {\n final trimmed = model.trim().isEmpty ? _defaultDeepSeekModel : model.trim();\n final modelId = trimmed.contains('/')\n ? trimmed.split('/').last.trim()\n : trimmed;\n final normalizedModelId = modelId.isEmpty ? 'deepseek-chat' : modelId;\n return '$_backendDeepSeekProviderId/$normalizedModelId';\n }\n\n String _deepSeekProxyBaseUrl() {\n final baseUrl = AppConfig.currentAiBackendUrl.replaceFirst(\n RegExp(r'/+$'),\n '',\n );\n return '$baseUrl/api/openclaw/deepseek/v1';\n }\n\n Map<String, dynamic> _mutableMap(Object? value) {\n if (value is Map<String, dynamic>) return Map<String, dynamic>.from(value);\n if (value is Map) return Map<String, dynamic>.from(value);\n return <String, dynamic>{};\n }\n\n List<String> _stringList(Object? value) {\n if (value is! Iterable) return <String>[];\n return value\n .map((item) => item.toString().trim())\n .where((item) => item.isNotEmpty)\n .toList();\n }\n\n List<String> _mergeStringLists(Object? first, Object? second) {\n final merged = <String>{};\n merged.addAll(_stringList(first));\n merged.addAll(_stringList(second));\n return merged.toList();\n }\n\n void _appendBounded(StringBuffer buffer, String chunk) {\n const maxChars = 120000;\n if (chunk.isEmpty || buffer.length >= maxChars) return;\n final remaining = maxChars - buffer.length;\n buffer.write(\n chunk.length <= remaining ? chunk : chunk.substring(0, remaining),\n );\n }\n\n void _captureLogs(Process process) {\n void addLog(String line) {\n final text = line.trim();\n if (text.isEmpty) return;\n _recentLogs.insert(0, text);\n if (_recentLogs.length > 80) {\n _recentLogs.removeRange(80, _recentLogs.length);\n }\n if (kDebugMode) {\n debugPrint('[OpenClaw] $text');\n }\n _diag('process.output', data: {'line': text});\n }\n\n process.stdout\n .transform(utf8.decoder)\n .transform(const LineSplitter())\n .listen(addLog, onError: (_) {});\n process.stderr\n .transform(utf8.decoder)\n .transform(const LineSplitter())\n .listen(addLog, onError: (_) {});\n }\n\n bool _recentLogsContainRuntimeModuleFailure() {\n return _recentLogs.any((line) {\n final lower = line.toLowerCase();\n return lower.contains('err_vm_module_link_failure') ||\n lower.contains('is not in cache') ||\n lower.contains('request for') && lower.contains('.js');\n });\n }\n\n Future<Directory> _stateRoot() async {\n final support = await getApplicationSupportDirectory();\n final dir = Directory(p.join(support.path, 'openclaw_embedded'));\n await dir.create(recursive: true);\n return dir;\n }\n\n Future<_DesktopToolsLaunch> _ensureDesktopTools() async {\n try {\n final status = await DesktopControlBridge.instance.ensureStarted();\n _diag(\n 'desktop-tools.started',\n data: {\n 'bridgeRunning': status.bridgeRunning,\n 'bridgeUri': status.bridgeUri?.toString(),\n 'message': status.message,\n },\n );\n return _DesktopToolsLaunch(\n uri: status.bridgeUri,\n token: await DesktopControlBridge.instance.bridgeToken,\n statusJson: status.toJson(),\n );\n } catch (error) {\n _diag('desktop-tools.error', error: error);\n return _DesktopToolsLaunch(\n uri: null,\n token: null,\n statusJson: {\n 'enabledByBuild': false,\n 'bridgeRunning': false,\n 'message': error.toString(),\n },\n );\n }\n }\n\n Future<Directory> _runtimeDir(\n _OpenClawBundleSpec spec,\n String platformKey,\n ) async {\n final support = await getApplicationSupportDirectory();\n final runtimeBase = Platform.isMacOS\n ? Directory(\n p.join(\n support.parent.path,\n '${p.basename(support.path)}.openclaw_runtime',\n ),\n )\n : Directory(p.join(support.path, 'openclaw_embedded'));\n return Directory(\n p.join(runtimeBase.path, 'runtime', spec.version, platformKey),\n );\n }\n\n Future<Directory?> _bundledRuntimeDir(\n _OpenClawBundleSpec spec,\n String platformKey,\n ) async {\n if (!Platform.isMacOS) return null;\n\n final contentsDir = Directory(\n p.dirname(p.dirname(Platform.resolvedExecutable)),\n );\n final candidates = <Directory>[\n Directory(\n p.join(\n contentsDir.path,\n 'Frameworks',\n 'App.framework',\n 'Resources',\n 'flutter_assets',\n 'assets',\n 'openclaw',\n platformKey,\n ),\n ),\n Directory(\n p.join(\n contentsDir.path,\n 'Frameworks',\n 'App.framework',\n 'Versions',\n 'A',\n 'Resources',\n 'flutter_assets',\n 'assets',\n 'openclaw',\n platformKey,\n ),\n ),\n Directory(\n p.join(\n contentsDir.path,\n 'Resources',\n 'flutter_assets',\n 'assets',\n 'openclaw',\n platformKey,\n ),\n ),\n ];\n\n for (final dir in candidates) {\n final nodePath = File(p.join(dir.path, spec.nodeExecutable));\n final cliPath = File(p.join(dir.path, spec.cliEntrypoint));\n if (await nodePath.exists() && await cliPath.exists()) {\n return dir;\n }\n }\n _diag(\n 'bundle.macos-bundled-runtime-missing',\n data: {\n 'platformKey': platformKey,\n 'contentsDir': contentsDir.path,\n 'candidates': candidates.map((item) => item.path).toList(),\n },\n );\n return null;\n }\n\n Future<Directory?> _runtimeDirOverride(\n _OpenClawBundleSpec spec,\n String platformKey,\n ) async {\n final raw = _configuredRuntimeDirOverride(platformKey);\n if (raw == null) return null;\n\n final dir = Directory(raw);\n final nodePath = File(p.join(dir.path, spec.nodeExecutable));\n final cliPath = File(p.join(dir.path, spec.cliEntrypoint));\n final nodeExists = await nodePath.exists();\n final cliExists = await cliPath.exists();\n _diag(\n 'bundle.override.paths',\n data: {\n 'platformKey': platformKey,\n 'runtimeDir': dir.path,\n 'nodePath': nodePath.path,\n 'nodeExists': nodeExists,\n 'cliPath': cliPath.path,\n 'cliExists': cliExists,\n },\n );\n\n if (!nodeExists || !cliExists) {\n throw OpenClawRuntimeException(\n 'DACHENG_OPENCLAW_RUNTIME_DIR 指向的 OpenClaw runtime 不完整: ${dir.path}',\n );\n }\n return dir;\n }\n\n String? _configuredRuntimeDirOverride(String platformKey) {\n final raw = _runtimeDirOverrideDefine.trim().isNotEmpty\n ? _runtimeDirOverrideDefine.trim()\n : Platform.environment['DACHENG_OPENCLAW_RUNTIME_DIR']?.trim();\n if (raw == null || raw.isEmpty) return null;\n return p.normalize(raw.replaceAll('{platform}', platformKey));\n }\n\n Future<Directory> _legacyRuntimeDir(\n _OpenClawBundleSpec spec,\n String platformKey,\n ) async {\n final support = await getApplicationSupportDirectory();\n return Directory(\n p.join(\n support.path,\n 'openclaw_embedded',\n 'runtime',\n spec.version,\n platformKey,\n ),\n );\n }\n\n OpenClawRuntimeStatus _remember(OpenClawRuntimeStatus status) {\n _lastStatus = status;\n return status;\n }\n\n String? get _platformKey {\n final abi = Abi.current().toString().toLowerCase();\n if (Platform.isMacOS) {\n return abi.contains('arm64') ? 'macos-arm64' : 'macos-x64';\n }\n if (Platform.isWindows) {\n return abi.contains('arm64') ? 'windows-arm64' : 'windows-x64';\n }\n if (Platform.isLinux) {\n return abi.contains('arm64') ? 'linux-arm64' : 'linux-x64';\n }\n return null;\n }\n\n OpenClawRuntimeStatus? get lastStatus => _lastStatus;\n\n void _diag(\n String message, {\n Map<String, Object?> data = const {},\n Object? error,\n StackTrace? stackTrace,\n }) {\n unawaited(\n DiagnosticLogService.instance.log(\n 'openclaw.runtime',\n message,\n data: data,\n error: error,\n stackTrace: stackTrace,\n ),\n );\n }\n}\n\nint? _readInt(Object? value) {\n if (value is int) return value;\n if (value is num) return value.toInt();\n if (value is String) return int.tryParse(value);\n return null;\n}\n\nObject? _decodeJsonOrNull(String body) {\n try {\n return jsonDecode(body);\n } catch (_) {\n return null;\n }\n}\n\nbool _looksLikeHealthPayload(Object? payload) {\n if (payload is! Map) return false;\n if (payload['ok'] == true) return true;\n final status = payload['status']?.toString().toLowerCase();\n return status == 'live' || status == 'ok' || status == 'healthy';\n}\n\nbool _looksLikeModelsPayload(Object? payload) {\n if (payload is List) return true;\n if (payload is! Map) return false;\n return payload['data'] is List ||\n payload['models'] is List ||\n payload['object'] == 'list';\n}\n\nString _previewBody(String body) {\n final trimmed = body.trim();\n return trimmed.length <= 80 ? trimmed : trimmed.substring(0, 80);\n}\n\nString _previewProcessOutput(Object? output) {\n final text = output?.toString().trim() ?? '';\n return text.length <= 200 ? text : text.substring(0, 200);\n}\n"}, "dc1c9f261e7ed7f15eb1e20fd6dbd40bbd416840": {"t": 1786068343.5551102, "n": 97198, "h": "2aff8c1751728d25", "c": "import 'dart:async';\nimport 'dart:convert';\nimport 'dart:ffi' show Abi;\nimport 'dart:io';\nimport 'dart:math' as math;\nimport 'package:archive/archive_io.dart';\nimport 'package:convert/convert.dart';\nimport 'package:crypto/crypto.dart';\nimport 'package:flutter/foundation.dart';\nimport 'package:flutter/services.dart';\nimport 'package:http/http.dart' as http;\nimport 'package:path/path.dart' as p;\nimport 'package:path_provider/path_provider.dart';\n\nimport '../../core/config/app_config.dart';\nimport '../app_settings.dart';\nimport '../diagnostic_log_service.dart';\nimport '../desktop_control/desktop_control_bridge.dart';\nimport '../desktop_control/desktop_control_policy.dart';\nimport 'openclaw_port_resolver.dart';\n\nenum OpenClawRuntimeState {\n unsupported,\n notBundled,\n stopped,\n starting,\n running,\n failed,\n}\n\nclass OpenClawRuntimeStatus {\n final OpenClawRuntimeState state;\n final String message;\n final int? port;\n final String? platformKey;\n final String? runtimePath;\n final Map<String, dynamic>? desktopToolsStatus;\n final DateTime checkedAt;\n\n const OpenClawRuntimeStatus({\n required this.state,\n required this.message,\n this.port,\n this.platformKey,\n this.runtimePath,\n this.desktopToolsStatus,\n required this.checkedAt,\n });\n\n bool get isHealthy => state == OpenClawRuntimeState.running;\n\n String get label {\n switch (state) {\n case OpenClawRuntimeState.unsupported:\n return '不支持';\n case OpenClawRuntimeState.notBundled:\n return '未内置';\n case OpenClawRuntimeState.stopped:\n return '未启动';\n case OpenClawRuntimeState.starting:\n return '启动中';\n case OpenClawRuntimeState.running:\n return '运行中';\n case OpenClawRuntimeState.failed:\n return '异常';\n }\n }\n}\n\nclass OpenClawGatewayTarget {\n final Uri baseUri;\n final String token;\n final String model;\n final String? modelOverride;\n final Uri? desktopToolsUri;\n final String? desktopToolsToken;\n final Map<String, dynamic>? desktopToolsStatus;\n\n const OpenClawGatewayTarget({\n required this.baseUri,\n required this.token,\n required this.model,\n this.modelOverride,\n this.desktopToolsUri,\n this.desktopToolsToken,\n this.desktopToolsStatus,\n });\n}\n\nclass OpenClawCliResult {\n final List<String> args;\n final int exitCode;\n final String stdout;\n final String stderr;\n final bool timedOut;\n\n const OpenClawCliResult({\n required this.args,\n required this.exitCode,\n required this.stdout,\n required this.stderr,\n this.timedOut = false,\n });\n\n bool get succeeded => exitCode == 0 && !timedOut;\n\n String get command => 'openclaw ${args.join(' ')}';\n\n String get combinedOutput {\n final parts = <String>[\n if (stdout.trim().isNotEmpty) stdout.trimRight(),\n if (stderr.trim().isNotEmpty) stderr.trimRight(),\n ];\n return parts.join('\\n');\n }\n\n OpenClawCliResult append(OpenClawCliResult next) {\n return OpenClawCliResult(\n args: [...args, '&&', ...next.args],\n exitCode: next.exitCode,\n stdout: [\n if (stdout.trim().isNotEmpty) stdout.trimRight(),\n if (next.stdout.trim().isNotEmpty) next.stdout.trimRight(),\n ].join('\\n'),\n stderr: [\n if (stderr.trim().isNotEmpty) stderr.trimRight(),\n if (next.stderr.trim().isNotEmpty) next.stderr.trimRight(),\n ].join('\\n'),\n timedOut: timedOut || next.timedOut,\n );\n }\n}\n\nclass _OpenClawBundleSpec {\n final String version;\n final int defaultPort;\n final String defaultModel;\n final String? defaultModelOverride;\n final String nodeExecutable;\n final String cliEntrypoint;\n final List<String> gatewayArgs;\n final bool downloaded;\n\n const _OpenClawBundleSpec({\n required this.version,\n required this.defaultPort,\n required this.defaultModel,\n this.defaultModelOverride,\n required this.nodeExecutable,\n required this.cliEntrypoint,\n required this.gatewayArgs,\n this.downloaded = false,\n });\n\n Map<String, dynamic> toJson() => {\n 'schema': 1,\n 'version': version,\n 'defaultPort': defaultPort,\n 'defaultModel': defaultModel,\n if (defaultModelOverride != null)\n 'defaultModelOverride': defaultModelOverride,\n 'nodeExecutable': nodeExecutable,\n 'cliEntrypoint': cliEntrypoint,\n 'gatewayArgs': gatewayArgs,\n 'downloaded': downloaded,\n };\n}\n\nclass _OpenClawRuntimeRelease {\n final String version;\n final Uri archiveUri;\n final String sha256Hex;\n final int size;\n final int defaultPort;\n final String defaultModel;\n final String? defaultModelOverride;\n final String nodeExecutable;\n final String cliEntrypoint;\n final List<String> gatewayArgs;\n\n const _OpenClawRuntimeRelease({\n required this.version,\n required this.archiveUri,\n required this.sha256Hex,\n required this.size,\n required this.defaultPort,\n required this.defaultModel,\n this.defaultModelOverride,\n required this.nodeExecutable,\n required this.cliEntrypoint,\n required this.gatewayArgs,\n });\n\n _OpenClawBundleSpec toBundleSpec() => _OpenClawBundleSpec(\n version: version,\n defaultPort: defaultPort,\n defaultModel: defaultModel,\n defaultModelOverride: defaultModelOverride,\n nodeExecutable: nodeExecutable,\n cliEntrypoint: cliEntrypoint,\n gatewayArgs: gatewayArgs,\n downloaded: true,\n );\n}\n\nclass _DesktopToolsLaunch {\n final Uri? uri;\n final String? token;\n final Map<String, dynamic> statusJson;\n\n const _DesktopToolsLaunch({\n required this.uri,\n required this.token,\n required this.statusJson,\n });\n}\n\nclass _OpenClawCliLaunch {\n final _OpenClawBundleSpec spec;\n final Directory runtimeDir;\n final Directory stateRoot;\n final File configPath;\n final String nodePath;\n final String cliPath;\n final int port;\n final String token;\n\n const _OpenClawCliLaunch({\n required this.spec,\n required this.runtimeDir,\n required this.stateRoot,\n required this.configPath,\n required this.nodePath,\n required this.cliPath,\n required this.port,\n required this.token,\n });\n}\n\nclass OpenClawRuntimeException implements Exception {\n final String message;\n\n const OpenClawRuntimeException(this.message);\n\n @override\n String toString() => message;\n}\n\nclass OpenClawRuntime {\n OpenClawRuntime._();\n\n static final OpenClawRuntime instance = OpenClawRuntime._();\n\n static const String _manifestAsset = 'assets/openclaw/bundle_manifest.json';\n static const String _runtimeDirOverrideDefine = String.fromEnvironment(\n 'DACHENG_OPENCLAW_RUNTIME_DIR',\n );\n static const String _defaultGatewayModel =\n AppSettings.defaultOpenClawGatewayModel;\n static const String _defaultDeepSeekModel =\n AppSettings.defaultOpenClawDeepSeekModel;\n static const String _backendDeepSeekProviderId = 'dacheng-deepseek-proxy';\n static const String _desktopToolsPluginId = 'dacheng-desktop-tools';\n static const String _weChatPluginId = 'openclaw-weixin';\n static const String _weChatPluginPackage = '@tencent-weixin/openclaw-weixin';\n static const Duration _startupTimeout = Duration(seconds: 120);\n static const Duration _probeTimeout = Duration(seconds: 3);\n static const Duration _cliDefaultTimeout = Duration(seconds: 45);\n\n Process? _process;\n Future<OpenClawGatewayTarget>? _starting;\n String _processAuthToken = '';\n OpenClawRuntimeStatus? _lastStatus;\n final List<String> _recentLogs = <String>[];\n final OpenClawPortResolver _portResolver = const OpenClawPortResolver();\n\n Future<OpenClawRuntimeStatus> getStatus({bool probe = true}) async {\n final platformKey = _platformKey;\n final port = await AppSettings.getOpenClawGatewayPort();\n _diag(\n 'status.start',\n data: {'platformKey': platformKey, 'port': port, 'probe': probe},\n );\n\n if (platformKey == null) {\n return _remember(\n OpenClawRuntimeStatus(\n state: OpenClawRuntimeState.unsupported,\n message: '当前平台不是 macOS / Windows / Linux 桌面端',\n port: port,\n checkedAt: DateTime.now(),\n ),\n );\n }\n\n try {\n final spec = await _loadSpec(platformKey, checkUpdates: false);\n final overrideRuntimeDir = await _runtimeDirOverride(spec, platformKey);\n final bundledRuntimeDir = overrideRuntimeDir == null\n ? await _bundledRuntimeDir(spec, platformKey)\n : null;\n final runtimeDir =\n overrideRuntimeDir ??\n bundledRuntimeDir ??\n await _runtimeDir(spec, platformKey);\n final nodePath = p.join(runtimeDir.path, spec.nodeExecutable);\n final cliPath = p.join(runtimeDir.path, spec.cliEntrypoint);\n final nodeExists = await File(nodePath).exists();\n final cliExists = await File(cliPath).exists();\n _diag(\n 'status.paths',\n data: {\n 'platformKey': platformKey,\n 'runtimeDir': runtimeDir.path,\n 'nodePath': nodePath,\n 'nodeExists': nodeExists,\n 'cliPath': cliPath,\n 'cliExists': cliExists,\n 'runtimeSource': overrideRuntimeDir != null\n ? 'override'\n : (bundledRuntimeDir == null ? 'cache' : 'bundle'),\n },\n );\n\n if (!nodeExists || !cliExists) {\n final hasBundleAssets = await _hasRequiredBundleAssets(\n platformKey,\n spec,\n );\n _diag(\n 'status.bundle-assets',\n data: {\n 'platformKey': platformKey,\n 'hasBundleAssets': hasBundleAssets,\n 'nodeAsset': 'assets/openclaw/$platformKey/${spec.nodeExecutable}',\n 'cliAsset': 'assets/openclaw/$platformKey/${spec.cliEntrypoint}',\n },\n );\n return _remember(\n OpenClawRuntimeStatus(\n state: hasBundleAssets\n ? OpenClawRuntimeState.stopped\n : OpenClawRuntimeState.notBundled,\n message: hasBundleAssets\n ? '内置 OpenClaw 已随 App 打包,尚未释放或启动'\n : '当前 App 包未包含 $platformKey 的 OpenClaw runtime',\n port: port,\n platformKey: platformKey,\n runtimePath: runtimeDir.path,\n checkedAt: DateTime.now(),\n ),\n );\n }\n\n if (probe) {\n final healthy = await _probe(\n port,\n await AppSettings.getOpenClawGatewayToken(),\n );\n if (healthy) {\n return _remember(\n OpenClawRuntimeStatus(\n state: OpenClawRuntimeState.running,\n message: '本机 OpenClaw Gateway 正在运行',\n port: port,\n platformKey: platformKey,\n runtimePath: runtimeDir.path,\n checkedAt: DateTime.now(),\n ),\n );\n }\n }\n\n return _remember(\n OpenClawRuntimeStatus(\n state: _process == null\n ? OpenClawRuntimeState.stopped\n : OpenClawRuntimeState.starting,\n message: _process == null ? '本机 OpenClaw 未启动' : '本机 OpenClaw 正在启动',\n port: port,\n platformKey: platformKey,\n runtimePath: runtimeDir.path,\n checkedAt: DateTime.now(),\n ),\n );\n } catch (error) {\n _diag('status.error', error: error);\n return _remember(\n OpenClawRuntimeStatus(\n state: OpenClawRuntimeState.failed,\n message: error.toString(),\n port: port,\n platformKey: platformKey,\n checkedAt: DateTime.now(),\n ),\n );\n }\n }\n\n Future<OpenClawGatewayTarget> ensureStarted({\n String? authToken,\n String? username,\n bool isMember = false,\n }) {\n _starting ??= _ensureStartedInternal(\n authToken: authToken,\n username: username,\n isMember: isMember,\n ).whenComplete(() => _starting = null);\n return _starting!;\n }\n\n Future<OpenClawRuntimeStatus> restart() async {\n await stop();\n try {\n await ensureStarted();\n return getStatus();\n } catch (error) {\n return _remember(\n OpenClawRuntimeStatus(\n state: OpenClawRuntimeState.failed,\n message: error.toString(),\n port: await AppSettings.getOpenClawGatewayPort(),\n platformKey: _platformKey,\n checkedAt: DateTime.now(),\n ),\n );\n }\n }\n\n Future<void> stop() async {\n final process = _process;\n _process = null;\n _processAuthToken = '';\n if (process == null) return;\n process.kill();\n try {\n await process.exitCode.timeout(const Duration(seconds: 5));\n } catch (_) {\n process.kill();\n }\n }\n\n Future<OpenClawCliResult> createMobilePairingCode({bool remote = true}) {\n return runCli([\n 'qr',\n if (remote) '--remote',\n '--json',\n ], timeout: const Duration(seconds: 30));\n }\n\n Future<OpenClawCliResult> loginWeChat() {\n return runCli([\n 'channels',\n 'login',\n '--channel',\n _weChatPluginId,\n ], timeout: const Duration(minutes: 3));\n }\n\n Future<OpenClawCliResult> inspectChannels() {\n return runCli([\n 'channels',\n 'status',\n '--probe',\n ], timeout: const Duration(seconds: 40));\n }\n\n Future<OpenClawCliResult> installWeChatPlugin() async {\n final install = await runCli(\n ['plugins', 'install', _weChatPluginPackage, '--force'],\n timeout: const Duration(minutes: 3),\n ensureGateway: false,\n );\n if (!install.succeeded) return install;\n final enable = await runCli(\n ['config', 'set', 'plugins.entries.$_weChatPluginId.enabled', 'true'],\n timeout: const Duration(seconds: 30),\n ensureGateway: false,\n );\n return install.append(enable);\n }\n\n Future<OpenClawCliResult> runCli(\n List<String> args, {\n Duration timeout = _cliDefaultTimeout,\n bool ensureGateway = true,\n }) async {\n if (args.isEmpty) {\n throw const OpenClawRuntimeException('OpenClaw CLI 参数不能为空');\n }\n final launch = await _prepareCliLaunch(ensureGateway: ensureGateway);\n final processArgs = <String>[launch.cliPath, ...args];\n _diag(\n 'cli.start',\n data: {\n 'args': args,\n 'runtimeDir': launch.runtimeDir.path,\n 'configPath': launch.configPath.path,\n 'timeoutSeconds': timeout.inSeconds,\n },\n );\n\n final process = await Process.start(\n launch.nodePath,\n processArgs,\n workingDirectory: launch.runtimeDir.path,\n environment: _buildOpenClawEnvironment(\n runtimeDir: launch.runtimeDir,\n stateRoot: launch.stateRoot,\n configPath: launch.configPath,\n port: launch.port,\n token: launch.token,\n ),\n mode: ProcessStartMode.normal,\n runInShell: false,\n );\n final stdoutBuffer = StringBuffer();\n final stderrBuffer = StringBuffer();\n final stdoutSub = process.stdout\n .transform(utf8.decoder)\n .listen((chunk) => _appendBounded(stdoutBuffer, chunk));\n final stderrSub = process.stderr\n .transform(utf8.decoder)\n .listen((chunk) => _appendBounded(stderrBuffer, chunk));\n\n var timedOut = false;\n int exitCode;\n try {\n exitCode = await process.exitCode.timeout(timeout);\n } on TimeoutException {\n timedOut = true;\n process.kill();\n exitCode = await process.exitCode.timeout(\n const Duration(seconds: 5),\n onTimeout: () => -1,\n );\n } finally {\n await stdoutSub.cancel();\n await stderrSub.cancel();\n }\n\n final result = OpenClawCliResult(\n args: args,\n exitCode: exitCode,\n stdout: stdoutBuffer.toString(),\n stderr: stderrBuffer.toString(),\n timedOut: timedOut,\n );\n _diag(\n result.succeeded ? 'cli.complete' : 'cli.failed',\n data: {\n 'args': args,\n 'exitCode': exitCode,\n 'timedOut': timedOut,\n 'stdoutPreview': _previewProcessOutput(result.stdout),\n 'stderrPreview': _previewProcessOutput(result.stderr),\n },\n );\n return result;\n }\n\n Future<OpenClawGatewayTarget> _ensureStartedInternal({\n String? authToken,\n String? username,\n bool isMember = false,\n }) async {\n final platformKey = _platformKey;\n _diag('ensure-start.start', data: {'platformKey': platformKey});\n if (platformKey == null) {\n throw const OpenClawRuntimeException('当前平台不支持内置 OpenClaw Gateway');\n }\n\n final spec = await _loadSpec(platformKey, checkUpdates: true);\n final requestedPort = await AppSettings.getOpenClawGatewayPort(\n defaultValue: spec.defaultPort,\n );\n final token = await AppSettings.getOpenClawGatewayToken();\n final savedModel = await AppSettings.getOpenClawModel(\n defaultValue: spec.defaultModel,\n );\n final model = _gatewayChatModel(savedModel);\n final requestedAuthToken = authToken?.trim() ?? '';\n final backendDeepSeekModel = _backendDeepSeekModelRef(\n await AppSettings.getOpenClawDeepSeekModel(\n defaultValue: _defaultDeepSeekModelFor(spec.defaultModel),\n ),\n );\n final deepSeekProxyBaseUrl = _deepSeekProxyBaseUrl();\n final modelOverride = await AppSettings.getOpenClawModelOverride(\n defaultValue: spec.defaultModelOverride ?? '',\n );\n final remoteGatewayUrl = await AppSettings.getOpenClawRemoteGatewayUrl();\n final desktopTools = await _ensureDesktopTools();\n _diag(\n 'ensure-start.config',\n data: {\n 'platformKey': platformKey,\n 'requestedPort': requestedPort,\n 'model': model,\n 'rawModel': savedModel,\n 'backendDeepSeekModel': backendDeepSeekModel,\n 'deepSeekProxyBaseUrl': deepSeekProxyBaseUrl,\n 'hasAuthToken': requestedAuthToken.isNotEmpty,\n 'modelOverrideSet': modelOverride.trim().isNotEmpty,\n 'remoteGatewayUrlSet': remoteGatewayUrl.trim().isNotEmpty,\n 'desktopToolsUri': desktopTools.uri?.toString(),\n 'desktopToolsStatus': desktopTools.statusJson,\n },\n );\n\n final canReuseStartedProcess =\n _process != null && _processAuthToken == requestedAuthToken;\n if (canReuseStartedProcess &&\n await _probe(requestedPort, token, context: 'pre-start')) {\n _diag('ensure-start.reuse-existing', data: {'port': requestedPort});\n return OpenClawGatewayTarget(\n baseUri: Uri.parse('http://127.0.0.1:$requestedPort'),\n token: token,\n model: model,\n modelOverride: modelOverride.trim().isEmpty\n ? null\n : modelOverride.trim(),\n desktopToolsUri: desktopTools.uri,\n desktopToolsToken: desktopTools.token,\n desktopToolsStatus: desktopTools.statusJson,\n );\n } else if (_process != null) {\n _diag(\n 'ensure-start.restart-auth-changed',\n data: {\n 'hasPreviousAuthToken': _processAuthToken.isNotEmpty,\n 'hasRequestedAuthToken': requestedAuthToken.isNotEmpty,\n },\n );\n await stop();\n }\n\n final runtimeDir = await _prepareBundle(spec, platformKey);\n final stateRoot = await _stateRoot();\n final desktopToolsPluginDir = await _ensureDesktopToolsPlugin(\n stateRoot: stateRoot,\n );\n if (_process != null) {\n _diag(\n 'process.stop-stale-before-start',\n data: {'requestedPort': requestedPort},\n );\n await stop();\n }\n final portCandidate = await _portResolver.resolve(requestedPort);\n final port = portCandidate.port;\n if (portCandidate.isFallback) {\n await AppSettings.setOpenClawGatewayPort(port);\n _diag(\n 'ensure-start.fallback-port',\n data: {\n 'requestedPort': requestedPort,\n 'selectedPort': port,\n if (portCandidate.reason != null) 'reason': portCandidate.reason,\n },\n );\n } else {\n _diag(\n 'ensure-start.port-bindable',\n data: {'requestedPort': requestedPort, 'selectedPort': port},\n );\n }\n final configPath = await _ensureConfigFile(\n runtimeDir: runtimeDir,\n stateRoot: stateRoot,\n port: port,\n token: token,\n backendDeepSeekModel: backendDeepSeekModel,\n deepSeekProxyBaseUrl: deepSeekProxyBaseUrl,\n remoteGatewayUrl: remoteGatewayUrl,\n extraPluginLoadPaths: [desktopToolsPluginDir.path],\n );\n await _ensureAgentModelConfig(\n stateRoot: stateRoot,\n deepSeekProxyBaseUrl: deepSeekProxyBaseUrl,\n );\n final desktopToolsManifestPath = await _ensureDesktopToolsManifest(\n stateRoot: stateRoot,\n desktopTools: desktopTools,\n );\n\n final nodePath = p.join(runtimeDir.path, spec.nodeExecutable);\n final cliPath = p.join(runtimeDir.path, spec.cliEntrypoint);\n if (!await File(nodePath).exists()) {\n _diag('ensure-start.missing-node', data: {'nodePath': nodePath});\n throw OpenClawRuntimeException('OpenClaw 内置 Node 不存在: $nodePath');\n }\n if (!await File(cliPath).exists()) {\n _diag('ensure-start.missing-cli', data: {'cliPath': cliPath});\n throw OpenClawRuntimeException('OpenClaw CLI 入口不存在: $cliPath');\n }\n\n _remember(\n OpenClawRuntimeStatus(\n state: OpenClawRuntimeState.starting,\n message: '正在启动内置 OpenClaw Gateway',\n port: port,\n platformKey: platformKey,\n runtimePath: runtimeDir.path,\n checkedAt: DateTime.now(),\n ),\n );\n\n final args = <String>[\n cliPath,\n ...spec.gatewayArgs.map((arg) => arg.replaceAll('{port}', '$port')),\n ];\n _recentLogs.clear();\n _diag(\n 'process.starting',\n data: {\n 'nodePath': nodePath,\n 'args': args,\n 'workingDirectory': runtimeDir.path,\n 'configPath': configPath.path,\n 'desktopToolsPluginPath': desktopToolsPluginDir.path,\n 'desktopToolsManifestPath': desktopToolsManifestPath.path,\n 'requestedPort': requestedPort,\n 'selectedPort': port,\n },\n );\n\n final env =\n _buildOpenClawEnvironment(\n runtimeDir: runtimeDir,\n stateRoot: stateRoot,\n configPath: configPath,\n port: port,\n token: token,\n )..addAll({\n 'DACHENG_DESKTOP_TOOLS_ENABLED': desktopTools.uri == null ? '0' : '1',\n 'DACHENG_DESKTOP_TOOLS_MANIFEST': desktopToolsManifestPath.path,\n if (desktopTools.uri != null)\n 'DACHENG_DESKTOP_TOOLS_URL': desktopTools.uri.toString(),\n if (desktopTools.token != null)\n 'DACHENG_DESKTOP_TOOLS_TOKEN': desktopTools.token!,\n 'DACHENG_APP_RUNTIME': '1',\n // This is a process-local OpenClaw capability, never a Mahayana\n // account credential. Account HTTP stays inside the Rust runtime.\n 'DACHENG_AUTH_TOKEN': token,\n if (username != null && username.isNotEmpty)\n 'DACHENG_USERNAME': username,\n 'DACHENG_IS_MEMBER': isMember ? '1' : '0',\n 'DACHENG_OPENCLAW_PROXY_TOKEN': 'dacheng-openclaw-proxy',\n });\n\n _process = await Process.start(\n nodePath,\n args,\n workingDirectory: runtimeDir.path,\n environment: env,\n mode: ProcessStartMode.normal,\n runInShell: false,\n );\n _processAuthToken = requestedAuthToken;\n _captureLogs(_process!);\n _diag('process.started', data: {'pid': _process!.pid, 'port': port});\n\n final deadline = DateTime.now().add(_startupTimeout);\n while (DateTime.now().isBefore(deadline)) {\n if (await _probe(port, token, context: 'startup')) {\n _diag('process.ready', data: {'pid': _process?.pid, 'port': port});\n _remember(\n OpenClawRuntimeStatus(\n state: OpenClawRuntimeState.running,\n message: '本机 OpenClaw Gateway 已启动',\n port: port,\n platformKey: platformKey,\n runtimePath: runtimeDir.path,\n desktopToolsStatus: desktopTools.statusJson,\n checkedAt: DateTime.now(),\n ),\n );\n return OpenClawGatewayTarget(\n baseUri: Uri.parse('http://127.0.0.1:$port'),\n token: token,\n model: model,\n modelOverride: modelOverride.trim().isEmpty\n ? null\n : modelOverride.trim(),\n desktopToolsUri: desktopTools.uri,\n desktopToolsToken: desktopTools.token,\n desktopToolsStatus: desktopTools.statusJson,\n );\n }\n\n if (_process != null) {\n final exited = await _process!.exitCode.timeout(\n const Duration(milliseconds: 1),\n onTimeout: () => -999999,\n );\n if (exited != -999999) {\n final logs = _recentLogs.take(12).join('\\n');\n _diag(\n 'process.exited-early',\n data: {\n 'exitCode': exited,\n 'recentLogs': _recentLogs.take(12).toList(),\n },\n );\n throw OpenClawRuntimeException(\n 'OpenClaw Gateway 提前退出,exitCode=$exited${logs.isEmpty ? '' : '\\n$logs'}',\n );\n }\n }\n if (_recentLogsContainRuntimeModuleFailure()) {\n final logs = _recentLogs.take(12).join('\\n');\n _diag(\n 'process.runtime-module-failure',\n data: {'recentLogs': _recentLogs.take(12).toList()},\n );\n await stop();\n throw OpenClawRuntimeException(\n 'OpenClaw runtime 模块缓存异常${logs.isEmpty ? '' : '\\n$logs'}',\n );\n }\n await Future<void>.delayed(const Duration(milliseconds: 450));\n }\n\n final logs = _recentLogs.take(12).join('\\n');\n _diag(\n 'process.startup-timeout',\n data: {\n 'timeoutSeconds': _startupTimeout.inSeconds,\n 'recentLogs': _recentLogs.take(12).toList(),\n },\n );\n throw OpenClawRuntimeException(\n 'OpenClaw Gateway 启动超时${logs.isEmpty ? '' : '\\n$logs'}',\n );\n }\n\n Future<_OpenClawBundleSpec> _loadSpec(\n String platformKey, {\n bool checkUpdates = false,\n }) async {\n final raw = await rootBundle.loadString(_manifestAsset);\n final decoded = jsonDecode(raw) as Map<String, dynamic>;\n final platforms = Map<String, dynamic>.from(\n decoded['platforms'] as Map? ?? const {},\n );\n final platform = Map<String, dynamic>.from(\n platforms[platformKey] as Map? ?? const {},\n );\n if (platform.isEmpty) {\n throw OpenClawRuntimeException('bundle_manifest.json 缺少 $platformKey 配置');\n }\n\n final gatewayArgs =\n (platform['gatewayArgs'] as List? ??\n decoded['gatewayArgs'] as List? ??\n const ['gateway', '--port', '{port}'])\n .map((item) => item.toString())\n .toList();\n\n final spec = _OpenClawBundleSpec(\n version: (decoded['version'] ?? 'dev').toString(),\n defaultPort: _readInt(decoded['defaultPort']) ?? 18789,\n defaultModel: (decoded['defaultModel'] ?? 'openclaw/default').toString(),\n defaultModelOverride: decoded['defaultModelOverride']?.toString(),\n nodeExecutable: (platform['nodeExecutable'] ?? 'node/bin/node')\n .toString(),\n cliEntrypoint: (platform['cliEntrypoint'] ?? 'openclaw/bin/openclaw.js')\n .toString(),\n gatewayArgs: gatewayArgs,\n );\n _diag(\n 'manifest.loaded',\n data: {\n 'platformKey': platformKey,\n 'version': spec.version,\n 'defaultPort': spec.defaultPort,\n 'defaultModel': spec.defaultModel,\n 'nodeExecutable': spec.nodeExecutable,\n 'cliEntrypoint': spec.cliEntrypoint,\n 'gatewayArgs': spec.gatewayArgs,\n },\n );\n final allowDownloadedRuntime = await _allowDownloadedRuntime(platformKey);\n var effectiveSpec = spec;\n if (allowDownloadedRuntime) {\n effectiveSpec =\n await _loadActiveDownloadedSpec(platformKey, fallbackSpec: spec) ??\n spec;\n } else {\n await _clearActiveDownloadedRuntimeSpec(\n platformKey: platformKey,\n reason: 'macos-sandbox',\n );\n }\n if (checkUpdates && allowDownloadedRuntime) {\n effectiveSpec =\n await _maybeInstallRuntimeUpdate(\n platformKey: platformKey,\n currentSpec: effectiveSpec,\n ) ??\n effectiveSpec;\n } else if (checkUpdates) {\n _diag(\n 'runtime-update.skip-disabled',\n data: {'platformKey': platformKey, 'reason': 'macos-sandbox'},\n );\n }\n return effectiveSpec;\n }\n\n Future<bool> _allowDownloadedRuntime(String platformKey) async {\n if (!Platform.isMacOS) return true;\n\n final support = await getApplicationSupportDirectory();\n final sandboxContainerId =\n Platform.environment['APP_SANDBOX_CONTAINER_ID']?.trim() ?? '';\n final pathLooksSandboxed = p\n .split(p.normalize(support.path))\n .contains('Containers');\n final sandboxed = sandboxContainerId.isNotEmpty || pathLooksSandboxed;\n if (!sandboxed) return true;\n\n // App Store/TestFlight sandboxed apps cannot reliably execute downloaded\n // Mach-O payloads. Use the bundled runtime that was signed with inherited\n // sandbox entitlements during packaging.\n _diag(\n 'runtime-update.disabled-macos-sandbox',\n data: {\n 'platformKey': platformKey,\n 'supportPath': support.path,\n 'hasSandboxContainerId': sandboxContainerId.isNotEmpty,\n },\n );\n return false;\n }\n\n Future<void> _clearActiveDownloadedRuntimeSpec({\n required String platformKey,\n required String reason,\n }) async {\n final saved = await AppSettings.getOpenClawActiveRuntimeSpec();\n if (saved == null) return;\n await AppSettings.clearOpenClawActiveRuntimeSpec();\n _diag(\n 'runtime-update.active-cleared',\n data: {\n 'platformKey': platformKey,\n 'reason': reason,\n 'version': saved['version']?.toString(),\n 'downloaded': saved['downloaded'] == true,\n },\n );\n }\n\n Future<_OpenClawBundleSpec?> _loadActiveDownloadedSpec(\n String platformKey, {\n required _OpenClawBundleSpec fallbackSpec,\n }) async {\n final saved = await AppSettings.getOpenClawActiveRuntimeSpec();\n if (saved == null) return null;\n final spec = _specFromJson(saved, downloaded: true);\n if (spec == null) {\n await AppSettings.clearOpenClawActiveRuntimeSpec();\n return null;\n }\n final runtimeDir = await _cachedRuntimeDirIfComplete(spec, platformKey);\n if (runtimeDir == null) {\n await AppSettings.clearOpenClawActiveRuntimeSpec();\n _diag(\n 'runtime-update.active-missing',\n data: {'platformKey': platformKey, 'version': spec.version},\n );\n return null;\n }\n if (_isRuntimeVersionNewer(fallbackSpec.version, spec.version)) {\n await AppSettings.clearOpenClawActiveRuntimeSpec();\n _diag(\n 'runtime-update.active-cleared',\n data: {\n 'platformKey': platformKey,\n 'reason': 'bundled-newer',\n 'version': spec.version,\n 'bundledVersion': fallbackSpec.version,\n },\n );\n return null;\n }\n _diag(\n 'runtime-update.active-loaded',\n data: {\n 'platformKey': platformKey,\n 'version': spec.version,\n 'runtimeDir': runtimeDir.path,\n 'bundledVersion': fallbackSpec.version,\n },\n );\n return spec;\n }\n\n bool _isRuntimeVersionNewer(String candidate, String current) {\n final candidateRank = _runtimeVersionRank(candidate);\n final currentRank = _runtimeVersionRank(current);\n final length = math.max(candidateRank.length, currentRank.length);\n for (var i = 0; i < length; i++) {\n final a = i < candidateRank.length ? candidateRank[i] : 0;\n final b = i < currentRank.length ? currentRank[i] : 0;\n if (a != b) return a > b;\n }\n return false;\n }\n\n List<int> _runtimeVersionRank(String version) {\n return RegExp(r'\\d+')\n .allMatches(version)\n .map((match) => int.tryParse(match.group(0) ?? '') ?? 0)\n .toList(growable: false);\n }\n\n _OpenClawBundleSpec? _specFromJson(\n Map<String, dynamic> json, {\n required bool downloaded,\n }) {\n final version = json['version']?.toString().trim() ?? '';\n final nodeExecutable = json['nodeExecutable']?.toString().trim() ?? '';\n final cliEntrypoint = json['cliEntrypoint']?.toString().trim() ?? '';\n if (version.isEmpty || nodeExecutable.isEmpty || cliEntrypoint.isEmpty) {\n return null;\n }\n return _OpenClawBundleSpec(\n version: version,\n defaultPort: _readInt(json['defaultPort']) ?? 18789,\n defaultModel: (json['defaultModel'] ?? _defaultGatewayModel).toString(),\n defaultModelOverride: json['defaultModelOverride']?.toString(),\n nodeExecutable: nodeExecutable,\n cliEntrypoint: cliEntrypoint,\n gatewayArgs:\n (json['gatewayArgs'] as List? ??\n const ['gateway', '--port', '{port}', '--force'])\n .map((item) => item.toString())\n .toList(),\n downloaded: downloaded,\n );\n }\n\n Future<_OpenClawBundleSpec?> _maybeInstallRuntimeUpdate({\n required String platformKey,\n required _OpenClawBundleSpec currentSpec,\n }) async {\n if (_configuredRuntimeDirOverride(platformKey) != null) {\n _diag('runtime-update.skip-override', data: {'platformKey': platformKey});\n return null;\n }\n\n try {\n final release = await _fetchRuntimeRelease(\n platformKey: platformKey,\n currentVersion: currentSpec.version,\n );\n if (release == null || release.version == currentSpec.version) {\n return null;\n }\n\n final releaseSpec = release.toBundleSpec();\n final cached = await _cachedRuntimeDirIfComplete(\n releaseSpec,\n platformKey,\n );\n if (cached != null) {\n await AppSettings.setOpenClawActiveRuntimeSpec(releaseSpec.toJson());\n _diag(\n 'runtime-update.use-cached',\n data: {\n 'platformKey': platformKey,\n 'version': release.version,\n 'runtimeDir': cached.path,\n },\n );\n return releaseSpec;\n }\n\n return await _downloadAndInstallRuntimeUpdate(\n release: release,\n platformKey: platformKey,\n );\n } catch (error, stackTrace) {\n _diag(\n 'runtime-update.failed',\n data: {\n 'platformKey': platformKey,\n 'currentVersion': currentSpec.version,\n 'error': error.toString(),\n },\n error: error,\n stackTrace: stackTrace,\n );\n return null;\n }\n }\n\n Future<_OpenClawRuntimeRelease?> _fetchRuntimeRelease({\n required String platformKey,\n required String currentVersion,\n }) async {\n final baseUrl = AppConfig.currentAiBackendUrl.replaceFirst(\n RegExp(r'/+$'),\n '',\n );\n final uri = Uri.parse('$baseUrl/api/openclaw/runtime/manifest').replace(\n queryParameters: {\n 'platform': platformKey,\n 'currentVersion': currentVersion,\n 'appVersion': AppConfig.appVersion,\n },\n );\n final response = await http\n .get(uri, headers: {'Accept': 'application/json'})\n .timeout(const Duration(seconds: 20));\n if (response.statusCode < 200 || response.statusCode >= 300) {\n _diag(\n 'runtime-update.manifest-http',\n data: {'statusCode': response.statusCode, 'uri': uri.toString()},\n );\n return null;\n }\n final decoded = _decodeJsonOrNull(response.body);\n if (decoded is! Map) return null;\n final payload = Map<String, dynamic>.from(decoded);\n if (payload['updateAvailable'] != true) {\n _diag(\n 'runtime-update.none',\n data: {\n 'platformKey': platformKey,\n 'reason': payload['reason']?.toString(),\n 'currentVersion': currentVersion,\n 'latestVersion': payload['latestVersion']?.toString(),\n },\n );\n return null;\n }\n final latest = payload['latest'];\n if (latest is! Map) return null;\n return _releaseFromJson(Map<String, dynamic>.from(latest));\n }\n\n _OpenClawRuntimeRelease? _releaseFromJson(Map<String, dynamic> json) {\n final version = json['version']?.toString().trim() ?? '';\n final archiveUrl = json['archiveUrl']?.toString().trim() ?? '';\n final sha256Hex = json['sha256']?.toString().trim().toLowerCase() ?? '';\n final archiveUri = Uri.tryParse(archiveUrl);\n final gatewayArgs =\n (json['gatewayArgs'] as List? ??\n const ['gateway', '--port', '{port}', '--force'])\n .map((item) => item.toString())\n .toList();\n if (version.isEmpty ||\n archiveUri == null ||\n !archiveUri.hasScheme ||\n !RegExp(r'^[a-f0-9]{64}$').hasMatch(sha256Hex)) {\n _diag(\n 'runtime-update.invalid-release',\n data: {\n 'version': version,\n 'archiveUrlSet': archiveUrl.isNotEmpty,\n 'sha256Length': sha256Hex.length,\n },\n );\n return null;\n }\n return _OpenClawRuntimeRelease(\n version: version,\n archiveUri: archiveUri,\n sha256Hex: sha256Hex,\n size: _readInt(json['size']) ?? 0,\n defaultPort: _readInt(json['defaultPort']) ?? 18789,\n defaultModel: (json['defaultModel'] ?? _defaultGatewayModel).toString(),\n defaultModelOverride: json['defaultModelOverride']?.toString(),\n nodeExecutable: (json['nodeExecutable'] ?? 'node/bin/node').toString(),\n cliEntrypoint: (json['cliEntrypoint'] ?? 'openclaw/openclaw.mjs')\n .toString(),\n gatewayArgs: gatewayArgs,\n );\n }\n\n Future<_OpenClawBundleSpec> _downloadAndInstallRuntimeUpdate({\n required _OpenClawRuntimeRelease release,\n required String platformKey,\n }) async {\n final spec = release.toBundleSpec();\n final runtimeDir = await _runtimeDir(spec, platformKey);\n final support = await getApplicationSupportDirectory();\n final downloadDir = Directory(p.join(support.path, 'openclaw_downloads'));\n await downloadDir.create(recursive: true);\n final archivePath = p.join(\n downloadDir.path,\n '${_safePathPart(release.version)}-$platformKey${_archiveExtension(release.archiveUri)}',\n );\n final archiveFile = File(archivePath);\n final digest = await _downloadRuntimeArchive(release, archiveFile);\n if (digest != release.sha256Hex) {\n await _deleteFileIfExists(archiveFile);\n throw OpenClawRuntimeException(\n 'OpenClaw runtime 校验失败: expected=${release.sha256Hex}, actual=$digest',\n );\n }\n\n final extractDir = Directory(\n p.join(\n downloadDir.path,\n '${_safePathPart(release.version)}-$platformKey-extract',\n ),\n );\n final stagingDir = Directory(\n p.join(\n downloadDir.path,\n '${_safePathPart(release.version)}-$platformKey-stage',\n ),\n );\n if (await extractDir.exists()) await extractDir.delete(recursive: true);\n if (await stagingDir.exists()) await stagingDir.delete(recursive: true);\n await extractDir.create(recursive: true);\n await _extractRuntimeArchive(archiveFile, extractDir);\n\n final extractedRoot = await _findExtractedRuntimeRoot(\n extractDir: extractDir,\n spec: spec,\n );\n if (extractedRoot == null) {\n throw OpenClawRuntimeException('下载的 OpenClaw runtime 不包含必要的 node/cli 文件');\n }\n\n await runtimeDir.parent.create(recursive: true);\n if (await runtimeDir.exists()) await runtimeDir.delete(recursive: true);\n await _moveOrCopyRuntimeDirectory(extractedRoot, stagingDir);\n await stagingDir.rename(runtimeDir.path);\n final marker = File(p.join(runtimeDir.path, '.bundle_ready'));\n await marker.writeAsString(\n const JsonEncoder.withIndent(' ').convert({\n 'installedAt': DateTime.now().toIso8601String(),\n 'version': release.version,\n 'platform': platformKey,\n 'sha256': digest,\n 'source': release.archiveUri.toString(),\n }),\n );\n await _repairRuntimeLaunchMetadata(\n runtimeDir: runtimeDir,\n nodePath: File(p.join(runtimeDir.path, spec.nodeExecutable)),\n platformKey: platformKey,\n cached: false,\n );\n await AppSettings.setOpenClawActiveRuntimeSpec(spec.toJson());\n await _deleteDirectoryIfExists(extractDir);\n await _deleteFileIfExists(archiveFile);\n _diag(\n 'runtime-update.installed',\n data: {\n 'platformKey': platformKey,\n 'version': release.version,\n 'runtimeDir': runtimeDir.path,\n 'size': release.size,\n },\n );\n return spec;\n }\n\n Future<String> _downloadRuntimeArchive(\n _OpenClawRuntimeRelease release,\n File destination,\n ) async {\n final client = http.Client();\n IOSink? output;\n try {\n final request = http.Request('GET', release.archiveUri)\n ..headers['Accept'] = 'application/octet-stream';\n final response = await client\n .send(request)\n .timeout(const Duration(seconds: 20));\n if (response.statusCode < 200 || response.statusCode >= 300) {\n throw OpenClawRuntimeException(\n 'OpenClaw runtime 下载失败 (${response.statusCode})',\n );\n }\n await destination.parent.create(recursive: true);\n output = destination.openWrite();\n final digestSink = AccumulatorSink<Digest>();\n final hashSink = sha256.startChunkedConversion(digestSink);\n var bytes = 0;\n await for (final chunk in response.stream.timeout(\n const Duration(minutes: 5),\n )) {\n bytes += chunk.length;\n hashSink.add(chunk);\n output.add(chunk);\n }\n await output.flush();\n await output.close();\n output = null;\n hashSink.close();\n final digest = digestSink.events.single.toString();\n _diag(\n 'runtime-update.downloaded',\n data: {\n 'archiveUri': release.archiveUri.toString(),\n 'bytes': bytes,\n 'sha256': digest,\n },\n );\n return digest;\n } finally {\n await output?.close().catchError((_) {});\n client.close();\n }\n }\n\n Future<void> _extractRuntimeArchive(\n File archiveFile,\n Directory destination,\n ) async {\n final lowerPath = archiveFile.path.toLowerCase();\n if (Platform.isMacOS && lowerPath.endsWith('.zip')) {\n try {\n final result = await Process.run('/usr/bin/ditto', [\n '-x',\n '-k',\n archiveFile.path,\n destination.path,\n ]).timeout(const Duration(minutes: 5));\n _diag(\n 'runtime-update.extract-native',\n data: {\n 'archivePath': archiveFile.path,\n 'destination': destination.path,\n 'exitCode': result.exitCode,\n if ((result.stderr as Object).toString().isNotEmpty)\n 'stderr': _previewProcessOutput(result.stderr),\n },\n );\n if (result.exitCode == 0) return;\n } catch (error, stackTrace) {\n _diag(\n 'runtime-update.extract-native-failed',\n data: {\n 'archivePath': archiveFile.path,\n 'destination': destination.path,\n 'error': error.toString(),\n },\n error: error,\n stackTrace: stackTrace,\n );\n }\n }\n\n await extractFileToDisk(archiveFile.path, destination.path);\n _diag(\n 'runtime-update.extract-dart',\n data: {'archivePath': archiveFile.path, 'destination': destination.path},\n );\n }\n\n Future<Directory?> _findExtractedRuntimeRoot({\n required Directory extractDir,\n required _OpenClawBundleSpec spec,\n }) async {\n final direct = await _runtimeRootIfComplete(extractDir, spec);\n if (direct != null) return direct;\n await for (final entity in extractDir.list(followLinks: false)) {\n if (entity is! Directory) continue;\n final nested = await _runtimeRootIfComplete(entity, spec);\n if (nested != null) return nested;\n }\n return null;\n }\n\n Future<Directory?> _runtimeRootIfComplete(\n Directory dir,\n _OpenClawBundleSpec spec,\n ) async {\n final nodePath = File(p.join(dir.path, spec.nodeExecutable));\n final cliPath = File(p.join(dir.path, spec.cliEntrypoint));\n if (await nodePath.exists() && await cliPath.exists()) return dir;\n return null;\n }\n\n Future<Directory?> _cachedRuntimeDirIfComplete(\n _OpenClawBundleSpec spec,\n String platformKey,\n ) async {\n final runtimeDir = await _runtimeDir(spec, platformKey);\n final marker = File(p.join(runtimeDir.path, '.bundle_ready'));\n final nodePath = File(p.join(runtimeDir.path, spec.nodeExecutable));\n final cliPath = File(p.join(runtimeDir.path, spec.cliEntrypoint));\n if (await marker.exists() &&\n await nodePath.exists() &&\n await cliPath.exists()) {\n return runtimeDir;\n }\n return null;\n }\n\n String _safePathPart(String value) {\n final safe = value.replaceAll(RegExp(r'[^A-Za-z0-9_.-]+'), '-');\n return safe.isEmpty ? 'runtime' : safe;\n }\n\n String _archiveExtension(Uri uri) {\n final lower = uri.path.toLowerCase();\n if (lower.endsWith('.tar.gz')) return '.tar.gz';\n if (lower.endsWith('.tgz')) return '.tgz';\n if (lower.endsWith('.tar')) return '.tar';\n return '.zip';\n }\n\n Future<void> _deleteFileIfExists(File file) async {\n try {\n if (await file.exists()) await file.delete();\n } catch (_) {}\n }\n\n Future<void> _deleteDirectoryIfExists(Directory dir) async {\n try {\n if (await dir.exists()) await dir.delete(recursive: true);\n } catch (_) {}\n }\n\n Future<Directory> _prepareBundle(\n _OpenClawBundleSpec spec,\n String platformKey,\n ) async {\n final overrideRuntimeDir = await _runtimeDirOverride(spec, platformKey);\n if (overrideRuntimeDir != null) {\n final nodePath = File(\n p.join(overrideRuntimeDir.path, spec.nodeExecutable),\n );\n await _repairRuntimeLaunchMetadata(\n runtimeDir: overrideRuntimeDir,\n nodePath: nodePath,\n platformKey: platformKey,\n cached: true,\n );\n _diag(\n 'bundle.launch.override-runtime',\n data: {\n 'platformKey': platformKey,\n 'runtimeDir': overrideRuntimeDir.path,\n },\n );\n return overrideRuntimeDir;\n }\n\n if (spec.downloaded) {\n final downloadedRuntimeDir = await _cachedRuntimeDirIfComplete(\n spec,\n platformKey,\n );\n if (downloadedRuntimeDir != null) {\n await _repairRuntimeLaunchMetadata(\n runtimeDir: downloadedRuntimeDir,\n nodePath: File(\n p.join(downloadedRuntimeDir.path, spec.nodeExecutable),\n ),\n platformKey: platformKey,\n cached: true,\n );\n _diag(\n 'bundle.launch.downloaded-runtime',\n data: {\n 'platformKey': platformKey,\n 'version': spec.version,\n 'runtimeDir': downloadedRuntimeDir.path,\n },\n );\n return downloadedRuntimeDir;\n }\n await AppSettings.clearOpenClawActiveRuntimeSpec();\n throw OpenClawRuntimeException(\n '已下载的 OpenClaw runtime 不完整,已回退到内置版本,请重新启动。',\n );\n }\n\n final bundledRuntimeDir = await _bundledRuntimeDir(spec, platformKey);\n if (bundledRuntimeDir != null) {\n _diag(\n 'bundle.launch.bundled-runtime',\n data: {\n 'platformKey': platformKey,\n 'runtimeDir': bundledRuntimeDir.path,\n },\n );\n return bundledRuntimeDir;\n }\n\n if (Platform.isMacOS) {\n throw OpenClawRuntimeException(\n '当前安装包没有可执行的 $platformKey OpenClaw runtime。请先运行 scripts/build_openclaw_desktop_bundle.sh $platformKey,再通过 flutter run 启动。',\n );\n }\n\n final runtimeDir = await _runtimeDir(spec, platformKey);\n final marker = File(p.join(runtimeDir.path, '.bundle_ready'));\n final nodePath = File(p.join(runtimeDir.path, spec.nodeExecutable));\n final cliPath = File(p.join(runtimeDir.path, spec.cliEntrypoint));\n\n if (await marker.exists() &&\n await nodePath.exists() &&\n await cliPath.exists()) {\n await _repairRuntimeLaunchMetadata(\n runtimeDir: runtimeDir,\n nodePath: nodePath,\n platformKey: platformKey,\n cached: true,\n );\n _diag(\n 'bundle.prepare.cached',\n data: {\n 'platformKey': platformKey,\n 'runtimeDir': runtimeDir.path,\n 'marker': marker.path,\n },\n );\n return runtimeDir;\n }\n\n final legacyRuntimeDir = await _legacyRuntimeDir(spec, platformKey);\n if (legacyRuntimeDir.path != runtimeDir.path &&\n await File(\n p.join(legacyRuntimeDir.path, spec.nodeExecutable),\n ).exists() &&\n await File(\n p.join(legacyRuntimeDir.path, spec.cliEntrypoint),\n ).exists()) {\n _diag(\n 'bundle.prepare.migrate-legacy',\n data: {\n 'platformKey': platformKey,\n 'from': legacyRuntimeDir.path,\n 'to': runtimeDir.path,\n },\n );\n if (await runtimeDir.exists()) {\n await runtimeDir.delete(recursive: true);\n }\n await _copyRuntimeDirectory(legacyRuntimeDir, runtimeDir);\n await _repairRuntimeLaunchMetadata(\n runtimeDir: runtimeDir,\n nodePath: nodePath,\n platformKey: platformKey,\n cached: false,\n );\n await marker.writeAsString(DateTime.now().toIso8601String());\n _diag(\n 'bundle.prepare.migrate-complete',\n data: {\n 'platformKey': platformKey,\n 'runtimeDir': runtimeDir.path,\n 'nodeExists': await nodePath.exists(),\n 'cliExists': await cliPath.exists(),\n },\n );\n return runtimeDir;\n }\n\n final prefix = 'assets/openclaw/$platformKey/';\n final assets = await _listAssets(prefix);\n final hasNodeAsset = assets.contains('$prefix${spec.nodeExecutable}');\n final hasCliAsset = assets.contains('$prefix${spec.cliEntrypoint}');\n _diag(\n 'bundle.prepare.assets',\n data: {\n 'platformKey': platformKey,\n 'prefix': prefix,\n 'assetCount': assets.length,\n 'hasNodeAsset': hasNodeAsset,\n 'hasCliAsset': hasCliAsset,\n 'nodeAsset': '$prefix${spec.nodeExecutable}',\n 'cliAsset': '$prefix${spec.cliEntrypoint}',\n 'sample': assets.take(8).toList(),\n },\n );\n if (!hasNodeAsset || !hasCliAsset) {\n throw OpenClawRuntimeException(\n '当前安装包没有内置 $platformKey 的 OpenClaw runtime。请在 release 构建中运行 scripts/build_openclaw_desktop_bundle.sh 后再打包。',\n );\n }\n\n if (await runtimeDir.exists()) {\n _diag('bundle.prepare.remove-old', data: {'runtimeDir': runtimeDir.path});\n await runtimeDir.delete(recursive: true);\n }\n await runtimeDir.create(recursive: true);\n\n for (final asset in assets) {\n final relative = asset.substring(prefix.length);\n if (relative.isEmpty || relative.endsWith('/')) continue;\n final bytes = await rootBundle.load(asset);\n final file = File(p.join(runtimeDir.path, relative));\n await file.parent.create(recursive: true);\n await file.writeAsBytes(\n bytes.buffer.asUint8List(bytes.offsetInBytes, bytes.lengthInBytes),\n flush: false,\n );\n }\n\n await _repairRuntimeLaunchMetadata(\n runtimeDir: runtimeDir,\n nodePath: nodePath,\n platformKey: platformKey,\n cached: false,\n );\n await marker.writeAsString(DateTime.now().toIso8601String());\n _diag(\n 'bundle.prepare.complete',\n data: {\n 'platformKey': platformKey,\n 'runtimeDir': runtimeDir.path,\n 'assetCount': assets.length,\n 'nodeExists': await nodePath.exists(),\n 'nodeSize': await nodePath.exists() ? await nodePath.length() : 0,\n 'cliExists': await cliPath.exists(),\n 'cliSize': await cliPath.exists() ? await cliPath.length() : 0,\n },\n );\n return runtimeDir;\n }\n\n Future<void> _copyRuntimeDirectory(\n Directory source,\n Directory destination,\n ) async {\n await destination.create(recursive: true);\n await for (final entity in source.list(\n recursive: true,\n followLinks: false,\n )) {\n final relative = p.relative(entity.path, from: source.path);\n if (relative == '.') continue;\n final target = p.join(destination.path, relative);\n if (entity is Directory) {\n await Directory(target).create(recursive: true);\n } else if (entity is File) {\n final targetFile = File(target);\n await targetFile.parent.create(recursive: true);\n await entity.openRead().pipe(targetFile.openWrite());\n }\n }\n }\n\n Future<void> _moveOrCopyRuntimeDirectory(\n Directory source,\n Directory destination,\n ) async {\n if (await destination.exists()) {\n await destination.delete(recursive: true);\n }\n await destination.parent.create(recursive: true);\n try {\n await source.rename(destination.path);\n return;\n } on FileSystemException {\n await _copyRuntimeDirectory(source, destination);\n }\n }\n\n Future<void> _repairRuntimeLaunchMetadata({\n required Directory runtimeDir,\n required File nodePath,\n required String platformKey,\n required bool cached,\n }) async {\n if (Platform.isWindows) return;\n\n final chmodResult = await Process.run('/bin/chmod', ['+x', nodePath.path]);\n ProcessResult? xattrResult;\n if (Platform.isMacOS) {\n // Files created by a quarantined app can inherit the quarantine xattr.\n // macOS then rejects Process.start with \"Operation not permitted\".\n xattrResult = await Process.run('/usr/bin/xattr', [\n '-dr',\n 'com.apple.quarantine',\n runtimeDir.path,\n ]);\n await Process.run('/usr/bin/xattr', [\n '-dr',\n 'com.apple.provenance',\n runtimeDir.path,\n ]);\n }\n\n _diag(\n 'bundle.prepare.launch-metadata',\n data: {\n 'platformKey': platformKey,\n 'runtimeDir': runtimeDir.path,\n 'nodePath': nodePath.path,\n 'cached': cached,\n 'chmodExitCode': chmodResult.exitCode,\n if ((chmodResult.stderr as Object).toString().isNotEmpty)\n 'chmodStderr': _previewProcessOutput(chmodResult.stderr),\n if (xattrResult != null) 'xattrExitCode': xattrResult.exitCode,\n if (xattrResult != null &&\n (xattrResult.stderr as Object).toString().isNotEmpty)\n 'xattrStderr': _previewProcessOutput(xattrResult.stderr),\n },\n );\n }\n\n Future<bool> _hasRequiredBundleAssets(\n String platformKey,\n _OpenClawBundleSpec spec,\n ) async {\n final prefix = 'assets/openclaw/$platformKey/';\n final assets = await _listAssets(prefix);\n final hasRequired =\n assets.contains('$prefix${spec.nodeExecutable}') &&\n assets.contains('$prefix${spec.cliEntrypoint}');\n _diag(\n 'bundle.has-required-assets',\n data: {\n 'platformKey': platformKey,\n 'assetCount': assets.length,\n 'hasRequired': hasRequired,\n },\n );\n return hasRequired;\n }\n\n Future<List<String>> _listAssets(String prefix) async {\n final assets = <String>{};\n var jsonCount = 0;\n var binCount = 0;\n var indexCount = 0;\n final failures = <String>[];\n\n try {\n final raw = await rootBundle.loadString('AssetManifest.json');\n final decoded = jsonDecode(raw) as Map<String, dynamic>;\n final items = decoded.keys\n .where((key) => key.startsWith(prefix))\n .toList();\n jsonCount = items.length;\n assets.addAll(items);\n } catch (error) {\n failures.add('AssetManifest.json: $error');\n // Newer Flutter release builds may only include AssetManifest.bin.\n }\n\n try {\n final manifest = await AssetManifest.loadFromAssetBundle(rootBundle);\n final items = manifest\n .listAssets()\n .where((key) => key.startsWith(prefix))\n .toList();\n binCount = items.length;\n assets.addAll(items);\n } catch (error) {\n failures.add('AssetManifest.bin: $error');\n // App Store archives patched after flutter build carry a JSON/index file.\n }\n\n try {\n final raw = await rootBundle.loadString(\n 'assets/openclaw/asset_index.json',\n );\n final decoded = jsonDecode(raw);\n final indexedAssets = decoded is Map\n ? decoded['assets']\n : decoded is List\n ? decoded\n : const [];\n if (indexedAssets is List) {\n final items = indexedAssets\n .map((item) => item.toString())\n .where((key) => key.startsWith(prefix))\n .toList();\n indexCount = items.length;\n assets.addAll(items);\n }\n } catch (error) {\n failures.add('asset_index.json: $error');\n // Older builds do not have an OpenClaw asset index.\n }\n\n final sortedAssets = assets.toList()..sort();\n _diag(\n 'asset.list',\n data: {\n 'prefix': prefix,\n 'total': sortedAssets.length,\n 'jsonCount': jsonCount,\n 'binCount': binCount,\n 'indexCount': indexCount,\n if (failures.isNotEmpty) 'failures': failures,\n 'sample': sortedAssets.take(8).toList(),\n },\n );\n return sortedAssets;\n }\n\n Future<bool> _probe(\n int port,\n String token, {\n String context = 'probe',\n }) async {\n try {\n final healthResponse = await http\n .get(\n Uri.parse('http://127.0.0.1:$port/health'),\n headers: {'Authorization': 'Bearer $token'},\n )\n .timeout(_probeTimeout);\n final healthPayload = _decodeJsonOrNull(healthResponse.body);\n if (healthResponse.statusCode < 200 ||\n healthResponse.statusCode >= 300 ||\n !_looksLikeHealthPayload(healthPayload)) {\n _diag(\n 'probe.health-unhealthy',\n data: {\n 'context': context,\n 'port': port,\n 'statusCode': healthResponse.statusCode,\n 'contentType': healthResponse.headers['content-type'],\n 'bodyPreview': _previewBody(healthResponse.body),\n },\n );\n return false;\n }\n\n final modelsResponse = await http\n .get(\n Uri.parse('http://127.0.0.1:$port/v1/models'),\n headers: {'Authorization': 'Bearer $token'},\n )\n .timeout(_probeTimeout);\n final modelsPayload = _decodeJsonOrNull(modelsResponse.body);\n if (modelsResponse.statusCode < 200 ||\n modelsResponse.statusCode >= 300 ||\n !_looksLikeModelsPayload(modelsPayload)) {\n _diag(\n 'probe.models-unhealthy',\n data: {\n 'context': context,\n 'port': port,\n 'statusCode': modelsResponse.statusCode,\n 'contentType': modelsResponse.headers['content-type'],\n 'bodyPreview': _previewBody(modelsResponse.body),\n },\n );\n return false;\n }\n\n // `/v1/models` can be healthy while `/v1/chat/completions` is disabled\n // by an old config. Use GET so the probe only checks route wiring and\n // never starts a real model request.\n final chatResponse = await http\n .get(\n Uri.parse('http://127.0.0.1:$port/v1/chat/completions'),\n headers: {'Authorization': 'Bearer $token'},\n )\n .timeout(_probeTimeout);\n final healthy =\n chatResponse.statusCode < 500 &&\n chatResponse.statusCode != 401 &&\n chatResponse.statusCode != 403 &&\n chatResponse.statusCode != 404;\n _diag(\n healthy ? 'probe.healthy' : 'probe.chat-unhealthy',\n data: {\n 'context': context,\n 'port': port,\n 'chatStatusCode': chatResponse.statusCode,\n },\n );\n return healthy;\n } catch (error) {\n if (context != 'startup') {\n _diag(\n 'probe.error',\n data: {'context': context, 'port': port},\n error: error,\n );\n }\n return false;\n }\n }\n\n Future<File> _ensureConfigFile({\n required Directory runtimeDir,\n required Directory stateRoot,\n required int port,\n required String token,\n required String backendDeepSeekModel,\n required String deepSeekProxyBaseUrl,\n required String remoteGatewayUrl,\n List<String> extraPluginLoadPaths = const [],\n }) async {\n final configDir = Directory(p.join(stateRoot.path, 'config'));\n await configDir.create(recursive: true);\n final workspace = Directory(p.join(stateRoot.path, 'workspace'));\n await workspace.create(recursive: true);\n final configPath = File(p.join(configDir.path, 'openclaw.json'));\n final bundledPluginLoadPaths = await _bundledPluginLoadPaths(runtimeDir);\n final pluginLoadPaths = _mergeStringLists(\n extraPluginLoadPaths,\n bundledPluginLoadPaths,\n );\n final hasWeChatPlugin = pluginLoadPaths.any(\n (path) => p.basename(path) == _weChatPluginId,\n );\n final remoteUrl = remoteGatewayUrl.trim();\n\n final config = _buildEmbeddedConfig(\n stateRoot: stateRoot,\n port: port,\n token: token,\n backendDeepSeekModel: backendDeepSeekModel,\n deepSeekProxyBaseUrl: deepSeekProxyBaseUrl,\n remoteGatewayUrl: remoteUrl,\n pluginLoadPaths: pluginLoadPaths,\n hasWeChatPlugin: hasWeChatPlugin,\n );\n\n // Repair older embedded configs in-place. OpenClaw 2026.6 requires\n // gateway.mode=local, and the home screen requires the OpenAI-compatible\n // chat endpoint. Preserve unrelated user edits while fixing the fields\n // needed for the app-owned embedded gateway to start reliably.\n try {\n final mcpConfigPath = File(\n p.join(\n Platform.environment['HOME'] ?? '',\n '.gemini',\n 'config',\n 'mcp_config.json',\n ),\n );\n if (await mcpConfigPath.exists()) {\n final mcpConfig = jsonDecode(await mcpConfigPath.readAsString());\n if (mcpConfig is Map && mcpConfig.containsKey('mcpServers')) {\n config['mcpServers'] = mcpConfig['mcpServers'];\n }\n }\n } catch (e) {\n _diag('mcp.config-error', data: {'error': e.toString()});\n }\n\n final merged = await _mergeEmbeddedConfig(configPath, config);\n await configPath.writeAsString(\n const JsonEncoder.withIndent(' ').convert(merged),\n );\n _diag(\n 'config.written',\n data: {\n 'configPath': configPath.path,\n 'port': port,\n 'gatewayMode': _mutableMap(merged['gateway'])['mode'],\n 'backendDeepSeekModel': backendDeepSeekModel,\n 'deepSeekProxyBaseUrl': deepSeekProxyBaseUrl,\n 'remoteGatewayUrlSet': remoteUrl.isNotEmpty,\n 'pluginLoadPaths': pluginLoadPaths,\n 'hasWeChatPlugin': hasWeChatPlugin,\n },\n );\n return configPath;\n }\n\n @visibleForTesting\n Map<String, dynamic> buildEmbeddedConfigForTest({\n required Directory stateRoot,\n required int port,\n required String token,\n required String backendDeepSeekModel,\n required String deepSeekProxyBaseUrl,\n String remoteGatewayUrl = '',\n List<String> pluginLoadPaths = const [],\n bool hasWeChatPlugin = false,\n }) {\n return _buildEmbeddedConfig(\n stateRoot: stateRoot,\n port: port,\n token: token,\n backendDeepSeekModel: backendDeepSeekModel,\n deepSeekProxyBaseUrl: deepSeekProxyBaseUrl,\n remoteGatewayUrl: remoteGatewayUrl,\n pluginLoadPaths: pluginLoadPaths,\n hasWeChatPlugin: hasWeChatPlugin,\n );\n }\n\n Map<String, dynamic> _buildEmbeddedConfig({\n required Directory stateRoot,\n required int port,\n required String token,\n required String backendDeepSeekModel,\n required String deepSeekProxyBaseUrl,\n required String remoteGatewayUrl,\n required List<String> pluginLoadPaths,\n required bool hasWeChatPlugin,\n }) {\n final workspace = p.join(stateRoot.path, 'workspace');\n final remoteUrl = remoteGatewayUrl.trim();\n final desktopToolAllowList = DesktopControlPolicy.supportedTools.toList()\n ..sort();\n final canvasHostConfig = <String, dynamic>{\n 'enabled': true,\n 'root': p.join(stateRoot.path, 'canvas'),\n };\n\n return <String, dynamic>{\n 'gateway': {\n 'mode': 'local',\n 'port': port,\n 'bind': 'loopback',\n 'auth': {'mode': 'token', 'token': token},\n if (remoteUrl.isNotEmpty) 'remote': {'enabled': true, 'url': remoteUrl},\n 'http': {\n 'endpoints': {\n 'chatCompletions': {'enabled': true},\n 'responses': {'enabled': true},\n },\n },\n },\n 'models': {\n 'mode': 'merge',\n 'providers': {\n _backendDeepSeekProviderId: {\n 'baseUrl': deepSeekProxyBaseUrl,\n 'api': 'openai-completions',\n 'apiKey': 'DACHENG_OPENCLAW_PROXY_TOKEN',\n 'authHeader': true,\n 'headers': {'x-dacheng-auth-token': 'DACHENG_AUTH_TOKEN'},\n 'models': [\n {\n 'id': 'deepseek-chat',\n 'name': 'DeepSeek Chat',\n 'contextWindow': 131072,\n 'maxTokens': 8192,\n 'input': ['text'],\n 'compat': {\n 'requiresStringContent': true,\n 'strictMessageKeys': true,\n },\n },\n {\n 'id': 'deepseek-reasoner',\n 'name': 'DeepSeek Reasoner',\n 'contextWindow': 131072,\n 'maxTokens': 8192,\n 'reasoning': true,\n 'input': ['text'],\n 'compat': {\n 'requiresStringContent': true,\n 'strictMessageKeys': true,\n },\n },\n ],\n },\n },\n },\n 'browser': {\n 'enabled': true,\n 'defaultProfile': 'openclaw',\n 'headless': false,\n 'ssrfPolicy': {'dangerouslyAllowPrivateNetwork': true},\n },\n 'tools': {\n 'profile': 'full',\n 'alsoAllow': desktopToolAllowList,\n 'exec': {'host': 'gateway', 'security': 'full', 'ask': 'off'},\n },\n 'plugins': {\n 'enabled': true,\n 'deny': <String>[],\n 'load': {'paths': pluginLoadPaths},\n 'slots': {'memory': 'memory-core'},\n 'entries': <String, dynamic>{\n 'memory-core': {'enabled': true},\n 'bonjour': {'enabled': false},\n 'browser': {'enabled': true},\n 'canvas': {\n 'enabled': true,\n 'config': {'host': canvasHostConfig},\n },\n 'device-pair': {'enabled': true},\n 'file-transfer': {'enabled': true},\n 'phone-control': {'enabled': true},\n 'talk-voice': {'enabled': true},\n _desktopToolsPluginId: {'enabled': true},\n if (hasWeChatPlugin) _weChatPluginId: {'enabled': true},\n },\n },\n 'channels': {\n 'defaults': {'groupPolicy': 'allowlist'},\n if (hasWeChatPlugin)\n _weChatPluginId: {\n 'enabled': true,\n 'dmPolicy': 'pairing',\n 'allowFrom': <String>[],\n 'accounts': <String, dynamic>{},\n },\n },\n 'agents': {\n 'defaults': {\n 'workspace': workspace,\n 'model': {'primary': backendDeepSeekModel},\n },\n 'list': [\n {\n 'id': 'dacheng',\n 'default': true,\n 'workspace': workspace,\n 'model': {'primary': backendDeepSeekModel},\n },\n ],\n },\n };\n }\n\n Future<File> _ensureDesktopToolsManifest({\n required Directory stateRoot,\n required _DesktopToolsLaunch desktopTools,\n }) async {\n final configDir = Directory(p.join(stateRoot.path, 'config'));\n await configDir.create(recursive: true);\n final manifestPath = File(p.join(configDir.path, 'desktop_tools.json'));\n final tools =\n DesktopControlPolicy.supportedTools\n .map(\n (name) => {\n 'name': name,\n 'endpoint': '/v1/tools/execute',\n 'readOnly': DesktopControlPolicy.isReadOnly(name),\n 'requiresConfirmation':\n DesktopControlPolicy.requiresConfirmation(name),\n },\n )\n .toList()\n ..sort(\n (a, b) => a['name'].toString().compareTo(b['name'].toString()),\n );\n\n await manifestPath.writeAsString(\n const JsonEncoder.withIndent(' ').convert({\n 'version': 1,\n 'transport': 'http-loopback',\n 'baseUrl': desktopTools.uri?.toString(),\n 'auth': {'type': 'bearer', 'env': 'DACHENG_DESKTOP_TOOLS_TOKEN'},\n 'status': desktopTools.statusJson,\n 'tools': tools,\n }),\n );\n _diag(\n 'desktop-tools.manifest-written',\n data: {\n 'manifestPath': manifestPath.path,\n 'hasBaseUrl': desktopTools.uri != null,\n },\n );\n return manifestPath;\n }\n\n Future<File> _ensureAgentModelConfig({\n required Directory stateRoot,\n required String deepSeekProxyBaseUrl,\n }) async {\n final agentDir = Directory(\n p.join(stateRoot.path, 'state', 'agents', 'dacheng', 'agent'),\n );\n await agentDir.create(recursive: true);\n final modelsPath = File(p.join(agentDir.path, 'models.json'));\n final providerConfig = <String, dynamic>{\n 'baseUrl': deepSeekProxyBaseUrl,\n 'api': 'openai-completions',\n 'apiKey': 'DACHENG_OPENCLAW_PROXY_TOKEN',\n 'authHeader': true,\n 'headers': {'x-dacheng-auth-token': 'DACHENG_AUTH_TOKEN'},\n 'models': [\n {\n 'id': 'deepseek-chat',\n 'name': 'DeepSeek Chat',\n 'contextWindow': 131072,\n 'maxTokens': 8192,\n 'input': ['text'],\n 'compat': {'requiresStringContent': true, 'strictMessageKeys': true},\n },\n {\n 'id': 'deepseek-reasoner',\n 'name': 'DeepSeek Reasoner',\n 'contextWindow': 131072,\n 'maxTokens': 8192,\n 'reasoning': true,\n 'input': ['text'],\n 'compat': {'requiresStringContent': true, 'strictMessageKeys': true},\n },\n ],\n };\n await modelsPath.writeAsString(\n const JsonEncoder.withIndent(' ').convert({\n 'providers': {_backendDeepSeekProviderId: providerConfig},\n }),\n );\n _diag(\n 'agent-models.written',\n data: {\n 'modelsPath': modelsPath.path,\n 'provider': _backendDeepSeekProviderId,\n 'deepSeekProxyBaseUrl': deepSeekProxyBaseUrl,\n },\n );\n return modelsPath;\n }\n\n Future<_OpenClawCliLaunch> _prepareCliLaunch({\n required bool ensureGateway,\n }) async {\n if (ensureGateway) {\n await ensureStarted();\n }\n final platformKey = _platformKey;\n if (platformKey == null) {\n throw const OpenClawRuntimeException('当前平台不支持内置 OpenClaw Gateway');\n }\n final spec = await _loadSpec(platformKey, checkUpdates: false);\n final runtimeDir = await _prepareBundle(spec, platformKey);\n final stateRoot = await _stateRoot();\n final desktopToolsPluginDir = await _ensureDesktopToolsPlugin(\n stateRoot: stateRoot,\n );\n final port = await AppSettings.getOpenClawGatewayPort(\n defaultValue: spec.defaultPort,\n );\n final token = await AppSettings.getOpenClawGatewayToken();\n final backendDeepSeekModel = _backendDeepSeekModelRef(\n await AppSettings.getOpenClawDeepSeekModel(\n defaultValue: _defaultDeepSeekModelFor(spec.defaultModel),\n ),\n );\n final configPath = await _ensureConfigFile(\n runtimeDir: runtimeDir,\n stateRoot: stateRoot,\n port: port,\n token: token,\n backendDeepSeekModel: backendDeepSeekModel,\n deepSeekProxyBaseUrl: _deepSeekProxyBaseUrl(),\n remoteGatewayUrl: await AppSettings.getOpenClawRemoteGatewayUrl(),\n extraPluginLoadPaths: [desktopToolsPluginDir.path],\n );\n final nodePath = p.join(runtimeDir.path, spec.nodeExecutable);\n final cliPath = p.join(runtimeDir.path, spec.cliEntrypoint);\n if (!await File(nodePath).exists()) {\n throw OpenClawRuntimeException('OpenClaw 内置 Node 不存在: $nodePath');\n }\n if (!await File(cliPath).exists()) {\n throw OpenClawRuntimeException('OpenClaw CLI 入口不存在: $cliPath');\n }\n return _OpenClawCliLaunch(\n spec: spec,\n runtimeDir: runtimeDir,\n stateRoot: stateRoot,\n configPath: configPath,\n nodePath: nodePath,\n cliPath: cliPath,\n port: port,\n token: token,\n );\n }\n\n Map<String, String> _buildOpenClawEnvironment({\n required Directory runtimeDir,\n required Directory stateRoot,\n required File configPath,\n required int port,\n required String token,\n }) {\n final nodeBinDir = Platform.isWindows\n ? p.join(runtimeDir.path, 'node')\n : p.join(runtimeDir.path, 'node', 'bin');\n final pathKey = Platform.isWindows ? 'Path' : 'PATH';\n final currentPath =\n Platform.environment[pathKey] ?? Platform.environment['PATH'] ?? '';\n final separator = Platform.isWindows ? ';' : ':';\n final pathValue = currentPath.isEmpty\n ? nodeBinDir\n : '$nodeBinDir$separator$currentPath';\n final env = Map<String, String>.from(Platform.environment)\n ..remove('NODE_COMPILE_CACHE')\n ..addAll({\n 'OPENCLAW_GATEWAY_PORT': '$port',\n 'OPENCLAW_GATEWAY_BIND': 'loopback',\n 'OPENCLAW_GATEWAY_TOKEN': token,\n 'OPENCLAW_CONFIG_PATH': configPath.path,\n 'OPENCLAW_STATE_DIR': p.join(stateRoot.path, 'state'),\n 'OPENCLAW_AGENT_DIR': p.join(stateRoot.path, 'agents'),\n 'OPENCLAW_WORKSPACE': p.join(stateRoot.path, 'workspace'),\n 'OPENCLAW_DISABLE_BONJOUR': '1',\n 'NODE_DISABLE_COMPILE_CACHE': '1',\n pathKey: pathValue,\n if (pathKey != 'PATH') 'PATH': pathValue,\n });\n return env;\n }\n\n @visibleForTesting\n Map<String, String> buildOpenClawEnvironmentForTest({\n required Directory runtimeDir,\n required Directory stateRoot,\n required File configPath,\n required int port,\n required String token,\n }) {\n return _buildOpenClawEnvironment(\n runtimeDir: runtimeDir,\n stateRoot: stateRoot,\n configPath: configPath,\n port: port,\n token: token,\n );\n }\n\n Future<List<String>> _bundledPluginLoadPaths(Directory runtimeDir) async {\n final paths = <String>[];\n final weChatPluginDir = Directory(\n p.join(runtimeDir.path, 'plugins', _weChatPluginId),\n );\n if (await File(\n p.join(weChatPluginDir.path, 'openclaw.plugin.json'),\n ).exists()) {\n paths.add(weChatPluginDir.path);\n }\n return paths;\n }\n\n Future<Directory> _ensureDesktopToolsPlugin({\n required Directory stateRoot,\n }) async {\n final dir = Directory(\n p.join(stateRoot.path, 'plugins', _desktopToolsPluginId),\n );\n await dir.create(recursive: true);\n final tools = DesktopControlPolicy.supportedTools.toList()..sort();\n await File(p.join(dir.path, 'package.json')).writeAsString(\n const JsonEncoder.withIndent(' ').convert({\n 'name': '@dacheng/openclaw-desktop-tools',\n 'version': '1.0.0',\n 'type': 'module',\n 'openclaw': {\n 'extensions': ['./index.mjs'],\n },\n }),\n );\n await File(p.join(dir.path, 'openclaw.plugin.json')).writeAsString(\n const JsonEncoder.withIndent(' ').convert({\n 'id': _desktopToolsPluginId,\n 'name': 'Dacheng Desktop Tools',\n 'description':\n 'Expose Fabushi desktop and Chrome control bridge tools to embedded OpenClaw.',\n 'activation': {'onStartup': true},\n 'contracts': {'tools': tools},\n 'configSchema': {\n 'type': 'object',\n 'additionalProperties': false,\n 'properties': <String, dynamic>{},\n },\n }),\n );\n await File(\n p.join(dir.path, 'index.mjs'),\n ).writeAsString(_desktopToolsPluginSource(tools));\n _diag(\n 'desktop-tools.plugin-written',\n data: {'pluginDir': dir.path, 'tools': tools},\n );\n return dir;\n }\n\n String _desktopToolsPluginSource(List<String> tools) {\n final toolsJson = jsonEncode(tools);\n return '''\nconst tools = $toolsJson;\n\nconst descriptions = {\n \"desktop.observe\": \"Observe the active desktop application and visible windows.\",\n \"desktop.screenshot\": \"Capture a screenshot of the local desktop.\",\n \"desktop.windows\": \"List visible desktop windows.\",\n \"desktop.click\": \"Click a local desktop coordinate after user confirmation.\",\n \"desktop.type\": \"Type text into the focused local desktop app after user confirmation.\",\n \"desktop.hotkey\": \"Send a keyboard shortcut to the local desktop after user confirmation.\",\n \"desktop.scroll\": \"Scroll the local desktop after user confirmation.\",\n \"chrome.tabs\": \"List tabs from the paired Chrome connector.\",\n \"chrome.navigate\": \"Navigate the active or selected Chrome tab after user confirmation.\",\n \"chrome.dom_snapshot\": \"Read a structured DOM snapshot from the paired Chrome connector.\",\n \"chrome.screenshot\": \"Capture a screenshot from the paired Chrome connector.\",\n \"chrome.click\": \"Click an element or point in the paired Chrome tab after user confirmation.\",\n \"chrome.type\": \"Type into an element in the paired Chrome tab after user confirmation.\"\n};\n\nconst parameterSchemas = {\n \"desktop.click\": {\n type: \"object\",\n additionalProperties: true,\n properties: {\n x: { type: \"number\" },\n y: { type: \"number\" },\n button: { type: \"string\", enum: [\"left\", \"right\"] }\n },\n required: [\"x\", \"y\"]\n },\n \"desktop.type\": {\n type: \"object\",\n additionalProperties: true,\n properties: { text: { type: \"string\" } },\n required: [\"text\"]\n },\n \"desktop.hotkey\": {\n type: \"object\",\n additionalProperties: true,\n properties: {\n key: { type: \"string\" },\n keys: { type: \"array\", items: { type: \"string\" } }\n }\n },\n \"chrome.navigate\": {\n type: \"object\",\n additionalProperties: true,\n properties: { url: { type: \"string\" }, tabId: { type: \"integer\" } },\n required: [\"url\"]\n },\n \"chrome.click\": {\n type: \"object\",\n additionalProperties: true,\n properties: {\n selector: { type: \"string\" },\n x: { type: \"number\" },\n y: { type: \"number\" },\n tabId: { type: \"integer\" }\n }\n },\n \"chrome.type\": {\n type: \"object\",\n additionalProperties: true,\n properties: {\n selector: { type: \"string\" },\n x: { type: \"number\" },\n y: { type: \"number\" },\n text: { type: \"string\" },\n tabId: { type: \"integer\" }\n },\n required: [\"text\"]\n }\n};\n\nconst defaultSchema = {\n type: \"object\",\n additionalProperties: true,\n properties: {}\n};\n\nfunction toolResultText(value) {\n try {\n return JSON.stringify(value, null, 2);\n } catch {\n return String(value);\n }\n}\n\nasync function callBridge(toolName, args) {\n if (process.env.DACHENG_DESKTOP_TOOLS_ENABLED !== \"1\") {\n return {\n isError: true,\n content: [{ type: \"text\", text: \"Fabushi desktop tools bridge is not running.\" }]\n };\n }\n const rawBaseUrl = process.env.DACHENG_DESKTOP_TOOLS_URL || \"\";\n const baseUrl = rawBaseUrl.endsWith(\"/\") ? rawBaseUrl.slice(0, -1) : rawBaseUrl;\n const token = process.env.DACHENG_DESKTOP_TOOLS_TOKEN || \"\";\n if (!baseUrl || !token) {\n return {\n isError: true,\n content: [{ type: \"text\", text: \"Fabushi desktop tools bridge URL/token is missing.\" }]\n };\n }\n const response = await fetch(`\\${baseUrl}/v1/tools/execute`, {\n method: \"POST\",\n headers: {\n \"Authorization\": `Bearer \\${token}`,\n \"Content-Type\": \"application/json\"\n },\n body: JSON.stringify({ tool: toolName, arguments: args || {} })\n });\n const payload = await response.json().catch(() => ({}));\n const ok = response.ok && payload && payload.ok !== false;\n const data = payload && Object.prototype.hasOwnProperty.call(payload, \"data\")\n ? payload.data\n : payload;\n return {\n isError: !ok,\n content: [{ type: \"text\", text: toolResultText(data) }],\n details: data\n };\n}\n\nexport default {\n id: \"dacheng-desktop-tools\",\n name: \"Dacheng Desktop Tools\",\n description: \"Expose Fabushi desktop and Chrome control tools to embedded OpenClaw.\",\n configSchema: { type: \"object\", additionalProperties: false, properties: {} },\n register(api) {\n for (const name of tools) {\n api.registerTool({\n name,\n label: name,\n description: descriptions[name] || `Run \\${name} through the Fabushi desktop bridge.`,\n parameters: parameterSchemas[name] || defaultSchema,\n async execute(_toolCallId, args) {\n return await callBridge(name, args);\n }\n });\n }\n }\n};\n''';\n }\n\n Future<Map<String, dynamic>> _mergeEmbeddedConfig(\n File configPath,\n Map<String, dynamic> defaults,\n ) async {\n Map<String, dynamic> current = <String, dynamic>{};\n if (await configPath.exists()) {\n try {\n final decoded = jsonDecode(await configPath.readAsString());\n if (decoded is Map<String, dynamic>) {\n current = decoded;\n } else if (decoded is Map) {\n current = Map<String, dynamic>.from(decoded);\n }\n } catch (_) {\n current = <String, dynamic>{};\n }\n }\n\n final gateway = _mutableMap(current['gateway']);\n final defaultGateway = _mutableMap(defaults['gateway']);\n gateway['mode'] = 'local';\n gateway['port'] = defaultGateway['port'];\n gateway['bind'] = defaultGateway['bind'];\n gateway['auth'] = Map<String, dynamic>.from(defaultGateway['auth'] as Map);\n if (defaultGateway.containsKey('remote')) {\n gateway['remote'] = {\n ..._mutableMap(gateway['remote']),\n ..._mutableMap(defaultGateway['remote']),\n };\n } else {\n final remote = _mutableMap(gateway['remote'])..remove('url');\n if (remote.isEmpty) {\n gateway.remove('remote');\n } else {\n gateway['remote'] = remote;\n }\n }\n\n final gatewayHttp = _mutableMap(gateway['http']);\n final endpoints = _mutableMap(gatewayHttp['endpoints']);\n endpoints['chatCompletions'] = {'enabled': true};\n endpoints['responses'] = {'enabled': true};\n gatewayHttp['endpoints'] = endpoints;\n gateway['http'] = gatewayHttp;\n current['gateway'] = gateway;\n\n final env = _mutableMap(current['env']);\n env.remove('DEEPSEEK_API_KEY');\n if (env.isEmpty) {\n current.remove('env');\n } else {\n current['env'] = env;\n }\n\n final defaultModels = _mutableMap(defaults['models']);\n final models = _mutableMap(current['models']);\n models['mode'] ??= defaultModels['mode'];\n final providers = _mutableMap(models['providers']);\n final defaultProviders = _mutableMap(defaultModels['providers']);\n providers.remove('deepseek');\n providers.remove('dacheng-deepseek');\n providers.remove(_backendDeepSeekProviderId);\n for (final entry in defaultProviders.entries) {\n providers[entry.key] = entry.value is Map\n ? Map<String, dynamic>.from(entry.value as Map)\n : entry.value;\n }\n models['providers'] = providers;\n current['models'] = models;\n\n final defaultBrowser = _mutableMap(defaults['browser']);\n final browser = _mutableMap(current['browser']);\n browser.addAll(defaultBrowser);\n current['browser'] = browser;\n\n // OpenClaw 2026.6 rejects root-level canvas/canvasHost. Canvas is now\n // configured through plugins.entries.canvas.config.host.\n current.remove('canvas');\n current.remove('canvasHost');\n\n final defaultTools = _mutableMap(defaults['tools']);\n final tools = _mutableMap(current['tools']);\n tools.remove('toolSearch');\n tools.remove('fs');\n final alsoAllow = _mergeStringLists(\n tools['alsoAllow'],\n defaultTools['alsoAllow'],\n );\n final execTools = _mutableMap(tools['exec']);\n execTools.remove('mode');\n if (execTools.isNotEmpty) {\n tools['exec'] = execTools;\n }\n tools.addAll(defaultTools);\n if (alsoAllow.isEmpty) {\n tools.remove('alsoAllow');\n } else {\n tools['alsoAllow'] = alsoAllow;\n }\n current['tools'] = tools;\n\n final defaultPlugins = _mutableMap(defaults['plugins']);\n final plugins = _mutableMap(current['plugins']);\n plugins['enabled'] = true;\n final defaultEntries = _mutableMap(defaultPlugins['entries']);\n final requiredPluginIds = defaultEntries.keys.toSet();\n final allowList = _stringList(plugins['allow']);\n if (allowList.isEmpty) {\n plugins.remove('allow');\n } else {\n plugins['allow'] = _mergeStringLists(allowList, requiredPluginIds);\n }\n final denyList = _stringList(\n plugins['deny'],\n ).where((item) => !requiredPluginIds.contains(item)).toList();\n if (denyList.isEmpty) {\n plugins.remove('deny');\n } else {\n plugins['deny'] = denyList;\n }\n final defaultLoad = _mutableMap(defaultPlugins['load']);\n final load = _mutableMap(plugins['load']);\n load['paths'] = _mergeStringLists(load['paths'], defaultLoad['paths']);\n plugins['load'] = load;\n final slots = _mutableMap(plugins['slots']);\n slots.addAll(_mutableMap(defaultPlugins['slots']));\n plugins['slots'] = slots;\n final entries = _mutableMap(plugins['entries']);\n for (final entry in defaultEntries.entries) {\n final currentEntry = _mutableMap(entries[entry.key]);\n currentEntry.addAll(\n entry.value is Map\n ? Map<String, dynamic>.from(entry.value as Map)\n : {'enabled': true},\n );\n entries[entry.key] = currentEntry;\n }\n plugins['entries'] = entries;\n current['plugins'] = plugins;\n\n final defaultChannels = _mutableMap(defaults['channels']);\n final channels = _mutableMap(current['channels']);\n final defaultChannelDefaults = _mutableMap(defaultChannels['defaults']);\n if (defaultChannelDefaults.isNotEmpty) {\n channels['defaults'] = {\n ..._mutableMap(channels['defaults']),\n ...defaultChannelDefaults,\n };\n }\n for (final entry in defaultChannels.entries) {\n if (entry.key == 'defaults') continue;\n final currentChannel = _mutableMap(channels[entry.key]);\n currentChannel.addAll(\n entry.value is Map\n ? Map<String, dynamic>.from(entry.value as Map)\n : <String, dynamic>{},\n );\n channels[entry.key] = currentChannel;\n }\n current['channels'] = channels;\n\n final agents = _mutableMap(current['agents']);\n final defaultAgents = _mutableMap(defaults['agents']);\n final defaultAgentDefaults = _mutableMap(defaultAgents['defaults']);\n final agentDefaults = _mutableMap(agents['defaults']);\n agentDefaults['workspace'] = defaultAgentDefaults['workspace'];\n agentDefaults['model'] = defaultAgentDefaults['model'];\n agents['defaults'] = agentDefaults;\n\n final defaultList = (defaultAgents['list'] as List? ?? const [])\n .whereType<Map>()\n .map((item) => Map<String, dynamic>.from(item))\n .toList();\n final defaultDacheng = defaultList.isNotEmpty\n ? defaultList.first\n : <String, dynamic>{\n 'id': 'dacheng',\n 'default': true,\n 'workspace': agentDefaults['workspace'],\n 'model': agentDefaults['model'],\n };\n final list = (agents['list'] as List? ?? const [])\n .map((item) => item is Map ? Map<String, dynamic>.from(item) : item)\n .toList();\n var foundDacheng = false;\n for (var i = 0; i < list.length; i++) {\n final item = list[i];\n if (item is! Map<String, dynamic>) continue;\n if (item['id'] != 'dacheng') continue;\n foundDacheng = true;\n item['default'] = true;\n item['workspace'] = defaultDacheng['workspace'];\n item['model'] = defaultDacheng['model'];\n list[i] = item;\n }\n if (!foundDacheng) {\n list.add(defaultDacheng);\n }\n agents['list'] = list.isEmpty ? defaultList : list;\n current['agents'] = agents;\n\n return current;\n }\n\n @visibleForTesting\n Future<Map<String, dynamic>> mergeEmbeddedConfigForTest(\n File configPath,\n Map<String, dynamic> defaults,\n ) {\n return _mergeEmbeddedConfig(configPath, defaults);\n }\n\n String _gatewayChatModel(String model) {\n final trimmed = model.trim();\n if (trimmed == 'openclaw' || trimmed.startsWith('openclaw/')) {\n return trimmed;\n }\n return _defaultGatewayModel;\n }\n\n String _defaultDeepSeekModelFor(String model) {\n final trimmed = model.trim();\n if (trimmed.startsWith('deepseek/')) return trimmed;\n return _defaultDeepSeekModel;\n }\n\n String _backendDeepSeekModelRef(String model) {\n final trimmed = model.trim().isEmpty ? _defaultDeepSeekModel : model.trim();\n final modelId = trimmed.contains('/')\n ? trimmed.split('/').last.trim()\n : trimmed;\n final normalizedModelId = modelId.isEmpty ? 'deepseek-chat' : modelId;\n return '$_backendDeepSeekProviderId/$normalizedModelId';\n }\n\n String _deepSeekProxyBaseUrl() {\n final baseUrl = AppConfig.currentAiBackendUrl.replaceFirst(\n RegExp(r'/+$'),\n '',\n );\n return '$baseUrl/api/openclaw/deepseek/v1';\n }\n\n Map<String, dynamic> _mutableMap(Object? value) {\n if (value is Map<String, dynamic>) return Map<String, dynamic>.from(value);\n if (value is Map) return Map<String, dynamic>.from(value);\n return <String, dynamic>{};\n }\n\n List<String> _stringList(Object? value) {\n if (value is! Iterable) return <String>[];\n return value\n .map((item) => item.toString().trim())\n .where((item) => item.isNotEmpty)\n .toList();\n }\n\n List<String> _mergeStringLists(Object? first, Object? second) {\n final merged = <String>{};\n merged.addAll(_stringList(first));\n merged.addAll(_stringList(second));\n return merged.toList();\n }\n\n void _appendBounded(StringBuffer buffer, String chunk) {\n const maxChars = 120000;\n if (chunk.isEmpty || buffer.length >= maxChars) return;\n final remaining = maxChars - buffer.length;\n buffer.write(\n chunk.length <= remaining ? chunk : chunk.substring(0, remaining),\n );\n }\n\n void _captureLogs(Process process) {\n void addLog(String line) {\n final text = line.trim();\n if (text.isEmpty) return;\n _recentLogs.insert(0, text);\n if (_recentLogs.length > 80) {\n _recentLogs.removeRange(80, _recentLogs.length);\n }\n if (kDebugMode) {\n debugPrint('[OpenClaw] $text');\n }\n _diag('process.output', data: {'line': text});\n }\n\n process.stdout\n .transform(utf8.decoder)\n .transform(const LineSplitter())\n .listen(addLog, onError: (_) {});\n process.stderr\n .transform(utf8.decoder)\n .transform(const LineSplitter())\n .listen(addLog, onError: (_) {});\n }\n\n bool _recentLogsContainRuntimeModuleFailure() {\n return _recentLogs.any((line) {\n final lower = line.toLowerCase();\n return lower.contains('err_vm_module_link_failure') ||\n lower.contains('is not in cache') ||\n lower.contains('request for') && lower.contains('.js');\n });\n }\n\n Future<Directory> _stateRoot() async {\n final support = await getApplicationSupportDirectory();\n final dir = Directory(p.join(support.path, 'openclaw_embedded'));\n await dir.create(recursive: true);\n return dir;\n }\n\n Future<_DesktopToolsLaunch> _ensureDesktopTools() async {\n try {\n final status = await DesktopControlBridge.instance.ensureStarted();\n _diag(\n 'desktop-tools.started',\n data: {\n 'bridgeRunning': status.bridgeRunning,\n 'bridgeUri': status.bridgeUri?.toString(),\n 'message': status.message,\n },\n );\n return _DesktopToolsLaunch(\n uri: status.bridgeUri,\n token: await DesktopControlBridge.instance.bridgeToken,\n statusJson: status.toJson(),\n );\n } catch (error) {\n _diag('desktop-tools.error', error: error);\n return _DesktopToolsLaunch(\n uri: null,\n token: null,\n statusJson: {\n 'enabledByBuild': false,\n 'bridgeRunning': false,\n 'message': error.toString(),\n },\n );\n }\n }\n\n Future<Directory> _runtimeDir(\n _OpenClawBundleSpec spec,\n String platformKey,\n ) async {\n final support = await getApplicationSupportDirectory();\n final runtimeBase = Platform.isMacOS\n ? Directory(\n p.join(\n support.parent.path,\n '${p.basename(support.path)}.openclaw_runtime',\n ),\n )\n : Directory(p.join(support.path, 'openclaw_embedded'));\n return Directory(\n p.join(runtimeBase.path, 'runtime', spec.version, platformKey),\n );\n }\n\n Future<Directory?> _bundledRuntimeDir(\n _OpenClawBundleSpec spec,\n String platformKey,\n ) async {\n if (!Platform.isMacOS) return null;\n\n final contentsDir = Directory(\n p.dirname(p.dirname(Platform.resolvedExecutable)),\n );\n final candidates = <Directory>[\n Directory(\n p.join(\n contentsDir.path,\n 'Frameworks',\n 'App.framework',\n 'Resources',\n 'flutter_assets',\n 'assets',\n 'openclaw',\n platformKey,\n ),\n ),\n Directory(\n p.join(\n contentsDir.path,\n 'Frameworks',\n 'App.framework',\n 'Versions',\n 'A',\n 'Resources',\n 'flutter_assets',\n 'assets',\n 'openclaw',\n platformKey,\n ),\n ),\n Directory(\n p.join(\n contentsDir.path,\n 'Resources',\n 'flutter_assets',\n 'assets',\n 'openclaw',\n platformKey,\n ),\n ),\n ];\n\n for (final dir in candidates) {\n final nodePath = File(p.join(dir.path, spec.nodeExecutable));\n final cliPath = File(p.join(dir.path, spec.cliEntrypoint));\n if (await nodePath.exists() && await cliPath.exists()) {\n return dir;\n }\n }\n _diag(\n 'bundle.macos-bundled-runtime-missing',\n data: {\n 'platformKey': platformKey,\n 'contentsDir': contentsDir.path,\n 'candidates': candidates.map((item) => item.path).toList(),\n },\n );\n return null;\n }\n\n Future<Directory?> _runtimeDirOverride(\n _OpenClawBundleSpec spec,\n String platformKey,\n ) async {\n final raw = _configuredRuntimeDirOverride(platformKey);\n if (raw == null) return null;\n\n final dir = Directory(raw);\n final nodePath = File(p.join(dir.path, spec.nodeExecutable));\n final cliPath = File(p.join(dir.path, spec.cliEntrypoint));\n final nodeExists = await nodePath.exists();\n final cliExists = await cliPath.exists();\n _diag(\n 'bundle.override.paths',\n data: {\n 'platformKey': platformKey,\n 'runtimeDir': dir.path,\n 'nodePath': nodePath.path,\n 'nodeExists': nodeExists,\n 'cliPath': cliPath.path,\n 'cliExists': cliExists,\n },\n );\n\n if (!nodeExists || !cliExists) {\n throw OpenClawRuntimeException(\n 'DACHENG_OPENCLAW_RUNTIME_DIR 指向的 OpenClaw runtime 不完整: ${dir.path}',\n );\n }\n return dir;\n }\n\n String? _configuredRuntimeDirOverride(String platformKey) {\n final raw = _runtimeDirOverrideDefine.trim().isNotEmpty\n ? _runtimeDirOverrideDefine.trim()\n : Platform.environment['DACHENG_OPENCLAW_RUNTIME_DIR']?.trim();\n if (raw == null || raw.isEmpty) return null;\n return p.normalize(raw.replaceAll('{platform}', platformKey));\n }\n\n Future<Directory> _legacyRuntimeDir(\n _OpenClawBundleSpec spec,\n String platformKey,\n ) async {\n final support = await getApplicationSupportDirectory();\n return Directory(\n p.join(\n support.path,\n 'openclaw_embedded',\n 'runtime',\n spec.version,\n platformKey,\n ),\n );\n }\n\n OpenClawRuntimeStatus _remember(OpenClawRuntimeStatus status) {\n _lastStatus = status;\n return status;\n }\n\n String? get _platformKey {\n final abi = Abi.current().toString().toLowerCase();\n if (Platform.isMacOS) {\n return abi.contains('arm64') ? 'macos-arm64' : 'macos-x64';\n }\n if (Platform.isWindows) {\n return abi.contains('arm64') ? 'windows-arm64' : 'windows-x64';\n }\n if (Platform.isLinux) {\n return abi.contains('arm64') ? 'linux-arm64' : 'linux-x64';\n }\n return null;\n }\n\n OpenClawRuntimeStatus? get lastStatus => _lastStatus;\n\n void _diag(\n String message, {\n Map<String, Object?> data = const {},\n Object? error,\n StackTrace? stackTrace,\n }) {\n unawaited(\n DiagnosticLogService.instance.log(\n 'openclaw.runtime',\n message,\n data: data,\n error: error,\n stackTrace: stackTrace,\n ),\n );\n }\n}\n\nint? _readInt(Object? value) {\n if (value is int) return value;\n if (value is num) return value.toInt();\n if (value is String) return int.tryParse(value);\n return null;\n}\n\nObject? _decodeJsonOrNull(String body) {\n try {\n return jsonDecode(body);\n } catch (_) {\n return null;\n }\n}\n\nbool _looksLikeHealthPayload(Object? payload) {\n if (payload is! Map) return false;\n if (payload['ok'] == true) return true;\n final status = payload['status']?.toString().toLowerCase();\n return status == 'live' || status == 'ok' || status == 'healthy';\n}\n\nbool _looksLikeModelsPayload(Object? payload) {\n if (payload is List) return true;\n if (payload is! Map) return false;\n return payload['data'] is List ||\n payload['models'] is List ||\n payload['object'] == 'list';\n}\n\nString _previewBody(String body) {\n final trimmed = body.trim();\n return trimmed.length <= 80 ? trimmed : trimmed.substring(0, 80);\n}\n\nString _previewProcessOutput(Object? output) {\n final text = output?.toString().trim() ?? '';\n return text.length <= 200 ? text : text.substring(0, 200);\n}\n"}}, "ai-backend/src/server.js": {"3323b5c4de0a31d36fe4d8934069ae04a3a16fc0": {"t": 1785746707.5221958, "n": 144923, "h": "55cea71410263f42", "c": "import crypto from 'node:crypto';\nimport fs from 'node:fs';\nimport path from 'node:path';\nimport process from 'node:process';\nimport { fileURLToPath } from 'node:url';\n\nimport { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';\nimport { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';\nimport Database from 'better-sqlite3';\nimport cors from 'cors';\nimport dotenv from 'dotenv';\nimport express from 'express';\nimport rateLimit from 'express-rate-limit';\nimport helmet from 'helmet';\nimport pino from 'pino';\nimport { z } from 'zod';\n\nimport { registerPlatformApi } from './platform_api.js';\nimport {\n codexResponsesMessages,\n codexResponsesTools,\n deepSeekResultToResponseItems,\n} from './codex_deepseek_adapter.js';\nimport {\n createPluginCodexPolicy,\n pluginConversationNamespace,\n pluginMcpTokenEnv,\n} from './plugin_codex_policy.js';\nimport {\n handleOfficialMcpRequest,\n officialMcpApps,\n} from './official_mcp_apps.js';\n\ndotenv.config();\n\nconst __dirname = path.dirname(fileURLToPath(import.meta.url));\nconst rootDir = path.resolve(__dirname, '..');\nconst dataDir = process.env.DATA_DIR || path.join(rootDir, 'data');\nconst resourcesDir = path.join(dataDir, 'resources');\nconst openClawRuntimeUpdatesDir =\n process.env.OPENCLAW_RUNTIME_UPDATES_DIR || path.join(dataDir, 'openclaw-runtime');\nconst openClawRuntimeEnableMacosUpdates =\n process.env.OPENCLAW_RUNTIME_ENABLE_MACOS_UPDATES === 'true';\nconst codexHomeDir = process.env.CODEX_HOME || path.join(dataDir, 'codex-home');\nconst codexPluginSessionsDir = path.join(dataDir, 'codex-plugin-sessions');\nconst codexTempDir = process.env.CODEX_TMPDIR || path.join(dataDir, 'codex-tmp');\nconst codexRuntimeDir = process.env.XDG_RUNTIME_DIR || path.join(dataDir, 'codex-runtime');\nconst dbPath = process.env.SQLITE_PATH || path.join(dataDir, 'dacheng-ai.sqlite');\n\nfs.mkdirSync(resourcesDir, { recursive: true });\nfs.mkdirSync(openClawRuntimeUpdatesDir, { recursive: true });\nfs.mkdirSync(codexHomeDir, { recursive: true });\nfs.mkdirSync(codexPluginSessionsDir, { recursive: true });\nfs.mkdirSync(codexTempDir, { recursive: true });\nfs.mkdirSync(codexRuntimeDir, { recursive: true });\n\nconst logger = pino({\n level: process.env.LOG_LEVEL || 'info',\n});\n\nconst app = express();\napp.disable('x-powered-by');\napp.set('trust proxy', Number(process.env.TRUST_PROXY_HOPS || 1));\napp.use(helmet());\nconst corsOrigin =\n !process.env.CORS_ORIGIN || process.env.CORS_ORIGIN.trim() === '*'\n ? true\n : process.env.CORS_ORIGIN.split(',').map((item) => item.trim()).filter(Boolean);\napp.use(cors({\n origin: corsOrigin,\n credentials: true,\n exposedHeaders: ['mcp-session-id', 'mcp-protocol-version'],\n}));\napp.use(express.json({ limit: '1mb' }));\napp.use(\n rateLimit({\n windowMs: 60_000,\n limit: Number(process.env.RATE_LIMIT_PER_MINUTE || 90),\n standardHeaders: true,\n legacyHeaders: false,\n }),\n);\n\nconst db = new Database(dbPath);\ndb.pragma('journal_mode = WAL');\ndb.pragma('foreign_keys = ON');\n\ndb.exec(`\n CREATE TABLE IF NOT EXISTS conversations (\n id TEXT PRIMARY KEY,\n user_id TEXT NOT NULL,\n title TEXT NOT NULL,\n provider TEXT NOT NULL,\n model TEXT NOT NULL,\n created_at TEXT NOT NULL,\n updated_at TEXT NOT NULL\n );\n\n CREATE INDEX IF NOT EXISTS idx_conversations_user_updated\n ON conversations (user_id, updated_at DESC);\n\n CREATE TABLE IF NOT EXISTS messages (\n id TEXT PRIMARY KEY,\n conversation_id TEXT NOT NULL,\n role TEXT NOT NULL,\n content TEXT NOT NULL,\n prompt_tokens INTEGER NOT NULL DEFAULT 0,\n completion_tokens INTEGER NOT NULL DEFAULT 0,\n total_tokens INTEGER NOT NULL DEFAULT 0,\n created_at TEXT NOT NULL,\n FOREIGN KEY (conversation_id) REFERENCES conversations(id) ON DELETE CASCADE\n );\n\n CREATE INDEX IF NOT EXISTS idx_messages_conversation_created\n ON messages (conversation_id, created_at ASC);\n\n CREATE TABLE IF NOT EXISTS usage_months (\n user_id TEXT NOT NULL,\n month TEXT NOT NULL,\n total_tokens INTEGER NOT NULL DEFAULT 0,\n updated_at TEXT NOT NULL,\n PRIMARY KEY (user_id, month)\n );\n\n CREATE TABLE IF NOT EXISTS resource_downloads (\n id TEXT PRIMARY KEY,\n user_id TEXT NOT NULL,\n title TEXT NOT NULL,\n source_name TEXT NOT NULL,\n source_url TEXT NOT NULL,\n file_name TEXT NOT NULL,\n file_path TEXT NOT NULL,\n created_at TEXT NOT NULL\n );\n\n CREATE TABLE IF NOT EXISTS agent_runs (\n id TEXT PRIMARY KEY,\n user_id TEXT NOT NULL,\n conversation_id TEXT NOT NULL,\n message_id TEXT NOT NULL,\n openclaw_run_id TEXT,\n openclaw_session_key TEXT NOT NULL,\n status TEXT NOT NULL,\n mode TEXT NOT NULL,\n provider TEXT NOT NULL,\n model TEXT,\n input_tokens INTEGER NOT NULL DEFAULT 0,\n output_tokens INTEGER NOT NULL DEFAULT 0,\n tool_call_count INTEGER NOT NULL DEFAULT 0,\n cost_microusd INTEGER NOT NULL DEFAULT 0,\n started_at TEXT NOT NULL,\n completed_at TEXT,\n failed_at TEXT,\n error_code TEXT,\n error_message TEXT\n );\n\n CREATE INDEX IF NOT EXISTS idx_agent_runs_user_started\n ON agent_runs (user_id, started_at DESC);\n\n CREATE INDEX IF NOT EXISTS idx_agent_runs_status_started\n ON agent_runs (status, started_at DESC);\n\n CREATE TABLE IF NOT EXISTS agent_message_feedback (\n id TEXT PRIMARY KEY,\n message_id TEXT NOT NULL,\n user_id TEXT NOT NULL,\n rating TEXT NOT NULL,\n reason TEXT,\n comment TEXT,\n created_at TEXT NOT NULL\n );\n`);\n\nconst conversationColumns = new Set(\n db.prepare('PRAGMA table_info(conversations)').all().map((column) => column.name),\n);\nif (!conversationColumns.has('namespace')) {\n db.exec('ALTER TABLE conversations ADD COLUMN namespace TEXT');\n}\nif (!conversationColumns.has('thread_id')) {\n db.exec('ALTER TABLE conversations ADD COLUMN thread_id TEXT');\n}\ndb.exec(`\n CREATE INDEX IF NOT EXISTS idx_conversations_user_namespace_updated\n ON conversations (user_id, namespace, updated_at DESC)\n`);\n\nconst statements = {\n insertConversation: db.prepare(`\n INSERT INTO conversations (id, user_id, title, provider, model, created_at, updated_at)\n VALUES (@id, @userId, @title, @provider, @model, @createdAt, @updatedAt)\n `),\n updateConversation: db.prepare(`\n UPDATE conversations\n SET title = @title, updated_at = @updatedAt\n WHERE id = @id AND user_id = @userId\n `),\n getConversation: db.prepare(`\n SELECT * FROM conversations WHERE id = ? AND user_id = ? LIMIT 1\n `),\n listConversations: db.prepare(`\n SELECT id, title, updated_at AS updatedAt\n FROM conversations\n WHERE user_id = ? AND (namespace IS NULL OR namespace = '')\n ORDER BY updated_at DESC\n LIMIT 30\n `),\n insertPluginConversation: db.prepare(`\n INSERT INTO conversations (\n id, user_id, title, provider, model, namespace, thread_id, created_at, updated_at\n )\n VALUES (\n @id, @userId, @title, @provider, @model, @namespace, NULL, @createdAt, @updatedAt\n )\n `),\n getPluginConversation: db.prepare(`\n SELECT * FROM conversations\n WHERE id = ? AND user_id = ? AND namespace = ?\n LIMIT 1\n `),\n updatePluginConversation: db.prepare(`\n UPDATE conversations\n SET title = @title, thread_id = @threadId, updated_at = @updatedAt\n WHERE id = @id AND user_id = @userId AND namespace = @namespace\n `),\n insertMessage: db.prepare(`\n INSERT INTO messages (\n id, conversation_id, role, content,\n prompt_tokens, completion_tokens, total_tokens, created_at\n )\n VALUES (\n @id, @conversationId, @role, @content,\n @promptTokens, @completionTokens, @totalTokens, @createdAt\n )\n `),\n listMessages: db.prepare(`\n SELECT role, content, prompt_tokens AS promptTokens,\n completion_tokens AS completionTokens, total_tokens AS totalTokens,\n created_at AS createdAt\n FROM messages\n WHERE conversation_id = ?\n ORDER BY created_at ASC\n `),\n getUsage: db.prepare(`\n SELECT total_tokens AS totalTokens FROM usage_months WHERE user_id = ? AND month = ?\n `),\n upsertUsage: db.prepare(`\n INSERT INTO usage_months (user_id, month, total_tokens, updated_at)\n VALUES (@userId, @month, @totalTokens, @updatedAt)\n ON CONFLICT(user_id, month)\n DO UPDATE SET total_tokens = usage_months.total_tokens + excluded.total_tokens,\n updated_at = excluded.updated_at\n `),\n insertResourceDownload: db.prepare(`\n INSERT INTO resource_downloads (\n id, user_id, title, source_name, source_url, file_name, file_path, created_at\n )\n VALUES (\n @id, @userId, @title, @sourceName, @sourceUrl, @fileName, @filePath, @createdAt\n )\n `),\n insertAgentRun: db.prepare(`\n INSERT INTO agent_runs (\n id, user_id, conversation_id, message_id, openclaw_run_id, openclaw_session_key,\n status, mode, provider, model, started_at\n )\n VALUES (\n @id, @userId, @conversationId, @messageId, @openClawRunId, @openClawSessionKey,\n @status, @mode, @provider, @model, @startedAt\n )\n `),\n getAgentRun: db.prepare(`\n SELECT * FROM agent_runs WHERE id = ? AND user_id = ? LIMIT 1\n `),\n updateAgentRunStatus: db.prepare(`\n UPDATE agent_runs\n SET status = @status,\n openclaw_run_id = COALESCE(@openClawRunId, openclaw_run_id),\n input_tokens = COALESCE(@inputTokens, input_tokens),\n output_tokens = COALESCE(@outputTokens, output_tokens),\n tool_call_count = COALESCE(@toolCallCount, tool_call_count),\n completed_at = @completedAt,\n failed_at = @failedAt,\n error_code = @errorCode,\n error_message = @errorMessage\n WHERE id = @id AND user_id = @userId\n `),\n insertAgentFeedback: db.prepare(`\n INSERT INTO agent_message_feedback (id, message_id, user_id, rating, reason, comment, created_at)\n VALUES (@id, @messageId, @userId, @rating, @reason, @comment, @createdAt)\n `),\n};\n\nconst deepseekApiKey = process.env.DEEPSEEK_API_KEY || '';\nconst deepseekBaseUrl = (process.env.DEEPSEEK_BASE_URL || 'https://api.deepseek.com').replace(/\\/+$/, '');\nconst deepseekModel = process.env.DEEPSEEK_MODEL || 'deepseek-chat';\nconst deepseekAvailableModels = Array.from(\n new Set(\n (process.env.DEEPSEEK_MODELS || `${deepseekModel},deepseek-chat,deepseek-reasoner`)\n .split(',')\n .map((item) => normalizeDeepSeekModelName(item))\n .filter(Boolean),\n ),\n);\nconst cbetaApiRoot = (process.env.CBETA_API_ROOT || 'https://api.ombhrum.com/api/cbeta').replace(/\\/+$/, '');\nconst fabushiApiBaseUrl = (process.env.FABUSHI_API_BASE_URL || 'https://api.ombhrum.com').replace(/\\/+$/, '');\nconst memberMonthlyLimit = Number(process.env.MEMBER_MONTHLY_TOKEN_LIMIT || 1_000_000);\nconst freeMonthlyLimit = Number(process.env.FREE_MONTHLY_TOKEN_LIMIT || 1_000);\nconst requireMemberForAi = process.env.REQUIRE_MEMBER_FOR_AI === 'true';\nconst deepseekMaxCompletionTokens = Math.max(1, Number(process.env.DEEPSEEK_MAX_COMPLETION_TOKENS || 1400));\nconst botFatherMaxCompletionTokens = Math.max(\n deepseekMaxCompletionTokens,\n Math.min(12_000, Number(process.env.BOT_FATHER_MAX_COMPLETION_TOKENS || 6000)),\n);\nconst maxResourceTextChars = Number(process.env.MAX_RESOURCE_TEXT_CHARS || 80_000);\nconst openClawRuntimeManifestPath =\n process.env.OPENCLAW_RUNTIME_MANIFEST_PATH || path.join(openClawRuntimeUpdatesDir, 'manifest.json');\nconst openClawRuntimePublicBaseUrl = (process.env.OPENCLAW_RUNTIME_PUBLIC_BASE_URL || '').replace(/\\/+$/, '');\nconst enableLibreChatAgentChat = process.env.ENABLE_LIBRECHAT_AGENT_CHAT === 'true';\nconst libreChatAgentsBaseUrl = (\n process.env.LIBRECHAT_AGENTS_BASE_URL || 'http://127.0.0.1:3080/api/agents/v1'\n).replace(/\\/+$/, '');\nconst libreChatAgentApiKey = process.env.LIBRECHAT_AGENT_API_KEY || '';\nconst libreChatAgentId = process.env.LIBRECHAT_AGENT_ID || '';\nconst enableCodexSdkChat = process.env.ENABLE_CODEX_SDK_CHAT === 'true';\nconst enableOpenClawAgentChat = process.env.ENABLE_OPENCLAW_AGENT_CHAT === 'true';\nconst openClawGatewayUrl = (process.env.OPENCLAW_GATEWAY_URL || 'http://127.0.0.1:18789').replace(/\\/+$/, '');\nconst openClawGatewayToken = process.env.OPENCLAW_GATEWAY_TOKEN || '';\nconst openClawAgentId = process.env.OPENCLAW_AGENT_ID || 'fabushi-public-agent';\nconst openClawRunsEndpoint = process.env.OPENCLAW_RUNS_ENDPOINT || '';\nconst codexDeepSeekProviderId = 'dacheng-deepseek-proxy';\nconst codexResponsesBaseUrl = (\n process.env.CODEX_DEEPSEEK_RESPONSES_BASE_URL ||\n `http://127.0.0.1:${process.env.PORT || 8788}/codex-deepseek/v1`\n).replace(/\\/+$/, '');\nconst codexAdapterTokenPrefix = 'dacheng-codex-proxy.';\nconst codexAdapterTokenTtlMs = Math.max(\n 60_000,\n Number(process.env.CODEX_DEEPSEEK_ADAPTER_TOKEN_TTL_MS || 15 * 60_000),\n);\nconst codexAdapterSecret =\n process.env.CODEX_DEEPSEEK_ADAPTER_SECRET ||\n deepseekApiKey ||\n openClawGatewayToken ||\n 'dacheng-codex-local-dev-secret';\n\nconst systemPrompt = [\n '你是“大乘”App 的 AI 助手。',\n '你的核心任务是帮助用户查找、整理、理解并全球法布施合法可分享的佛法资源。',\n '回答要庄重、简洁、可执行;涉及经典、仪轨或资源时,提醒用户尊重版权、来源和当地法规。',\n '如果用户想找资源、下载经文、寻找音频或准备可分享资料,你必须先在后端自动执行搜索、验证和下载步骤;不要要求用户去前端 + 菜单手动查找。',\n '你可以把执行进度总结给用户,但不要输出隐藏推理原文。',\n].join('\\n');\n\nfunction nowIso() {\n return new Date().toISOString();\n}\n\nfunction monthKey(date = new Date()) {\n return `${date.getUTCFullYear()}-${String(date.getUTCMonth() + 1).padStart(2, '0')}`;\n}\n\nfunction jsonResponse(res, status, payload) {\n res.status(status).json(payload);\n}\n\nasync function readJsonFile(filePath) {\n const raw = await fs.promises.readFile(filePath, 'utf8');\n return JSON.parse(raw);\n}\n\nfunction requestOrigin(req) {\n if (openClawRuntimePublicBaseUrl) return openClawRuntimePublicBaseUrl;\n const proto = req.get('X-Forwarded-Proto') || req.protocol || 'https';\n const host = req.get('X-Forwarded-Host') || req.get('Host');\n return host ? `${proto}://${host}` : '';\n}\n\nfunction absolutizeOpenClawRuntimeUrl(req, value) {\n const text = readText(value);\n if (!text) return '';\n if (/^https?:\\/\\//i.test(text)) return text;\n const normalized = text.startsWith('/')\n ? text\n : `/api/openclaw/runtime/files/${text.replace(/^\\/+/, '')}`;\n const origin = requestOrigin(req);\n return origin ? new URL(normalized, `${origin}/`).toString() : normalized;\n}\n\nfunction textToolResult(text, structuredContent = {}) {\n return {\n structuredContent,\n content: [{ type: 'text', text }],\n };\n}\n\nfunction sseHeaders(res) {\n res.status(200);\n res.setHeader('Content-Type', 'text/event-stream; charset=utf-8');\n res.setHeader('Cache-Control', 'no-cache, no-transform');\n res.setHeader('Connection', 'keep-alive');\n res.setHeader('X-Accel-Buffering', 'no');\n if (typeof res.flushHeaders === 'function') res.flushHeaders();\n}\n\nfunction writeSse(res, event, payload) {\n res.write(`event: ${event}\\n`);\n res.write(`data: ${JSON.stringify(payload)}\\n\\n`);\n}\n\nfunction asyncHandler(fn) {\n return (req, res, next) => {\n Promise.resolve(fn(req, res, next)).catch(next);\n };\n}\n\nfunction bearerToken(req) {\n const delegated = readText(req.get('x-dacheng-auth-token'));\n if (\n delegated &&\n delegated !== 'DACHENG_AUTH_TOKEN' &&\n !delegated.startsWith('secretref-env:') &&\n delegated !== 'dacheng-openclaw-proxy'\n ) {\n return delegated;\n }\n const header = req.get('Authorization') || '';\n if (!header.startsWith('Bearer ')) return '';\n const token = header.slice('Bearer '.length).trim();\n return token === 'dacheng-openclaw-proxy' ? '' : token;\n}\n\nfunction sha256(value) {\n return crypto.createHash('sha256').update(value).digest('hex');\n}\n\nfunction safeUserText(value, fallback = '') {\n if (typeof value !== 'string') return fallback;\n return value.replace(/\\u0000/g, '').trim();\n}\n\nfunction estimateTokens(text) {\n const normalized = String(text || '').trim();\n if (!normalized) return 0;\n return Math.max(1, Math.ceil(normalized.length / 3));\n}\n\nfunction readText(value) {\n if (value === undefined || value === null) return '';\n return String(value).replace(/\\u0000/g, '').trim();\n}\n\nfunction clientContextMessage(client) {\n if (!client || typeof client !== 'object') return '';\n const selectedModel = readText(client.selectedModel || client.model);\n const project = client.project && typeof client.project === 'object' ? client.project : {};\n const projectName = readText(project.name);\n const projectPath = readText(project.path);\n const lines = ['桌面端上下文:'];\n if (selectedModel) lines.push(`用户选择的 DeepSeek 模型:${normalizeDeepSeekModelName(selectedModel)}`);\n if (projectPath) {\n lines.push(`当前项目名称:${projectName || projectPath}`);\n lines.push(`当前项目路径:${projectPath}`);\n lines.push('如果本轮任务涉及读取、修改、运行、分析或整理项目,请优先围绕当前项目路径工作;不要擅自操作其他目录。');\n }\n return lines.length > 1 ? lines.join('\\n') : '';\n}\nfunction unwrapPayload(payload) {\n if (!payload || typeof payload !== 'object') return {};\n return payload.data && typeof payload.data === 'object' ? payload.data : payload;\n}\n\nfunction readFirstText(values) {\n for (const value of values) {\n const text = readText(value);\n if (text) return text;\n }\n return '';\n}\n\nfunction activeFlag(value) {\n if (value === true) return true;\n if (value === 1) return true;\n if (typeof value !== 'string') return false;\n return ['true', '1', 'yes', 'active'].includes(value.trim().toLowerCase());\n}\n\nfunction futureDate(value) {\n const text = readText(value);\n if (!text) return false;\n const timestamp = Date.parse(text);\n return Number.isFinite(timestamp) && timestamp > Date.now();\n}\n\nfunction normalizeMembershipPayload(payload) {\n if (!payload || typeof payload !== 'object') return false;\n const data = unwrapPayload(payload);\n const membership = data.membership && typeof data.membership === 'object' ? data.membership : data;\n const type = readText(membership.type || membership.membershipType || data.membershipType).toLowerCase();\n const status = readText(membership.status || data.status).toLowerCase();\n const expiry = membership.expiresAt ||\n membership.expires_at ||\n membership.membershipExpiry ||\n membership.membership_expires_at ||\n data.membershipExpiry ||\n data.membership_expires_at;\n const hasExplicitActiveFlag =\n membership.isActive !== undefined ||\n membership.active !== undefined ||\n membership.hasPremiumAccess !== undefined ||\n membership.hasPremiumMembership !== undefined ||\n membership.isPremium !== undefined;\n\n if (hasExplicitActiveFlag) {\n return Boolean(\n activeFlag(membership.isActive) ||\n activeFlag(membership.active) ||\n activeFlag(membership.hasPremiumAccess) ||\n activeFlag(membership.hasPremiumMembership) ||\n activeFlag(membership.isPremium),\n );\n }\n\n return Boolean(\n status === 'active' ||\n type === 'paid' ||\n type === 'premium' ||\n type === 'trial' ||\n futureDate(expiry),\n );\n}\n\nfunction extractRemoteAccount(payload) {\n const data = unwrapPayload(payload);\n const nestedUser = data.user && typeof data.user === 'object' ? data.user : {};\n const id = readFirstText([\n data.userId,\n data.user_id,\n data.userNo,\n data.user_no,\n data.id,\n nestedUser.userId,\n nestedUser.user_id,\n nestedUser.userNo,\n nestedUser.user_no,\n nestedUser.id,\n ]);\n const username = readFirstText([data.username, data.name, nestedUser.username, nestedUser.name]);\n const email = readFirstText([data.email, nestedUser.email]);\n const membership = data.membership && typeof data.membership === 'object' ? data.membership : null;\n return {\n authenticated: Boolean(id || username || email || membership || data.success === true),\n id,\n username,\n email,\n isMember: normalizeMembershipPayload(payload),\n membership,\n };\n}\n\nasync function resolveRemoteAccount(token) {\n const fallback = {\n authenticated: false,\n id: '',\n username: '',\n email: '',\n isMember: false,\n membership: null,\n };\n if (!token || !fabushiApiBaseUrl) return fallback;\n\n const endpoints = [\n '/api/stripe/membership-status',\n '/api/alipay/check-membership',\n '/api/auth/user-info',\n ];\n\n for (const endpoint of endpoints) {\n try {\n const response = await fetch(`${fabushiApiBaseUrl}${endpoint}`, {\n headers: {\n Accept: 'application/json',\n Authorization: `Bearer ${token}`,\n },\n signal: AbortSignal.timeout(8_000),\n });\n if (response.status === 401 || response.status === 403) return fallback;\n if (!response.ok) continue;\n const payload = await response.json();\n const account = extractRemoteAccount(payload);\n if (account.authenticated || account.isMember) {\n return {\n ...fallback,\n ...account,\n authenticated: true,\n };\n }\n } catch (error) {\n logger.debug({ endpoint, error: String(error) }, 'membership lookup skipped');\n }\n }\n\n return fallback;\n}\n\nasync function resolveUser(req, body = {}) {\n const token = bearerToken(req);\n const usernameHint = safeUserText(body.username || req.query.username);\n\n const internalMcpAccount = resolveCodexAdapterToken(token);\n if (\n internalMcpAccount?.audience === 'mcp-plugin' &&\n req.path.startsWith('/api/mcp/apps/')\n ) {\n return internalMcpAccount.user;\n }\n\n const testToken = readText(process.env.TEST_ACCOUNT_TOKEN);\n if (testToken && token && token === testToken) {\n return {\n userId: 'user:test_account',\n username: 'TestAccount',\n tokenHash: sha256(token).slice(0, 24),\n isAuthenticated: true,\n isMember: true,\n isTestAccount: true,\n membership: { type: 'lifetime', active: true },\n };\n }\n\n const tokenHash = token ? sha256(token).slice(0, 24) : '';\n const anonymousHash = sha256(`${req.ip}|${req.get('User-Agent') || ''}`).slice(0, 24);\n const remoteAccount = await resolveRemoteAccount(token);\n const remoteIdentity = remoteAccount.id ||\n remoteAccount.username ||\n remoteAccount.email ||\n (remoteAccount.authenticated ? tokenHash : '');\n const userId = remoteAccount.authenticated\n ? `user:${remoteIdentity}`\n : tokenHash\n ? `token:${tokenHash}`\n : `anon:${anonymousHash}`;\n return {\n userId,\n username: remoteAccount.username || usernameHint,\n tokenHash,\n isAuthenticated: remoteAccount.authenticated,\n isMember: remoteAccount.isMember,\n membership: remoteAccount.membership,\n };\n}\n\nfunction base64UrlJson(value) {\n return Buffer.from(JSON.stringify(value), 'utf8').toString('base64url');\n}\n\nfunction timingSafeEqualText(left, right) {\n const leftBuffer = Buffer.from(String(left || ''));\n const rightBuffer = Buffer.from(String(right || ''));\n if (leftBuffer.length !== rightBuffer.length) return false;\n return crypto.timingSafeEqual(leftBuffer, rightBuffer);\n}\n\nfunction signCodexAdapterPayload(encodedPayload) {\n return crypto\n .createHmac('sha256', codexAdapterSecret)\n .update(encodedPayload)\n .digest('base64url');\n}\n\nfunction createCodexAdapterToken(user, options = {}) {\n const requestedMaxCompletionTokens = Number(options.maxCompletionTokens || 0);\n const payload = {\n sub: user.userId,\n username: user.username || '',\n tokenHash: user.tokenHash || '',\n isAuthenticated: Boolean(user.isAuthenticated),\n isMember: Boolean(user.isMember),\n isTestAccount: Boolean(user.isTestAccount),\n membership: user.membership || {},\n aud: safeUserText(options.audience) || 'codex-responses',\n recordUsage: options.recordUsage !== false,\n maxCompletionTokens:\n Number.isFinite(requestedMaxCompletionTokens) && requestedMaxCompletionTokens > 0\n ? Math.floor(requestedMaxCompletionTokens)\n : null,\n exp: Date.now() + codexAdapterTokenTtlMs,\n };\n const encodedPayload = base64UrlJson(payload);\n return `${codexAdapterTokenPrefix}${encodedPayload}.${signCodexAdapterPayload(encodedPayload)}`;\n}\n\nfunction resolveCodexAdapterToken(token) {\n const raw = readText(token);\n if (!raw.startsWith(codexAdapterTokenPrefix)) return null;\n\n const signed = raw.slice(codexAdapterTokenPrefix.length);\n const separator = signed.lastIndexOf('.');\n if (separator < 1) return null;\n\n const encodedPayload = signed.slice(0, separator);\n const signature = signed.slice(separator + 1);\n const expectedSignature = signCodexAdapterPayload(encodedPayload);\n if (!timingSafeEqualText(signature, expectedSignature)) return null;\n\n let payload;\n try {\n payload = JSON.parse(Buffer.from(encodedPayload, 'base64url').toString('utf8'));\n } catch {\n return null;\n }\n\n if (!payload || typeof payload !== 'object') return null;\n if (Number(payload.exp || 0) < Date.now()) return null;\n\n const userId = safeUserText(payload.sub);\n if (!userId) return null;\n\n return {\n user: {\n userId,\n username: safeUserText(payload.username),\n tokenHash: safeUserText(payload.tokenHash),\n isAuthenticated: Boolean(payload.isAuthenticated),\n isMember: Boolean(payload.isMember),\n isTestAccount: Boolean(payload.isTestAccount),\n membership:\n payload.membership && typeof payload.membership === 'object'\n ? payload.membership\n : {},\n },\n audience: safeUserText(payload.aud) || 'codex-responses',\n recordUsage: payload.recordUsage !== false,\n maxCompletionTokens:\n Number.isFinite(Number(payload.maxCompletionTokens)) && Number(payload.maxCompletionTokens) > 0\n ? Math.floor(Number(payload.maxCompletionTokens))\n : null,\n };\n}\n\nasync function resolveCodexResponsesBilling(req) {\n const token = bearerToken(req);\n const adapter = resolveCodexAdapterToken(token);\n if (adapter) return adapter;\n\n if (deepseekApiKey && token === deepseekApiKey) {\n const error = new Error('Codex DeepSeek adapter now requires first-party user billing');\n error.statusCode = 401;\n throw error;\n }\n\n return {\n user: await resolveUser(req, req.body || {}),\n recordUsage: true,\n };\n}\n\nfunction usageFor(userId) {\n const month = monthKey();\n const row = statements.getUsage.get(userId, month);\n return {\n month,\n used: Number(row?.totalTokens || 0),\n };\n}\n\nfunction enforceTokenBudget(user, estimatedTokensForRequest) {\n if (user && user.isTestAccount) {\n return { limit: 999999999, usage: { month: monthKey(), used: 0 } };\n }\n if (requireMemberForAi && !user.isMember) {\n const error = new Error('大乘 AI 需要开通会员后使用');\n error.statusCode = 403;\n error.code = 'AI_MEMBERSHIP_REQUIRED';\n error.details = {\n requireMemberForAi,\n monthlyLimit: memberMonthlyLimit,\n remainingTokens: 0,\n };\n throw error;\n }\n\n const limit = user.isMember ? memberMonthlyLimit : freeMonthlyLimit;\n const usage = usageFor(user.userId);\n if (usage.used + estimatedTokensForRequest > limit) {\n const error = new Error('本月 AI token 额度已不足');\n error.statusCode = 429;\n error.code = 'USER_TOKEN_QUOTA_EXCEEDED';\n error.details = {\n monthlyLimit: limit,\n usedTokens: usage.used,\n remainingTokens: Math.max(0, limit - usage.used),\n };\n throw error;\n }\n return { limit, usage };\n}\n\nfunction completionBudgetFor(\n budget,\n estimatedPromptTokens,\n maximumCompletionTokens = deepseekMaxCompletionTokens,\n) {\n const remainingAfterPrompt = budget.limit - budget.usage.used - estimatedPromptTokens;\n return Math.max(1, Math.min(maximumCompletionTokens, Math.floor(remainingAfterPrompt)));\n}\n\nfunction recordUsage(userId, totalTokens) {\n if (!totalTokens || totalTokens < 1) return;\n statements.upsertUsage.run({\n userId,\n month: monthKey(),\n totalTokens,\n updatedAt: nowIso(),\n });\n}\n\nfunction titleFromMessage(message) {\n const compact = message.replace(/\\s+/g, ' ').trim();\n if (!compact) return '新对话';\n return compact.length > 22 ? `${compact.slice(0, 22)}...` : compact;\n}\n\nfunction normalizeMessages(rows) {\n return rows.map((row) => ({\n role: row.role,\n content: row.content,\n }));\n}\n\nfunction normalizeMaxCompletionTokens(\n value,\n maximumCompletionTokens = deepseekMaxCompletionTokens,\n) {\n const numeric = Number(value || maximumCompletionTokens);\n if (!Number.isFinite(numeric) || numeric < 1) return maximumCompletionTokens;\n return Math.max(1, Math.min(maximumCompletionTokens, Math.floor(numeric)));\n}\n\nfunction normalizeDeepSeekModelName(value) {\n const raw = readText(value || deepseekModel);\n if (!raw) return deepseekModel;\n const modelId = raw.includes('/') ? raw.split('/').pop() : raw;\n return modelId || deepseekModel;\n}\n\nasync function callDeepSeek(messages, options = {}) {\n if (!deepseekApiKey) {\n const error = new Error('DeepSeek API key is not configured');\n error.statusCode = 503;\n throw error;\n }\n\n const model = normalizeDeepSeekModelName(options.model);\n const response = await fetch(`${deepseekBaseUrl}/chat/completions`, {\n method: 'POST',\n headers: {\n Accept: 'application/json',\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${deepseekApiKey}`,\n },\n body: JSON.stringify({\n model,\n messages,\n temperature: 0.4,\n max_tokens: normalizeMaxCompletionTokens(\n options.maxCompletionTokens,\n options.maximumCompletionTokens || deepseekMaxCompletionTokens,\n ),\n stream: false,\n ...(options.tools?.length ? { tools: options.tools, tool_choice: 'auto' } : {}),\n }),\n signal: AbortSignal.timeout(60_000),\n });\n\n const bodyText = await response.text();\n let payload;\n try {\n payload = bodyText ? JSON.parse(bodyText) : {};\n } catch {\n payload = { error: bodyText };\n }\n\n if (!response.ok) {\n const upstreamMessage = payload?.error?.message || payload?.message || '';\n const friendlyMessage =\n response.status === 402 || /insufficient\\s+balance/i.test(upstreamMessage)\n ? '大乘 AI 服务额度暂不可用,请联系管理员充值 DeepSeek 账户后重试。'\n : upstreamMessage || `DeepSeek request failed: ${response.status}`;\n const error = new Error(friendlyMessage);\n error.statusCode = response.status;\n if (response.status === 402 || /insufficient\\s+balance/i.test(upstreamMessage)) {\n error.code = 'DEEPSEEK_PROVIDER_QUOTA';\n }\n throw error;\n }\n\n const upstreamMessage = payload?.choices?.[0]?.message || {};\n const message = typeof upstreamMessage.content === 'string'\n ? upstreamMessage.content.trim()\n : '';\n const toolCalls = Array.isArray(upstreamMessage.tool_calls)\n ? upstreamMessage.tool_calls\n : [];\n if (!message && toolCalls.length === 0) {\n const error = new Error('DeepSeek returned an empty response');\n error.statusCode = 502;\n throw error;\n }\n\n return {\n message,\n toolCalls,\n model,\n usage: {\n promptTokens: Number(payload?.usage?.prompt_tokens || 0),\n completionTokens: Number(payload?.usage?.completion_tokens || 0),\n totalTokens: Number(payload?.usage?.total_tokens || 0),\n },\n };\n}\n\nasync function callDeepSeekStream(messages, callbacks = {}) {\n if (!deepseekApiKey) {\n const error = new Error('DeepSeek API key is not configured');\n error.statusCode = 503;\n throw error;\n }\n\n const model = normalizeDeepSeekModelName(callbacks.model);\n const response = await fetch(`${deepseekBaseUrl}/chat/completions`, {\n method: 'POST',\n headers: {\n Accept: 'text/event-stream',\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${deepseekApiKey}`,\n },\n body: JSON.stringify({\n model,\n messages,\n temperature: 0.4,\n max_tokens: normalizeMaxCompletionTokens(\n callbacks.maxCompletionTokens,\n callbacks.maximumCompletionTokens || deepseekMaxCompletionTokens,\n ),\n stream: true,\n stream_options: { include_usage: true },\n }),\n signal: callbacks.signal || AbortSignal.timeout(90_000),\n });\n\n if (!response.ok) {\n const bodyText = await response.text();\n let payload;\n try {\n payload = bodyText ? JSON.parse(bodyText) : {};\n } catch {\n payload = { error: bodyText };\n }\n const upstreamMessage = payload?.error?.message || payload?.message || '';\n const friendlyMessage =\n response.status === 402 || /insufficient\\s+balance/i.test(upstreamMessage)\n ? '大乘 AI 服务额度暂不可用,请联系管理员充值 DeepSeek 账户后重试。'\n : upstreamMessage || `DeepSeek request failed: ${response.status}`;\n const error = new Error(friendlyMessage);\n error.statusCode = response.status;\n if (response.status === 402 || /insufficient\\s+balance/i.test(upstreamMessage)) {\n error.code = 'DEEPSEEK_PROVIDER_QUOTA';\n }\n throw error;\n }\n\n let buffer = '';\n let message = '';\n let usage = {\n promptTokens: 0,\n completionTokens: 0,\n totalTokens: 0,\n };\n const decoder = new TextDecoder();\n\n for await (const chunk of response.body) {\n buffer += decoder.decode(chunk, { stream: true });\n const lines = buffer.split(/\\r?\\n/);\n buffer = lines.pop() || '';\n\n for (const rawLine of lines) {\n const line = rawLine.trim();\n if (!line.startsWith('data:')) continue;\n const data = line.slice('data:'.length).trim();\n if (!data || data === '[DONE]') continue;\n\n let payload;\n try {\n payload = JSON.parse(data);\n } catch {\n continue;\n }\n\n const delta = payload?.choices?.[0]?.delta?.content || '';\n if (delta) {\n message += delta;\n callbacks.onToken?.(delta);\n }\n\n if (payload?.usage) {\n usage = {\n promptTokens: Number(payload.usage.prompt_tokens || usage.promptTokens || 0),\n completionTokens: Number(payload.usage.completion_tokens || usage.completionTokens || 0),\n totalTokens: Number(payload.usage.total_tokens || usage.totalTokens || 0),\n };\n }\n }\n }\n\n message = message.trim();\n if (!message) {\n const error = new Error('DeepSeek returned an empty response');\n error.statusCode = 502;\n throw error;\n }\n\n return { message, model, usage };\n}\n\nfunction createCodexDeepSeekRuntime(user = null, runtimeOptions = {}) {\n if (!enableCodexSdkChat || !deepseekApiKey) {\n return {\n enabled: false,\n provider: codexDeepSeekProviderId,\n reason: enableCodexSdkChat ? 'DeepSeek API key is not configured' : 'ENABLE_CODEX_SDK_CHAT is not true',\n };\n }\n\n const adapterToken = user\n ? createCodexAdapterToken(user, {\n maxCompletionTokens: runtimeOptions.maxCompletionTokens,\n })\n : '';\n return {\n enabled: true,\n provider: codexDeepSeekProviderId,\n options: {\n apiKey: adapterToken,\n baseUrl: codexResponsesBaseUrl,\n env: {\n ...process.env,\n DEEPSEEK_API_KEY: adapterToken,\n HOME: runtimeOptions.codexHomeDir || codexHomeDir,\n CODEX_HOME: runtimeOptions.codexHomeDir || codexHomeDir,\n TMPDIR: codexTempDir,\n XDG_RUNTIME_DIR: codexRuntimeDir,\n XDG_CACHE_HOME: path.join(codexHomeDir, '.cache'),\n XDG_CONFIG_HOME: path.join(codexHomeDir, '.config'),\n XDG_DATA_HOME: path.join(codexHomeDir, '.local', 'share'),\n PATH: process.env.PATH || '/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin',\n ...(runtimeOptions.env || {}),\n },\n config: {\n model_provider: codexDeepSeekProviderId,\n model_providers: {\n [codexDeepSeekProviderId]: {\n name: 'Dacheng DeepSeek Proxy',\n base_url: codexResponsesBaseUrl,\n env_key: 'DEEPSEEK_API_KEY',\n wire_api: 'responses',\n query_params: {},\n },\n },\n ...(runtimeOptions.config || {}),\n },\n },\n threadOptions: {\n model: deepseekModel,\n approvalPolicy: runtimeOptions.approvalPolicy || 'never',\n sandboxMode: runtimeOptions.sandboxMode || 'read-only',\n ...(runtimeOptions.workingDirectory\n ? { workingDirectory: runtimeOptions.workingDirectory }\n : {}),\n skipGitRepoCheck: true,\n networkAccessEnabled: runtimeOptions.networkAccessEnabled ?? true,\n webSearchMode: runtimeOptions.webSearchMode || 'disabled',\n },\n };\n}\n\nfunction codexPromptFromMessages(messages) {\n return messages\n .map((message) => {\n const role = String(message.role || 'user').toUpperCase();\n return `${role}:\\n${message.content}`;\n })\n .join('\\n\\n');\n}\n\nfunction responseContentText(content) {\n if (typeof content === 'string') return content;\n if (!Array.isArray(content)) return '';\n return content\n .map((part) => {\n if (typeof part === 'string') return part;\n if (!part || typeof part !== 'object') return '';\n return String(part.text || part.input_text || part.output_text || '');\n })\n .filter(Boolean)\n .join('\\n');\n}\n\nfunction codexResponsesPrompt(body) {\n const input = Array.isArray(body?.input) ? body.input : [];\n const userItems = input.filter((item) => item?.role === 'user');\n const source = userItems[userItems.length - 1] || input[input.length - 1];\n const prompt = responseContentText(source?.content).trim();\n if (prompt) return prompt;\n if (typeof body?.input === 'string') return body.input.trim();\n return '';\n}\n\nfunction writeResponsesEvent(res, event, data) {\n res.write(`event: ${event}\\n`);\n res.write(`data: ${JSON.stringify(data)}\\n\\n`);\n}\n\nfunction responseUsage(usage = {}) {\n return {\n input_tokens: Number(usage.promptTokens || usage.input_tokens || 0),\n cached_input_tokens: 0,\n output_tokens: Number(usage.completionTokens || usage.output_tokens || 0),\n reasoning_output_tokens: 0,\n total_tokens: Number(usage.totalTokens || usage.total_tokens || 0),\n };\n}\n\nfunction createLibreChatAgentRuntime() {\n if (!enableLibreChatAgentChat || !libreChatAgentApiKey || !libreChatAgentId) {\n return {\n enabled: false,\n provider: 'librechat-agent',\n reason: !enableLibreChatAgentChat\n ? 'ENABLE_LIBRECHAT_AGENT_CHAT is not true'\n : !libreChatAgentApiKey\n ? 'LIBRECHAT_AGENT_API_KEY is not configured'\n : 'LIBRECHAT_AGENT_ID is not configured',\n };\n }\n\n return {\n enabled: true,\n provider: 'librechat-agent',\n model: libreChatAgentId,\n chatCompletionsUrl: `${libreChatAgentsBaseUrl}/chat/completions`,\n };\n}\n\nfunction libreChatUsage(usage = {}) {\n return {\n promptTokens: Number(usage.prompt_tokens || usage.input_tokens || usage.promptTokens || 0),\n completionTokens: Number(usage.completion_tokens || usage.output_tokens || usage.completionTokens || 0),\n totalTokens: Number(usage.total_tokens || usage.totalTokens || 0),\n };\n}\n\nasync function callLibreChatAgent(messages) {\n const runtime = createLibreChatAgentRuntime();\n if (!runtime.enabled) {\n const error = new Error(runtime.reason || 'LibreChat Agent chat is not enabled');\n error.statusCode = 503;\n throw error;\n }\n\n const response = await fetch(runtime.chatCompletionsUrl, {\n method: 'POST',\n headers: {\n Accept: 'application/json',\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${libreChatAgentApiKey}`,\n },\n body: JSON.stringify({\n model: runtime.model,\n messages,\n temperature: 0.4,\n stream: false,\n }),\n signal: AbortSignal.timeout(120_000),\n });\n\n const bodyText = await response.text();\n let payload;\n try {\n payload = bodyText ? JSON.parse(bodyText) : {};\n } catch {\n payload = { error: bodyText };\n }\n\n if (!response.ok) {\n const upstreamMessage = payload?.error?.message || payload?.message || bodyText || '';\n const error = new Error(upstreamMessage || `LibreChat Agent request failed: ${response.status}`);\n error.statusCode = response.status;\n throw error;\n }\n\n const message = payload?.choices?.[0]?.message?.content?.trim();\n if (!message) {\n const error = new Error('LibreChat Agent returned an empty response');\n error.statusCode = 502;\n throw error;\n }\n\n return {\n message,\n usage: libreChatUsage(payload?.usage),\n };\n}\n\nasync function callLibreChatAgentStream(messages, callbacks = {}) {\n const runtime = createLibreChatAgentRuntime();\n if (!runtime.enabled) {\n const error = new Error(runtime.reason || 'LibreChat Agent chat is not enabled');\n error.statusCode = 503;\n throw error;\n }\n\n const response = await fetch(runtime.chatCompletionsUrl, {\n method: 'POST',\n headers: {\n Accept: 'text/event-stream',\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${libreChatAgentApiKey}`,\n },\n body: JSON.stringify({\n model: runtime.model,\n messages,\n temperature: 0.4,\n stream: true,\n }),\n signal: callbacks.signal || AbortSignal.timeout(180_000),\n });\n\n if (!response.ok) {\n const bodyText = await response.text();\n let payload;\n try {\n payload = bodyText ? JSON.parse(bodyText) : {};\n } catch {\n payload = { error: bodyText };\n }\n const upstreamMessage = payload?.error?.message || payload?.message || bodyText || '';\n const error = new Error(upstreamMessage || `LibreChat Agent request failed: ${response.status}`);\n error.statusCode = response.status;\n throw error;\n }\n\n let buffer = '';\n let message = '';\n let usage = { promptTokens: 0, completionTokens: 0, totalTokens: 0 };\n const decoder = new TextDecoder();\n\n for await (const chunk of response.body) {\n buffer += decoder.decode(chunk, { stream: true });\n const lines = buffer.split(/\\r?\\n/);\n buffer = lines.pop() || '';\n\n for (const rawLine of lines) {\n const line = rawLine.trim();\n if (!line.startsWith('data:')) continue;\n const data = line.slice('data:'.length).trim();\n if (!data || data === '[DONE]') continue;\n\n let payload;\n try {\n payload = JSON.parse(data);\n } catch {\n continue;\n }\n\n const delta = payload?.choices?.[0]?.delta?.content || '';\n if (delta) {\n message += delta;\n callbacks.onToken?.(delta);\n }\n\n if (payload?.usage) {\n usage = libreChatUsage(payload.usage);\n }\n }\n }\n\n message = message.trim();\n if (!message) {\n const error = new Error('LibreChat Agent returned an empty response');\n error.statusCode = 502;\n throw error;\n }\n\n return { message, usage };\n}\n\nfunction isDeepSeekQuotaError(error) {\n if (error?.code === 'DEEPSEEK_PROVIDER_QUOTA') return true;\n if (error?.code === 'USER_TOKEN_QUOTA_EXCEEDED' || error?.code === 'AI_MEMBERSHIP_REQUIRED') return false;\n const message = String(error?.message || error || '');\n return /insufficient\\s+balance|balance/i.test(message);\n}\n\nfunction publicAiErrorMessage(error) {\n if (isDeepSeekQuotaError(error)) {\n return '大乘 AI 服务额度暂不可用,请联系管理员充值 DeepSeek 账户后重试。';\n }\n return error?.message || 'Internal Server Error';\n}\n\nfunction responsesPayload({\n responseId,\n itemId,\n status,\n model,\n text = '',\n usage = null,\n error = null,\n outputItems = null,\n}) {\n return {\n id: responseId,\n object: 'response',\n status,\n model,\n output: status === 'completed'\n ? outputItems || [\n {\n id: itemId,\n type: 'message',\n role: 'assistant',\n status: 'completed',\n content: [{ type: 'output_text', text }],\n },\n ]\n : [],\n ...(usage ? { usage: responseUsage(usage) } : {}),\n ...(error\n ? {\n error: {\n message: error.message || String(error),\n type: 'server_error',\n },\n }\n : {}),\n };\n}\n\nasync function callCodexSdkDeepSeek(messages, callbacks = {}) {\n const codexRuntime = createCodexDeepSeekRuntime(callbacks.user, {\n maxCompletionTokens: callbacks.maxCompletionTokens,\n workingDirectory: callbacks.workingDirectory,\n sandboxMode: callbacks.sandboxMode,\n approvalPolicy: callbacks.approvalPolicy,\n networkAccessEnabled: callbacks.networkAccessEnabled,\n webSearchMode: callbacks.webSearchMode,\n codexHomeDir: callbacks.codexHomeDir,\n env: callbacks.env,\n config: callbacks.config,\n });\n if (!codexRuntime.enabled) {\n const error = new Error(codexRuntime.reason || 'Codex SDK chat is not enabled');\n error.statusCode = 503;\n throw error;\n }\n if (!callbacks.user) {\n const error = new Error('Codex SDK DeepSeek proxy requires a resolved user for billing');\n error.statusCode = 401;\n throw error;\n }\n\n const { Codex } = await import('@openai/codex-sdk');\n const codex = new Codex(codexRuntime.options);\n const thread = callbacks.threadId\n ? codex.resumeThread(callbacks.threadId, codexRuntime.threadOptions)\n : codex.startThread(codexRuntime.threadOptions);\n const prompt = codexPromptFromMessages(messages);\n const turnOptions = {\n ...(callbacks.outputSchema ? { outputSchema: callbacks.outputSchema } : {}),\n ...(callbacks.signal ? { signal: callbacks.signal } : {}),\n };\n\n if (callbacks.onToken || callbacks.onStep) {\n const { events } = await thread.runStreamed(prompt, turnOptions);\n let finalResponse = '';\n let usage = null;\n let threadId = callbacks.threadId || null;\n\n for await (const event of events) {\n if (event.type === 'thread.started') {\n threadId = event.thread_id || threadId;\n } else if (event.type === 'item.completed') {\n const item = event.item;\n if (item.type === 'agent_message') {\n finalResponse = item.text || finalResponse;\n callbacks.onToken?.(item.text || '');\n } else if (item.type === 'todo_list') {\n callbacks.onStep?.({\n title: 'Codex SDK 执行计划',\n message: item.items.map((todo) => `${todo.completed ? '✓' : '•'} ${todo.text}`).join('\\n'),\n });\n } else if (item.type === 'command_execution') {\n callbacks.onStep?.({\n title: 'Codex SDK 执行命令',\n message: item.command,\n });\n } else if (item.type === 'file_change') {\n callbacks.onStep?.({\n title: 'Codex SDK 修改文件',\n message: item.changes.map((change) => `${change.kind}: ${change.path}`).join('\\n'),\n });\n } else if (item.type === 'mcp_tool_call') {\n callbacks.onStep?.({\n title: 'Codex SDK 调用工具',\n message: `${item.server}.${item.tool}`,\n });\n }\n } else if (event.type === 'turn.completed') {\n usage = event.usage;\n } else if (event.type === 'turn.failed') {\n throw new Error(event.error?.message || 'Codex SDK turn failed');\n } else if (event.type === 'error') {\n throw new Error(event.message || 'Codex SDK stream failed');\n }\n }\n\n if (!finalResponse.trim()) {\n const error = new Error('Codex SDK returned an empty response');\n error.statusCode = 502;\n throw error;\n }\n\n return {\n message: finalResponse.trim(),\n usage: {\n promptTokens: Number(usage?.input_tokens || 0),\n completionTokens: Number(usage?.output_tokens || 0),\n totalTokens: Number((usage?.input_tokens || 0) + (usage?.output_tokens || 0)),\n },\n usageAlreadyRecorded: true,\n threadId: thread.id || threadId,\n };\n }\n\n const turn = await thread.run(prompt, turnOptions);\n if (!turn.finalResponse?.trim()) {\n const error = new Error('Codex SDK returned an empty response');\n error.statusCode = 502;\n throw error;\n }\n\n return {\n message: turn.finalResponse.trim(),\n usage: {\n promptTokens: Number(turn.usage?.input_tokens || 0),\n completionTokens: Number(turn.usage?.output_tokens || 0),\n totalTokens: Number((turn.usage?.input_tokens || 0) + (turn.usage?.output_tokens || 0)),\n },\n usageAlreadyRecorded: true,\n threadId: thread.id || callbacks.threadId || null,\n };\n}\n\nconst activeAgentRuns = new Map();\n\nfunction normalizeAgentMode(value) {\n const mode = safeUserText(value || 'dharma_guide');\n return /^[a-z][a-z0-9_:-]{0,48}$/i.test(mode) ? mode : 'dharma_guide';\n}\n\nfunction openClawSessionKey(user, conversationId) {\n const env = process.env.NODE_ENV === 'production' ? 'prod' : 'dev';\n return `fabushi:env:${env}:user:${user.userId}:conversation:${conversationId}`;\n}\n\nfunction createOpenClawRuntime() {\n if (!enableOpenClawAgentChat || !openClawGatewayToken || !openClawGatewayUrl) {\n return {\n enabled: false,\n provider: 'openclaw-gateway',\n reason: !enableOpenClawAgentChat\n ? 'ENABLE_OPENCLAW_AGENT_CHAT is not true'\n : !openClawGatewayToken\n ? 'OPENCLAW_GATEWAY_TOKEN is not configured'\n : 'OPENCLAW_GATEWAY_URL is not configured',\n };\n }\n\n return {\n enabled: true,\n provider: 'openclaw-gateway',\n model: openClawAgentId,\n runsUrl: openClawRunsEndpoint || `${openClawGatewayUrl}/v1/agents/${encodeURIComponent(openClawAgentId)}/runs`,\n };\n}\n\nfunction extractOpenClawText(payload) {\n if (!payload || typeof payload !== 'object') return '';\n const candidates = [\n payload.message,\n payload.content,\n payload.output_text,\n payload.finalResponse,\n payload.final_response,\n payload.result?.message,\n payload.result?.content,\n payload.output?.message,\n payload.output?.content,\n payload.choices?.[0]?.message?.content,\n ];\n for (const candidate of candidates) {\n if (typeof candidate === 'string' && candidate.trim()) return candidate.trim();\n }\n return '';\n}\n\nfunction extractOpenClawUsage(payload) {\n const usage = payload?.usage || payload?.result?.usage || {};\n return {\n promptTokens: Number(usage.input_tokens || usage.prompt_tokens || usage.promptTokens || 0),\n completionTokens: Number(usage.output_tokens || usage.completion_tokens || usage.completionTokens || 0),\n totalTokens: Number(usage.total_tokens || usage.totalTokens || 0),\n };\n}\n\nasync function callOpenClawAgent({ messages, user, conversationId, mode, signal }) {\n const runtime = createOpenClawRuntime();\n if (!runtime.enabled) {\n const error = new Error(runtime.reason || 'OpenClaw Agent chat is not enabled');\n error.statusCode = 503;\n throw error;\n }\n\n const sessionKey = openClawSessionKey(user, conversationId);\n const response = await fetch(runtime.runsUrl, {\n method: 'POST',\n headers: {\n Accept: 'application/json',\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${openClawGatewayToken}`,\n },\n body: JSON.stringify({\n agentId: openClawAgentId,\n sessionKey,\n mode,\n stream: false,\n input: { messages },\n messages,\n metadata: {\n app: 'fabushi',\n userId: user.userId,\n conversationId,\n toolPolicy: 'public-user-read-mostly',\n },\n }),\n signal: signal || AbortSignal.timeout(180_000),\n });\n\n const bodyText = await response.text();\n let payload;\n try {\n payload = bodyText ? JSON.parse(bodyText) : {};\n } catch {\n payload = { message: bodyText };\n }\n\n if (!response.ok) {\n const error = new Error(payload?.error?.message || payload?.message || `OpenClaw request failed: ${response.status}`);\n error.statusCode = response.status;\n throw error;\n }\n\n const message = extractOpenClawText(payload);\n if (!message) {\n const error = new Error('OpenClaw returned an empty response');\n error.statusCode = 502;\n throw error;\n }\n\n return {\n message,\n runId: payload.runId || payload.id || payload.result?.runId || null,\n provider: runtime.provider,\n model: runtime.model,\n usage: extractOpenClawUsage(payload),\n };\n}\n\nasync function runAgentModel({\n messages,\n user,\n conversationId,\n mode,\n callbacks = {},\n signal,\n maxCompletionTokens,\n preferCodex = false,\n requireCodex = false,\n workingDirectory,\n sandboxMode,\n outputSchema,\n threadId,\n}) {\n const codexRuntime = createCodexDeepSeekRuntime(user, {\n workingDirectory,\n sandboxMode,\n maxCompletionTokens,\n });\n if (requireCodex && !codexRuntime.enabled) {\n const error = new Error(codexRuntime.reason || 'Codex SDK chat is not enabled');\n error.statusCode = 503;\n throw error;\n }\n\n let preferredCodexFailed = false;\n if ((preferCodex || requireCodex) && codexRuntime.enabled) {\n try {\n const result = await callCodexSdkDeepSeek(messages, {\n ...callbacks,\n user,\n signal,\n maxCompletionTokens,\n workingDirectory,\n sandboxMode,\n outputSchema,\n threadId,\n });\n return {\n ...result,\n provider: codexRuntime.provider,\n model: deepseekModel,\n };\n } catch (error) {\n if (requireCodex || isDeepSeekQuotaError(error)) throw error;\n logger.warn(\n { error: String(error), mode },\n 'Preferred Codex SDK model failed, continuing through agent fallbacks',\n );\n preferredCodexFailed = true;\n }\n }\n\n const openClawRuntime = createOpenClawRuntime();\n if (openClawRuntime.enabled && !callbacks.onToken) {\n return await callOpenClawAgent({ messages, user, conversationId, mode, signal });\n }\n\n const libreChatRuntime = createLibreChatAgentRuntime();\n let provider = libreChatRuntime.enabled\n ? libreChatRuntime.provider\n : codexRuntime.enabled && !preferredCodexFailed\n ? codexRuntime.provider\n : 'deepseek';\n let model = libreChatRuntime.enabled ? libreChatRuntime.model : deepseekModel;\n\n try {\n if (libreChatRuntime.enabled) {\n const result = callbacks.onToken\n ? await callLibreChatAgentStream(messages, { ...callbacks, signal })\n : await callLibreChatAgent(messages);\n return { ...result, provider, model };\n }\n if (codexRuntime.enabled && !preferredCodexFailed) {\n const result = await callCodexSdkDeepSeek(messages, {\n ...callbacks,\n user,\n signal,\n maxCompletionTokens,\n workingDirectory,\n sandboxMode,\n outputSchema,\n threadId,\n });\n return { ...result, provider, model };\n }\n const result = callbacks.onToken\n ? await callDeepSeekStream(messages, { ...callbacks, signal, maxCompletionTokens })\n : await callDeepSeek(messages, { maxCompletionTokens });\n return { ...result, provider, model };\n } catch (error) {\n if (!codexRuntime.enabled || preferredCodexFailed || isDeepSeekQuotaError(error)) throw error;\n logger.warn({ error: String(error) }, 'Agent model failed, falling back to DeepSeek direct');\n const result = callbacks.onToken\n ? await callDeepSeekStream(messages, { ...callbacks, signal, maxCompletionTokens })\n : await callDeepSeek(messages, { maxCompletionTokens });\n return { ...result, provider: 'deepseek', model: deepseekModel };\n }\n}\n\nfunction updateAgentRunStatus(run, status, fields = {}) {\n statements.updateAgentRunStatus.run({\n id: run.id,\n userId: run.user_id || run.userId,\n status,\n openClawRunId: fields.openClawRunId ?? null,\n inputTokens: fields.inputTokens ?? null,\n outputTokens: fields.outputTokens ?? null,\n toolCallCount: fields.toolCallCount ?? null,\n completedAt: status === 'completed' || status === 'cancelled' ? nowIso() : null,\n failedAt: status === 'failed' ? nowIso() : null,\n errorCode: fields.errorCode ?? null,\n errorMessage: fields.errorMessage ?? null,\n });\n}\n\nfunction buildAgentMessages(conversationId) {\n const historyRows = statements.listMessages.all(conversationId).slice(-14);\n return [{ role: 'system', content: systemPrompt }, ...normalizeMessages(historyRows)];\n}\n\nfunction agentUsagePayload(usage, user, budget) {\n const promptTokens = usage.promptTokens || usage.inputTokens || 0;\n const completionTokens = usage.completionTokens || usage.outputTokens || 0;\n const totalTokens = usage.totalTokens || promptTokens + completionTokens;\n const latestUsage = usageFor(user.userId);\n return {\n inputTokens: promptTokens,\n outputTokens: completionTokens,\n totalTokens,\n monthlyLimit: budget.limit,\n remainingTokens: Math.max(0, budget.limit - latestUsage.used),\n };\n}\n\nfunction openAiContentToText(content) {\n if (typeof content === 'string') return content;\n if (Array.isArray(content)) {\n return content\n .map((part) => {\n if (typeof part === 'string') return part;\n if (!part || typeof part !== 'object') return '';\n return readText(part.text || part.content || part.value);\n })\n .filter(Boolean)\n .join('\\n');\n }\n return readText(content);\n}\n\nfunction normalizeOpenAiMessages(value) {\n if (!Array.isArray(value)) return [];\n const allowedRoles = new Set(['system', 'user', 'assistant', 'tool']);\n return value\n .map((item) => {\n if (!item || typeof item !== 'object') return null;\n const role = allowedRoles.has(item.role) ? item.role : 'user';\n const content = openAiContentToText(item.content);\n if (!content) return null;\n return { role, content };\n })\n .filter(Boolean);\n}\n\nfunction truncateForDeepSeekMessage(text, maxChars = 12_000) {\n const value = readText(text);\n if (value.length <= maxChars) return value;\n return `${value.slice(0, maxChars)}\\n\\n[内容已截断]`;\n}\n\nfunction truncateTailForDeepSeekMessage(text, maxChars = 2400) {\n const value = readText(text);\n if (value.length <= maxChars) return value;\n return `[前文已省略]\\n\\n${value.slice(-maxChars)}`;\n}\n\nfunction stripOpenClawPromptNoise(text) {\n const value = readText(text);\n if (!value) return '';\n\n const finalPromptMatch = value.match(/finalPromptText[\"'\\s::]+([\\s\\S]{1,4000})$/i);\n if (finalPromptMatch?.[1]) {\n return readText(finalPromptMatch[1].replace(/^[\"'`]+|[\"'`,}\\]]+$/g, ''));\n }\n\n const markers = [\n 'final prompt:',\n 'final user prompt:',\n 'user prompt:',\n '用户问题:',\n '用户消息:',\n '用户输入:',\n ];\n const lower = value.toLowerCase();\n for (const marker of markers) {\n const index = lower.lastIndexOf(marker.toLowerCase());\n if (index >= 0) {\n const tail = value.slice(index + marker.length).trim();\n if (tail) return tail;\n }\n }\n\n return value;\n}\n\nfunction compactOpenClawMessagesForDeepSeek(messages) {\n const conversationalMessages = messages\n .filter((message) => message.role === 'user' || message.role === 'assistant')\n .map((message) => ({\n role: message.role,\n content: truncateTailForDeepSeekMessage(stripOpenClawPromptNoise(message.content)),\n }))\n .filter((message) => message.content);\n\n const recent = conversationalMessages.slice(-6);\n while (recent.length > 0 && recent[0].role !== 'user') {\n recent.shift();\n }\n\n if (recent.length === 0) {\n const fallback = messages\n .slice()\n .reverse()\n .map((message) => truncateTailForDeepSeekMessage(stripOpenClawPromptNoise(message.content), 2400))\n .find(Boolean);\n if (!fallback) return messages;\n recent.push({ role: 'user', content: fallback });\n }\n\n return [\n {\n role: 'system',\n content: '你是大乘 App 的桌面 AI 助手。请直接回答用户问题,默认使用简洁、准确的中文。',\n },\n ...recent,\n ];\n}\n\nfunction isDeepSeekBlockedError(error) {\n return (\n Number(error?.statusCode) === 403 &&\n /blocked|被拦截|forbidden/i.test(String(error?.message || error || ''))\n );\n}\n\nfunction shouldRetryWithCompactedOpenClawMessages(error, messages, compactedMessages) {\n if (!isDeepSeekBlockedError(error)) return false;\n return JSON.stringify(messages) !== JSON.stringify(compactedMessages);\n}\n\nfunction openClawBlockedFallbackText(messages) {\n const lastUser = messages\n .slice()\n .reverse()\n .find((message) => message.role === 'user')?.content || '';\n const localActionIntent =\n /(file|folder|directory|project|workspace|path|文件|文件夹|目录|项目|工程|路径|本地)/i.test(lastUser) &&\n /(create|make|mkdir|write|edit|modify|rename|delete|move|copy|save|download|read|list|生成|创建|新建|写入|编辑|修改|重命名|删除|移动|复制|保存|下载|读取|列出)/i.test(lastUser);\n\n if (localActionIntent) {\n return [\n '本机工具规划被模型服务拦截了,所以这次没有实际读取、创建、修改或下载本机文件。',\n '如果只是新建文件夹,请先在底部选择项目后重新发送“新建文件夹”;桌面端会直接在已选项目里创建。',\n '如果是更复杂的本地文件操作,请在“连应用”里重启本机 AI 后重试,或把要操作的文件路径和目标说得更具体。',\n ].join('\\n');\n }\n\n return '模型服务拦截了这次回复,未能生成结果。请换一种更具体、更短的表述后重试。';\n}\nfunction usageWithFallback(usage, messages, text) {\n const promptTokens = Number(usage?.promptTokens || usage?.inputTokens || 0);\n const completionTokens = Number(usage?.completionTokens || usage?.outputTokens || 0);\n const totalTokens = Number(usage?.totalTokens || 0);\n if (totalTokens > 0) {\n return {\n promptTokens,\n completionTokens,\n totalTokens,\n };\n }\n const estimatedPrompt = promptTokens || estimateTokens(JSON.stringify(messages));\n const estimatedCompletion = completionTokens || estimateTokens(text);\n return {\n promptTokens: estimatedPrompt,\n completionTokens: estimatedCompletion,\n totalTokens: estimatedPrompt + estimatedCompletion,\n };\n}\n\nfunction openAiUsagePayload(usage, user, budget) {\n const promptTokens = Number(usage.promptTokens || 0);\n const completionTokens = Number(usage.completionTokens || 0);\n const totalTokens = Number(usage.totalTokens || promptTokens + completionTokens);\n const latestUsage = usageFor(user.userId);\n const remainingTokens = Math.max(0, budget.limit - latestUsage.used);\n return {\n prompt_tokens: promptTokens,\n completion_tokens: completionTokens,\n total_tokens: totalTokens,\n promptTokens,\n completionTokens,\n totalTokens,\n monthlyLimit: budget.limit,\n remainingTokens,\n };\n}\n\nfunction writeOpenAiSse(res, payload) {\n res.write(`data: ${JSON.stringify(payload)}\\n\\n`);\n}\n\napp.post(\n '/api/openclaw/deepseek/v1/chat/completions',\n asyncHandler(async (req, res) => {\n const user = await resolveUser(req, req.body || {});\n const incomingMessages = normalizeOpenAiMessages(req.body?.messages);\n if (incomingMessages.length === 0) {\n res.status(400).json({ error: { message: 'messages is required' } });\n return;\n }\n\n const messages = compactOpenClawMessagesForDeepSeek(incomingMessages);\n const promptEstimate = estimateTokens(JSON.stringify(messages));\n const estimated = promptEstimate + 600;\n const budget = enforceTokenBudget(user, estimated);\n const requestCompletionLimit = deepseekMaxCompletionTokens;\n const maxCompletionTokens = Math.min(\n completionBudgetFor(budget, promptEstimate, requestCompletionLimit),\n normalizeMaxCompletionTokens(req.body?.max_tokens, requestCompletionLimit),\n );\n const model = normalizeDeepSeekModelName(req.body?.model);\n const wantsStream =\n req.body?.stream === true ||\n String(req.get('accept') || '').includes('text/event-stream');\n const id = `chatcmpl_${crypto.randomUUID().replaceAll('-', '')}`;\n const created = Math.floor(Date.now() / 1000);\n const compactedMessages = messages;\n\n if (!wantsStream) {\n let result;\n let usageMessages = messages;\n try {\n result = await callDeepSeek(messages, {\n model,\n maxCompletionTokens,\n maximumCompletionTokens: requestCompletionLimit,\n });\n } catch (error) {\n if (!shouldRetryWithCompactedOpenClawMessages(error, messages, compactedMessages)) {\n throw error;\n }\n logger.warn(\n {\n statusCode: error?.statusCode,\n messageCount: messages.length,\n compactedMessageCount: compactedMessages.length,\n },\n 'OpenClaw DeepSeek proxy retrying with compacted messages',\n );\n usageMessages = compactedMessages;\n result = await callDeepSeek(compactedMessages, {\n model,\n maxCompletionTokens,\n maximumCompletionTokens: requestCompletionLimit,\n });\n }\n const usage = usageWithFallback(result.usage, usageMessages, result.message);\n recordUsage(user.userId, usage.totalTokens);\n res.json({\n id,\n object: 'chat.completion',\n created,\n model: result.model || model,\n choices: [\n {\n index: 0,\n message: { role: 'assistant', content: result.message },\n finish_reason: 'stop',\n },\n ],\n usage: openAiUsagePayload(usage, user, budget),\n });\n return;\n }\n\n const abortController = new AbortController();\n req.on('close', () => abortController.abort());\n\n res.status(200);\n res.set({\n 'Content-Type': 'text/event-stream; charset=utf-8',\n 'Cache-Control': 'no-cache, no-transform',\n Connection: 'keep-alive',\n 'X-Accel-Buffering': 'no',\n });\n res.flushHeaders?.();\n\n const baseChunk = {\n id,\n object: 'chat.completion.chunk',\n created,\n model,\n system_fingerprint: 'fabushi-openclaw-deepseek-proxy',\n };\n writeOpenAiSse(res, {\n ...baseChunk,\n choices: [{ index: 0, delta: { role: 'assistant' }, finish_reason: null }],\n });\n\n let text = '';\n let usageMessages = messages;\n try {\n let result;\n try {\n result = await callDeepSeekStream(messages, {\n model,\n maxCompletionTokens,\n maximumCompletionTokens: requestCompletionLimit,\n signal: abortController.signal,\n onToken: (delta) => {\n text += delta;\n writeOpenAiSse(res, {\n ...baseChunk,\n choices: [{ index: 0, delta: { content: delta }, finish_reason: null }],\n });\n },\n });\n } catch (error) {\n if (!shouldRetryWithCompactedOpenClawMessages(error, messages, compactedMessages)) {\n throw error;\n }\n logger.warn(\n {\n statusCode: error?.statusCode,\n messageCount: messages.length,\n compactedMessageCount: compactedMessages.length,\n },\n 'OpenClaw DeepSeek proxy stream retrying with compacted messages',\n );\n text = '';\n usageMessages = compactedMessages;\n result = await callDeepSeekStream(compactedMessages, {\n model,\n maxCompletionTokens,\n maximumCompletionTokens: requestCompletionLimit,\n signal: abortController.signal,\n onToken: (delta) => {\n text += delta;\n writeOpenAiSse(res, {\n ...baseChunk,\n choices: [{ index: 0, delta: { content: delta }, finish_reason: null }],\n });\n },\n });\n }\n text = result.message || text;\n const usage = usageWithFallback(result.usage, usageMessages, text);\n recordUsage(user.userId, usage.totalTokens);\n writeOpenAiSse(res, {\n ...baseChunk,\n model: result.model || model,\n choices: [{ index: 0, delta: {}, finish_reason: 'stop' }],\n });\n writeOpenAiSse(res, {\n ...baseChunk,\n model: result.model || model,\n choices: [],\n usage: openAiUsagePayload(usage, user, budget),\n });\n res.write('data: [DONE]\\n\\n');\n } catch (error) {\n if (isDeepSeekBlockedError(error)) {\n const fallbackMessage = openClawBlockedFallbackText(usageMessages);\n const usage = usageWithFallback(null, usageMessages, fallbackMessage);\n recordUsage(user.userId, usage.totalTokens);\n logger.warn(\n { statusCode: error?.statusCode, code: error?.code },\n 'OpenClaw DeepSeek proxy stream blocked; returning local fallback message',\n );\n writeOpenAiSse(res, {\n ...baseChunk,\n choices: [{ index: 0, delta: { content: fallbackMessage }, finish_reason: null }],\n });\n writeOpenAiSse(res, {\n ...baseChunk,\n choices: [{ index: 0, delta: {}, finish_reason: 'stop' }],\n });\n writeOpenAiSse(res, {\n ...baseChunk,\n choices: [],\n usage: openAiUsagePayload(usage, user, budget),\n });\n res.write('data: [DONE]\\n\\n');\n return;\n }\n logger.error(\n { error: String(error), code: error?.code, statusCode: error?.statusCode },\n 'OpenClaw DeepSeek proxy stream failed',\n );\n writeOpenAiSse(res, {\n ...baseChunk,\n choices: [],\n error: {\n message: publicAiErrorMessage(error),\n code: error?.code || 'OPENCLAW_DEEPSEEK_PROXY_ERROR',\n },\n });\n res.write('data: [DONE]\\n\\n');\n } finally {\n res.end();\n }\n }),\n);\n\napp.post(\n '/codex-deepseek/v1/responses',\n asyncHandler(async (req, res) => {\n if (!deepseekApiKey) {\n res.status(401).json({\n error: { message: 'Unauthorized Codex DeepSeek adapter request' },\n });\n return;\n }\n\n const messages = codexResponsesMessages(req.body);\n if (messages.length === 0) {\n res.status(400).json({ error: { message: 'Responses input is required' } });\n return;\n }\n const { tools: responseTools, kinds: responseToolKinds } = codexResponsesTools(req.body);\n\n const {\n user,\n recordUsage: shouldRecordUsage,\n maxCompletionTokens: adapterMaxCompletionTokens,\n } = await resolveCodexResponsesBilling(req);\n const promptEstimate = estimateTokens(JSON.stringify({ messages, tools: responseTools }));\n const budget = enforceTokenBudget(user, promptEstimate + 600);\n const responseCompletionLimit = adapterMaxCompletionTokens || deepseekMaxCompletionTokens;\n const maxCompletionTokens = Math.min(\n completionBudgetFor(budget, promptEstimate, responseCompletionLimit),\n normalizeMaxCompletionTokens(\n req.body?.max_output_tokens || req.body?.max_tokens,\n responseCompletionLimit,\n ),\n );\n const model = normalizeDeepSeekModelName(req.body?.model);\n const responseId = `resp_${crypto.randomUUID().replaceAll('-', '')}`;\n const itemId = `msg_${crypto.randomUUID().replaceAll('-', '')}`;\n const wantsStream =\n req.body?.stream !== false ||\n String(req.get('accept') || '').includes('text/event-stream');\n\n if (!wantsStream) {\n const result = await callDeepSeek(messages, {\n model,\n maxCompletionTokens,\n maximumCompletionTokens: responseCompletionLimit,\n tools: responseTools,\n });\n const outputItems = deepSeekResultToResponseItems(\n result,\n responseToolKinds,\n (prefix) => `${prefix}_${crypto.randomUUID().replaceAll('-', '')}`,\n );\n const usage = usageWithFallback(\n result.usage,\n messages,\n result.message || JSON.stringify(result.toolCalls || []),\n );\n if (shouldRecordUsage) recordUsage(user.userId, usage.totalTokens);\n res.json(\n responsesPayload({\n responseId,\n itemId,\n status: 'completed',\n model: result.model || model,\n text: result.message,\n usage,\n outputItems,\n }),\n );\n return;\n }\n\n if (responseTools.length > 0) {\n res.status(200);\n res.set({\n 'Content-Type': 'text/event-stream; charset=utf-8',\n 'Cache-Control': 'no-cache, no-transform',\n Connection: 'keep-alive',\n 'X-Accel-Buffering': 'no',\n });\n res.flushHeaders?.();\n writeResponsesEvent(res, 'response.created', {\n type: 'response.created',\n response: responsesPayload({ responseId, itemId, status: 'in_progress', model }),\n });\n try {\n const result = await callDeepSeek(messages, {\n model,\n maxCompletionTokens,\n maximumCompletionTokens: responseCompletionLimit,\n tools: responseTools,\n });\n const outputItems = deepSeekResultToResponseItems(\n result,\n responseToolKinds,\n (prefix) => `${prefix}_${crypto.randomUUID().replaceAll('-', '')}`,\n );\n outputItems.forEach((item, outputIndex) => {\n const addedItem = item.type === 'message'\n ? { ...item, status: 'in_progress', content: [] }\n : { ...item, status: 'in_progress' };\n writeResponsesEvent(res, 'response.output_item.added', {\n type: 'response.output_item.added',\n output_index: outputIndex,\n item: addedItem,\n });\n if (item.type === 'message') {\n const text = item.content?.find((part) => part.type === 'output_text')?.text || '';\n if (text) {\n writeResponsesEvent(res, 'response.output_text.delta', {\n type: 'response.output_text.delta',\n item_id: item.id,\n output_index: outputIndex,\n content_index: 0,\n delta: text,\n });\n }\n }\n writeResponsesEvent(res, 'response.output_item.done', {\n type: 'response.output_item.done',\n output_index: outputIndex,\n item,\n });\n });\n const usage = usageWithFallback(\n result.usage,\n messages,\n result.message || JSON.stringify(result.toolCalls || []),\n );\n if (shouldRecordUsage) recordUsage(user.userId, usage.totalTokens);\n writeResponsesEvent(res, 'response.completed', {\n type: 'response.completed',\n response: responsesPayload({\n responseId,\n itemId,\n status: 'completed',\n model: result.model || model,\n text: result.message,\n usage,\n outputItems,\n }),\n });\n } catch (error) {\n writeResponsesEvent(res, 'response.failed', {\n type: 'response.failed',\n response: responsesPayload({\n responseId,\n itemId,\n status: 'failed',\n model,\n error,\n }),\n });\n }\n res.end();\n return;\n }\n\n res.status(200);\n res.set({\n 'Content-Type': 'text/event-stream; charset=utf-8',\n 'Cache-Control': 'no-cache, no-transform',\n Connection: 'keep-alive',\n 'X-Accel-Buffering': 'no',\n });\n res.flushHeaders?.();\n\n writeResponsesEvent(res, 'response.created', {\n type: 'response.created',\n response: responsesPayload({\n responseId,\n itemId,\n status: 'in_progress',\n model,\n }),\n });\n writeResponsesEvent(res, 'response.output_item.added', {\n type: 'response.output_item.added',\n output_index: 0,\n item: {\n id: itemId,\n type: 'message',\n role: 'assistant',\n status: 'in_progress',\n content: [],\n },\n });\n writeResponsesEvent(res, 'response.content_part.added', {\n type: 'response.content_part.added',\n item_id: itemId,\n output_index: 0,\n content_index: 0,\n part: { type: 'output_text', text: '' },\n });\n\n let text = '';\n try {\n const result = await callDeepSeekStream(messages, {\n model,\n maxCompletionTokens,\n maximumCompletionTokens: responseCompletionLimit,\n onToken: (delta) => {\n text += delta;\n writeResponsesEvent(res, 'response.output_text.delta', {\n type: 'response.output_text.delta',\n item_id: itemId,\n output_index: 0,\n content_index: 0,\n delta,\n });\n },\n });\n text = result.message || text;\n const usage = usageWithFallback(result.usage, messages, text);\n if (shouldRecordUsage) recordUsage(user.userId, usage.totalTokens);\n writeResponsesEvent(res, 'response.output_text.done', {\n type: 'response.output_text.done',\n item_id: itemId,\n output_index: 0,\n content_index: 0,\n text,\n });\n writeResponsesEvent(res, 'response.content_part.done', {\n type: 'response.content_part.done',\n item_id: itemId,\n output_index: 0,\n content_index: 0,\n part: { type: 'output_text', text },\n });\n writeResponsesEvent(res, 'response.output_item.done', {\n type: 'response.output_item.done',\n output_index: 0,\n item: {\n id: itemId,\n type: 'message',\n role: 'assistant',\n status: 'completed',\n content: [{ type: 'output_text', text }],\n },\n });\n writeResponsesEvent(res, 'response.completed', {\n type: 'response.completed',\n response: responsesPayload({\n responseId,\n itemId,\n status: 'completed',\n model: result.model || model,\n text,\n usage,\n }),\n });\n res.end();\n } catch (error) {\n writeResponsesEvent(res, 'response.failed', {\n type: 'response.failed',\n response: responsesPayload({\n responseId,\n itemId,\n status: 'failed',\n model,\n text,\n error,\n }),\n });\n res.end();\n }\n }),\n);\n\napp.use(\n '/api/openclaw/runtime/files',\n express.static(openClawRuntimeUpdatesDir, {\n fallthrough: false,\n immutable: true,\n maxAge: '7d',\n setHeaders(res) {\n res.setHeader('X-Content-Type-Options', 'nosniff');\n },\n }),\n);\n\napp.get(\n '/api/openclaw/runtime/manifest',\n asyncHandler(async (req, res) => {\n const platform = readText(req.query.platform);\n const currentVersion = readText(req.query.currentVersion);\n const appVersion = readText(req.query.appVersion);\n\n let manifest;\n try {\n manifest = await readJsonFile(openClawRuntimeManifestPath);\n } catch (error) {\n logger.info(\n { manifestPath: openClawRuntimeManifestPath, error: String(error) },\n 'OpenClaw runtime manifest unavailable',\n );\n return jsonResponse(res, 200, {\n success: true,\n schema: 1,\n updateAvailable: false,\n reason: 'manifest-unavailable',\n platform,\n currentVersion,\n appVersion,\n });\n }\n\n const latest = manifest.latest && typeof manifest.latest === 'object'\n ? manifest.latest\n : manifest;\n const platforms = latest.platforms && typeof latest.platforms === 'object'\n ? latest.platforms\n : {};\n const platformRelease = platform ? platforms[platform] : null;\n const version = readText(latest.version || manifest.version);\n\n if (!platform || !platformRelease || typeof platformRelease !== 'object') {\n return jsonResponse(res, 200, {\n success: true,\n schema: Number(manifest.schema || 1),\n channel: readText(manifest.channel || latest.channel || 'stable'),\n updateAvailable: false,\n reason: platform ? 'platform-unavailable' : 'platform-required',\n platform,\n currentVersion,\n appVersion,\n latestVersion: version,\n });\n }\n\n if (platform.startsWith('macos-') && !openClawRuntimeEnableMacosUpdates) {\n return jsonResponse(res, 200, {\n success: true,\n schema: Number(manifest.schema || 1),\n channel: readText(manifest.channel || latest.channel || 'stable'),\n updateAvailable: false,\n reason: 'macos-runtime-updates-disabled',\n platform,\n currentVersion,\n appVersion,\n latestVersion: version,\n });\n }\n\n const downloadUrl = absolutizeOpenClawRuntimeUrl(\n req,\n platformRelease.archiveUrl || platformRelease.url || platformRelease.fileName,\n );\n const release = {\n version,\n channel: readText(manifest.channel || latest.channel || 'stable'),\n minAppVersion: readText(latest.minAppVersion || platformRelease.minAppVersion),\n notes: readText(latest.notes || platformRelease.notes),\n nodeExecutable: readText(\n platformRelease.nodeExecutable || latest.nodeExecutable || 'node/bin/node',\n ),\n cliEntrypoint: readText(\n platformRelease.cliEntrypoint || latest.cliEntrypoint || 'openclaw/openclaw.mjs',\n ),\n defaultPort: Number(latest.defaultPort || manifest.defaultPort || 18789),\n defaultModel: readText(latest.defaultModel || manifest.defaultModel || 'openclaw/default'),\n defaultModelOverride: readText(latest.defaultModelOverride || manifest.defaultModelOverride),\n gatewayArgs: Array.isArray(latest.gatewayArgs)\n ? latest.gatewayArgs.map((item) => String(item))\n : ['gateway', '--port', '{port}', '--force'],\n archiveUrl: downloadUrl,\n sha256: readText(platformRelease.sha256),\n size: Number(platformRelease.size || 0),\n };\n\n jsonResponse(res, 200, {\n success: true,\n schema: Number(manifest.schema || 1),\n updateAvailable: Boolean(version && downloadUrl && version !== currentVersion),\n platform,\n currentVersion,\n appVersion,\n latest: release,\n generatedAt: readText(manifest.generatedAt || latest.generatedAt),\n });\n }),\n);\n\napp.get('/health', (_req, res) => {\n const codexRuntime = createCodexDeepSeekRuntime();\n const libreChatRuntime = createLibreChatAgentRuntime();\n const openClawRuntime = createOpenClawRuntime();\n jsonResponse(res, 200, {\n status: 'ok',\n service: 'dacheng-ai-backend',\n provider: libreChatRuntime.enabled ? libreChatRuntime.provider : 'deepseek',\n model: libreChatRuntime.enabled ? libreChatRuntime.model : deepseekModel,\n deepseekModel,\n deepseekAvailableModels,\n libreChatAgentChatEnabled: libreChatRuntime.enabled,\n libreChatAgentProvider: libreChatRuntime.provider,\n libreChatAgentReason: libreChatRuntime.enabled ? undefined : libreChatRuntime.reason,\n codexSdkAvailable: true,\n codexSdkChatEnabled: codexRuntime.enabled,\n codexSdkProvider: codexRuntime.provider,\n openClawAgentChatEnabled: openClawRuntime.enabled,\n openClawAgentProvider: openClawRuntime.provider,\n openClawAgentReason: openClawRuntime.enabled ? undefined : openClawRuntime.reason,\n requireMemberForAi,\n memberMonthlyLimit,\n freeMonthlyLimit,\n deepseekMaxCompletionTokens,\n botFatherMaxCompletionTokens,\n openClawRuntimeUpdatesDir,\n openClawRuntimeManifestConfigured: fs.existsSync(openClawRuntimeManifestPath),\n cbetaApiRoot,\n timestamp: nowIso(),\n });\n});\n\napp.get(\n '/v1/ai/usage',\n asyncHandler(async (req, res) => {\n const user = await resolveUser(req, {});\n const now = new Date();\n const windowStart = Math.floor(Date.UTC(\n now.getUTCFullYear(), now.getUTCMonth(), 1,\n ) / 1000);\n const windowEnd = Math.floor(Date.UTC(\n now.getUTCFullYear(), now.getUTCMonth() + 1, 1,\n ) / 1000);\n const tokenLimit = user.isTestAccount\n ? 999_999_999\n : user.isMember ? memberMonthlyLimit : freeMonthlyLimit;\n const usedTokens = user.isTestAccount ? 0 : usageFor(user.userId).used;\n jsonResponse(res, 200, {\n windowStart,\n windowEnd,\n tokenLimit,\n usedTokens,\n reservedTokens: 0,\n remainingTokens: Math.max(0, tokenLimit - usedTokens),\n });\n }),\n);\n\napp.get('/api/plugins/registry', (_req, res) => {\n jsonResponse(res, 200, {\n success: true,\n schemaVersion: 1,\n protocol: 'mcp',\n plugins: officialMcpApps,\n updatedAt: nowIso(),\n });\n});\n\napp.all('/api/mcp/apps/:pluginId', async (req, res) => {\n try {\n const user = await resolveUser(req, req.body || {});\n await handleOfficialMcpRequest(req.params.pluginId, req, res, user.userId);\n } catch (error) {\n logger.error({ error: error.stack || String(error) }, 'official MCP app request failed');\n if (!res.headersSent) {\n res.status(500).json({ error: 'MCP request failed', message: publicAiErrorMessage(error) });\n }\n }\n});\n\napp.post(\n '/api/codex/apps/:pluginId/turns',\n asyncHandler(async (req, res) => {\n const pluginId = safeUserText(req.params.pluginId).replace(/^official\\./, '');\n const plugin = officialMcpApps.find((candidate) => candidate.id === pluginId);\n if (!plugin) {\n return jsonResponse(res, 404, { success: false, message: 'plugin not found' });\n }\n\n const message = safeUserText(req.body?.message);\n if (!message) {\n return jsonResponse(res, 400, { success: false, message: 'message is required' });\n }\n\n const user = await resolveUser(req, req.body || {});\n const promptEstimate = estimateTokens(message);\n const budget = enforceTokenBudget(user, promptEstimate + 600);\n const maxCompletionTokens = completionBudgetFor(budget, promptEstimate);\n const namespace = pluginConversationNamespace(pluginId);\n const requestedConversationId = safeUserText(req.body?.conversationId);\n if (requestedConversationId && !requestedConversationId.startsWith(`${namespace}:`)) {\n return jsonResponse(res, 400, {\n success: false,\n message: 'conversation does not belong to the selected plugin namespace',\n });\n }\n\n const conversationId = requestedConversationId || `${namespace}:${crypto.randomUUID()}`;\n const createdAt = nowIso();\n let conversation = statements.getPluginConversation.get(\n conversationId,\n user.userId,\n namespace,\n );\n if (requestedConversationId && !conversation) {\n return jsonResponse(res, 404, {\n success: false,\n message: 'plugin conversation not found for this account',\n });\n }\n const isNewConversation = !conversation;\n if (isNewConversation) {\n conversation = {\n id: conversationId,\n user_id: user.userId,\n title: titleFromMessage(message),\n namespace,\n thread_id: null,\n };\n }\n\n const accountKey = sha256(user.userId).slice(0, 24);\n const pluginCodexHome = path.join(codexPluginSessionsDir, accountKey, pluginId);\n fs.mkdirSync(pluginCodexHome, { recursive: true });\n const mcpAccountToken = createCodexAdapterToken(user, {\n audience: 'mcp-plugin',\n recordUsage: false,\n });\n const mcpOrigin = (\n process.env.MCP_INTERNAL_BASE_URL || `http://127.0.0.1:${process.env.PORT || 8788}`\n ).replace(/\\/+$/, '');\n const isolationPolicy = createPluginCodexPolicy(pluginId, mcpOrigin);\n const firstTurnMessages = [\n {\n role: 'system',\n content:\n `你正在 ${plugin.title} 插件的隔离 Codex 会话中。` +\n '只能使用 current_plugin MCP Server 暴露的 Tools;禁止 shell、文件系统、Web 搜索和其他插件。' +\n '只读 Tool 可以按需调用。需要写入、外部操作或破坏性操作时,不要自行执行,明确告诉用户改用对应的 /Tool 命令,让宿主显示审批。',\n },\n { role: 'user', content: message },\n ];\n const aiResult = await callCodexSdkDeepSeek(\n conversation?.thread_id ? [{ role: 'user', content: message }] : firstTurnMessages,\n {\n user,\n threadId: conversation?.thread_id || undefined,\n maxCompletionTokens,\n sandboxMode: isolationPolicy.sandboxMode,\n approvalPolicy: isolationPolicy.approvalPolicy,\n networkAccessEnabled: isolationPolicy.networkAccessEnabled,\n webSearchMode: isolationPolicy.webSearchMode,\n codexHomeDir: pluginCodexHome,\n env: { [pluginMcpTokenEnv]: mcpAccountToken },\n config: isolationPolicy.config,\n },\n );\n const usage = {\n promptTokens: aiResult.usage.promptTokens || promptEstimate,\n completionTokens: aiResult.usage.completionTokens || estimateTokens(aiResult.message),\n totalTokens:\n aiResult.usage.totalTokens || promptEstimate + estimateTokens(aiResult.message),\n };\n if (!aiResult.usageAlreadyRecorded) recordUsage(user.userId, usage.totalTokens);\n\n const answeredAt = nowIso();\n if (isNewConversation) {\n statements.insertPluginConversation.run({\n id: conversationId,\n userId: user.userId,\n title: conversation.title,\n provider: codexDeepSeekProviderId,\n model: deepseekModel,\n namespace,\n createdAt,\n updatedAt: answeredAt,\n });\n }\n statements.insertMessage.run({\n id: crypto.randomUUID(),\n conversationId,\n role: 'user',\n content: message,\n promptTokens: 0,\n completionTokens: 0,\n totalTokens: 0,\n createdAt,\n });\n statements.insertMessage.run({\n id: crypto.randomUUID(),\n conversationId,\n role: 'assistant',\n content: aiResult.message,\n promptTokens: usage.promptTokens,\n completionTokens: usage.completionTokens,\n totalTokens: usage.totalTokens,\n createdAt: answeredAt,\n });\n statements.updatePluginConversation.run({\n id: conversationId,\n userId: user.userId,\n namespace,\n title: conversation.title,\n threadId: aiResult.threadId || conversation?.thread_id || null,\n updatedAt: answeredAt,\n });\n\n jsonResponse(res, 200, {\n success: true,\n pluginId,\n conversationId,\n message: aiResult.message,\n provider: codexDeepSeekProviderId,\n model: deepseekModel,\n usage: {\n ...usage,\n monthlyLimit: budget.limit,\n remainingTokens: Math.max(0, budget.limit - usageFor(user.userId).used),\n },\n });\n }),\n);\n\napp.get('/api/ai/models', (_req, res) => {\n jsonResponse(res, 200, {\n success: true,\n provider: 'deepseek',\n defaultModel: deepseekModel,\n models: deepseekAvailableModels.map((model) => ({\n id: model,\n label: model === 'deepseek-reasoner' ? 'DeepSeek Reasoner' : 'DeepSeek Chat',\n })),\n });\n});\napp.get(\n '/api/ai/quota',\n asyncHandler(async (req, res) => {\n const user = await resolveUser(req, { username: req.query.username });\n const budget = enforceTokenBudget(user, 0);\n const usage = usageFor(user.userId);\n jsonResponse(res, 200, {\n success: true,\n isMember: user.isMember,\n monthlyLimit: budget.limit,\n usedTokens: usage.used,\n remainingTokens: Math.max(0, budget.limit - usage.used),\n month: usage.month,\n });\n }),\n);\n\napp.post(\n '/api/agent/chat',\n asyncHandler(async (req, res) => {\n const message = safeUserText(req.body?.message);\n if (!message) {\n return jsonResponse(res, 400, { success: false, message: 'message is required' });\n }\n\n const user = await resolveUser(req, req.body || {});\n const promptEstimate = estimateTokens(message);\n const estimated = promptEstimate + 600;\n const budget = enforceTokenBudget(user, estimated);\n const maxCompletionTokens = completionBudgetFor(budget, promptEstimate);\n const createdAt = nowIso();\n const conversationId = safeUserText(req.body?.conversationId) || crypto.randomUUID();\n const mode = normalizeAgentMode(req.body?.mode);\n const stream = req.body?.stream === true;\n let conversation = statements.getConversation.get(conversationId, user.userId);\n const isNewConversation = !conversation;\n\n if (isNewConversation) {\n conversation = { id: conversationId, user_id: user.userId, title: titleFromMessage(message) };\n statements.insertConversation.run({\n id: conversationId,\n userId: user.userId,\n title: conversation.title,\n provider: 'agent',\n model: openClawAgentId,\n createdAt,\n updatedAt: createdAt,\n });\n }\n\n const messageId = safeUserText(req.body?.messageId) || crypto.randomUUID();\n statements.insertMessage.run({\n id: messageId,\n conversationId,\n role: 'user',\n content: message,\n promptTokens: 0,\n completionTokens: 0,\n totalTokens: 0,\n createdAt,\n });\n\n const runId = `run_${crypto.randomUUID().replaceAll('-', '')}`;\n const sessionKey = openClawSessionKey(user, conversationId);\n const openClawRuntime = createOpenClawRuntime();\n statements.insertAgentRun.run({\n id: runId,\n userId: user.userId,\n conversationId,\n messageId,\n openClawRunId: null,\n openClawSessionKey: sessionKey,\n status: stream ? 'queued' : 'running',\n mode,\n provider: openClawRuntime.enabled ? openClawRuntime.provider : 'dacheng-ai-fallback',\n model: openClawRuntime.enabled ? openClawAgentId : deepseekModel,\n startedAt: createdAt,\n });\n\n if (stream) {\n return jsonResponse(res, 200, {\n success: true,\n runId,\n conversationId,\n streamUrl: `/api/agent/runs/${runId}/events`,\n });\n }\n\n const controller = new AbortController();\n activeAgentRuns.set(runId, controller);\n try {\n const skillResult = await runResourceFinderSkill({ message, user });\n const skillContext = resourceContextMessage(skillResult);\n const historyRows = statements.listMessages.all(conversationId).slice(-14);\n const modelMessages = [\n { role: 'system', content: systemPrompt },\n ...(skillContext ? [{ role: 'system', content: skillContext }] : []),\n ...normalizeMessages(historyRows),\n ];\n\n const aiResult = await runAgentModel({\n messages: modelMessages,\n user,\n conversationId,\n mode,\n signal: controller.signal,\n maxCompletionTokens,\n });\n const usage = {\n promptTokens: aiResult.usage?.promptTokens || estimateTokens(JSON.stringify(modelMessages)),\n completionTokens: aiResult.usage?.completionTokens || estimateTokens(aiResult.message),\n totalTokens:\n aiResult.usage?.totalTokens ||\n estimateTokens(JSON.stringify(modelMessages)) + estimateTokens(aiResult.message),\n };\n if (!aiResult.usageAlreadyRecorded) recordUsage(user.userId, usage.totalTokens);\n const answeredAt = nowIso();\n const assistantMessageId = crypto.randomUUID();\n statements.insertMessage.run({\n id: assistantMessageId,\n conversationId,\n role: 'assistant',\n content: aiResult.message,\n promptTokens: usage.promptTokens,\n completionTokens: usage.completionTokens,\n totalTokens: usage.totalTokens,\n createdAt: answeredAt,\n });\n statements.updateConversation.run({\n id: conversationId,\n userId: user.userId,\n title: isNewConversation ? titleFromMessage(message) : conversation.title,\n updatedAt: answeredAt,\n });\n updateAgentRunStatus({ id: runId, user_id: user.userId }, 'completed', {\n openClawRunId: aiResult.runId,\n inputTokens: usage.promptTokens,\n outputTokens: usage.completionTokens,\n });\n const action = determineClientAction(skillResult, message);\n return jsonResponse(res, 200, {\n success: true,\n runId,\n conversationId,\n messageId: assistantMessageId,\n provider: aiResult.provider,\n model: aiResult.model,\n message: aiResult.message,\n usage: agentUsagePayload(usage, user, budget),\n ...(action ? { clientAction: action } : {}),\n });\n } catch (error) {\n const status = error.name === 'AbortError' ? 'cancelled' : 'failed';\n updateAgentRunStatus({ id: runId, user_id: user.userId }, status, {\n errorCode: error.name || 'AGENT_RUN_FAILED',\n errorMessage: publicAiErrorMessage(error),\n });\n if (status === 'cancelled') {\n return jsonResponse(res, 499, { success: false, runId, status: 'cancelled' });\n }\n throw error;\n } finally {\n activeAgentRuns.delete(runId);\n }\n }),\n);\n\napp.get(\n '/api/agent/runs/:runId/events',\n asyncHandler(async (req, res) => {\n const user = await resolveUser(req, {});\n const run = statements.getAgentRun.get(req.params.runId, user.userId);\n if (!run) {\n return jsonResponse(res, 404, { success: false, message: 'run not found' });\n }\n if (run.status === 'completed' || run.status === 'failed' || run.status === 'cancelled') {\n return jsonResponse(res, 409, { success: false, message: `run is already ${run.status}` });\n }\n\n sseHeaders(res);\n const controller = new AbortController();\n activeAgentRuns.set(run.id, controller);\n updateAgentRunStatus(run, 'running');\n writeSse(res, 'run.started', { runId: run.id, conversationId: run.conversation_id });\n\n try {\n let streamedText = '';\n const modelMessages = buildAgentMessages(run.conversation_id);\n const lastUserMsg = [...modelMessages].reverse().find(m => m.role === 'user');\n const userMsgText = lastUserMsg ? lastUserMsg.content : '';\n const skillResult = userMsgText ? await runResourceFinderSkill({\n message: userMsgText,\n user,\n onStep: (step) => writeSse(res, 'tool.call.completed', {\n tool: 'agent.step',\n displayName: step.title || 'Agent step',\n summary: step.message || '',\n }),\n }) : null;\n const skillContext = resourceContextMessage(skillResult);\n const finalMessages = skillContext ? [\n modelMessages[0],\n { role: 'system', content: skillContext },\n ...modelMessages.slice(1)\n ] : modelMessages;\n\n const promptEstimate = estimateTokens(JSON.stringify(finalMessages));\n const estimated = promptEstimate + 600;\n const budget = enforceTokenBudget(user, estimated);\n const maxCompletionTokens = completionBudgetFor(budget, promptEstimate);\n const aiResult = await runAgentModel({\n messages: finalMessages,\n user,\n conversationId: run.conversation_id,\n mode: run.mode,\n signal: controller.signal,\n maxCompletionTokens,\n callbacks: {\n onStep: (step) => writeSse(res, 'tool.call.completed', {\n tool: 'agent.step',\n displayName: step.title || 'Agent step',\n summary: step.message || '',\n }),\n onToken: (delta) => {\n streamedText += delta;\n writeSse(res, 'assistant.delta', { text: delta });\n },\n },\n });\n const message = aiResult.message || streamedText.trim();\n const usage = {\n promptTokens: aiResult.usage?.promptTokens || estimateTokens(JSON.stringify(finalMessages)),\n completionTokens: aiResult.usage?.completionTokens || estimateTokens(message),\n totalTokens:\n aiResult.usage?.totalTokens || estimateTokens(JSON.stringify(finalMessages)) + estimateTokens(message),\n };\n if (!aiResult.usageAlreadyRecorded) recordUsage(user.userId, usage.totalTokens);\n const assistantMessageId = crypto.randomUUID();\n const answeredAt = nowIso();\n statements.insertMessage.run({\n id: assistantMessageId,\n conversationId: run.conversation_id,\n role: 'assistant',\n content: message,\n promptTokens: usage.promptTokens,\n completionTokens: usage.completionTokens,\n totalTokens: usage.totalTokens,\n createdAt: answeredAt,\n });\n statements.updateConversation.run({\n id: run.conversation_id,\n userId: user.userId,\n title: titleFromMessage(message),\n updatedAt: answeredAt,\n });\n updateAgentRunStatus(run, 'completed', {\n openClawRunId: aiResult.runId,\n inputTokens: usage.promptTokens,\n outputTokens: usage.completionTokens,\n });\n const action = determineClientAction(skillResult, userMsgText);\n writeSse(res, 'assistant.message', { messageId: assistantMessageId, content: message });\n writeSse(res, 'run.completed', {\n usage: agentUsagePayload(usage, user, budget),\n ...(action ? { clientAction: action } : {}),\n });\n res.end();\n } catch (error) {\n const status = error.name === 'AbortError' ? 'cancelled' : 'failed';\n updateAgentRunStatus(run, status, {\n errorCode: error.name || 'AGENT_RUN_FAILED',\n errorMessage: publicAiErrorMessage(error),\n });\n writeSse(res, status === 'cancelled' ? 'run.cancelled' : 'run.failed', {\n runId: run.id,\n message: publicAiErrorMessage(error),\n });\n res.end();\n } finally {\n activeAgentRuns.delete(run.id);\n }\n }),\n);\n\napp.post(\n '/api/agent/runs/:runId/cancel',\n asyncHandler(async (req, res) => {\n const user = await resolveUser(req, req.body || {});\n const run = statements.getAgentRun.get(req.params.runId, user.userId);\n if (!run) {\n return jsonResponse(res, 404, { success: false, message: 'run not found' });\n }\n const controller = activeAgentRuns.get(run.id);\n if (controller) controller.abort();\n updateAgentRunStatus(run, 'cancelled');\n return jsonResponse(res, 200, { success: true, runId: run.id, status: 'cancelled' });\n }),\n);\n\napp.post(\n '/api/agent/messages/:messageId/feedback',\n asyncHandler(async (req, res) => {\n const user = await resolveUser(req, req.body || {});\n const rating = safeUserText(req.body?.rating);\n if (!['up', 'down'].includes(rating)) {\n return jsonResponse(res, 400, { success: false, message: 'rating must be up or down' });\n }\n statements.insertAgentFeedback.run({\n id: crypto.randomUUID(),\n messageId: req.params.messageId,\n userId: user.userId,\n rating,\n reason: safeUserText(req.body?.reason),\n comment: safeUserText(req.body?.comment),\n createdAt: nowIso(),\n });\n return jsonResponse(res, 200, { success: true });\n }),\n);\n\napp.post(\n '/api/ai/chat',\n asyncHandler(async (req, res) => {\n const message = safeUserText(req.body?.message);\n if (!message) {\n return jsonResponse(res, 400, { success: false, message: 'message is required' });\n }\n\n const user = await resolveUser(req, req.body || {});\n const promptEstimate = estimateTokens(message);\n const estimated = promptEstimate + 600;\n const budget = enforceTokenBudget(user, estimated);\n const maxCompletionTokens = completionBudgetFor(budget, promptEstimate);\n const createdAt = nowIso();\n const conversationId = safeUserText(req.body?.conversationId) || crypto.randomUUID();\n const requestedModel = normalizeDeepSeekModelName(req.body?.model);\n const clientContext = clientContextMessage(req.body?.client);\n let conversation = statements.getConversation.get(conversationId, user.userId);\n const isNewConversation = !conversation;\n\n if (isNewConversation) {\n conversation = {\n id: conversationId,\n user_id: user.userId,\n title: titleFromMessage(message),\n };\n statements.insertConversation.run({\n id: conversationId,\n userId: user.userId,\n title: conversation.title,\n provider: 'deepseek',\n model: requestedModel,\n createdAt,\n updatedAt: createdAt,\n });\n }\n\n statements.insertMessage.run({\n id: crypto.randomUUID(),\n conversationId,\n role: 'user',\n content: message,\n promptTokens: 0,\n completionTokens: 0,\n totalTokens: 0,\n createdAt,\n });\n\n const libreChatRuntime = createLibreChatAgentRuntime();\n const skillResult = await runResourceFinderSkill({ message, user });\n const skillContext = resourceContextMessage(skillResult);\n const historyRows = statements.listMessages.all(conversationId).slice(-14);\n const modelMessages = [\n { role: 'system', content: systemPrompt },\n ...(clientContext ? [{ role: 'system', content: clientContext }] : []),\n ...(skillContext ? [{ role: 'system', content: skillContext }] : []),\n ...normalizeMessages(historyRows),\n ];\n\n const openClawRuntime = createOpenClawRuntime();\n const codexRuntime = createCodexDeepSeekRuntime(user);\n let provider = openClawRuntime.enabled\n ? openClawRuntime.provider\n : libreChatRuntime.enabled\n ? libreChatRuntime.provider\n : 'deepseek';\n let responseModel = openClawRuntime.enabled\n ? openClawRuntime.model\n : libreChatRuntime.enabled\n ? libreChatRuntime.model\n : requestedModel;\n let aiResult;\n try {\n if (openClawRuntime.enabled) {\n aiResult = await callOpenClawAgent({\n messages: modelMessages,\n user,\n conversationId,\n mode: normalizeAgentMode(req.body?.mode),\n });\n provider = aiResult.provider || provider;\n responseModel = aiResult.model || responseModel;\n } else if (libreChatRuntime.enabled) {\n aiResult = await callLibreChatAgent(modelMessages);\n } else if (codexRuntime.enabled) {\n provider = codexRuntime.provider;\n aiResult = await callCodexSdkDeepSeek(modelMessages, { user, maxCompletionTokens });\n } else {\n aiResult = await callDeepSeek(modelMessages, { model: requestedModel, maxCompletionTokens });\n }\n } catch (error) {\n if (libreChatRuntime.enabled) {\n logger.warn({ error: String(error) }, 'LibreChat Agent chat failed, falling back to Codex SDK or DeepSeek direct');\n provider = codexRuntime.enabled ? codexRuntime.provider : 'deepseek';\n responseModel = requestedModel;\n if (codexRuntime.enabled) {\n try {\n aiResult = await callCodexSdkDeepSeek(modelMessages, { user, maxCompletionTokens });\n } catch (codexError) {\n if (isDeepSeekQuotaError(codexError)) throw codexError;\n logger.warn({ error: String(codexError) }, 'Codex SDK chat failed, falling back to DeepSeek direct');\n provider = 'deepseek';\n aiResult = await callDeepSeek(modelMessages, { model: requestedModel, maxCompletionTokens });\n }\n } else {\n aiResult = await callDeepSeek(modelMessages, { model: requestedModel, maxCompletionTokens });\n }\n } else {\n if (!codexRuntime.enabled || isDeepSeekQuotaError(error)) throw error;\n logger.warn({ error: String(error) }, 'Codex SDK chat failed, falling back to DeepSeek direct');\n provider = 'deepseek';\n responseModel = requestedModel;\n aiResult = await callDeepSeek(modelMessages, { model: requestedModel, maxCompletionTokens });\n }\n }\n const usage = {\n promptTokens: aiResult.usage.promptTokens || estimateTokens(JSON.stringify(modelMessages)),\n completionTokens: aiResult.usage.completionTokens || estimateTokens(aiResult.message),\n totalTokens:\n aiResult.usage.totalTokens ||\n estimateTokens(JSON.stringify(modelMessages)) + estimateTokens(aiResult.message),\n };\n if (!aiResult.usageAlreadyRecorded) recordUsage(user.userId, usage.totalTokens);\n\n const answeredAt = nowIso();\n statements.insertMessage.run({\n id: crypto.randomUUID(),\n conversationId,\n role: 'assistant',\n content: aiResult.message,\n promptTokens: usage.promptTokens,\n completionTokens: usage.completionTokens,\n totalTokens: usage.totalTokens,\n createdAt: answeredAt,\n });\n statements.updateConversation.run({\n id: conversationId,\n userId: user.userId,\n title: isNewConversation ? titleFromMessage(message) : conversation.title,\n updatedAt: answeredAt,\n });\n\n const latestUsage = usageFor(user.userId);\n const monthlyLimit = budget.limit;\n const action = determineClientAction(skillResult, message);\n jsonResponse(res, 200, {\n success: true,\n conversationId,\n provider,\n model: responseModel,\n message: aiResult.message,\n usage: {\n ...usage,\n monthlyLimit,\n remainingTokens: Math.max(0, monthlyLimit - latestUsage.used),\n },\n ...(action ? { clientAction: action } : {}),\n });\n }),\n);\n\napp.post('/api/ai/chat/stream', async (req, res) => {\n sseHeaders(res);\n\n try {\n const message = safeUserText(req.body?.message);\n if (!message) {\n writeSse(res, 'error', { message: 'message is required' });\n res.end();\n return;\n }\n\n const user = await resolveUser(req, req.body || {});\n const promptEstimate = estimateTokens(message);\n const estimated = promptEstimate + 600;\n const budget = enforceTokenBudget(user, estimated);\n const maxCompletionTokens = completionBudgetFor(budget, promptEstimate);\n const createdAt = nowIso();\n const conversationId = safeUserText(req.body?.conversationId) || crypto.randomUUID();\n const requestedModel = normalizeDeepSeekModelName(req.body?.model);\n const clientContext = clientContextMessage(req.body?.client);\n let conversation = statements.getConversation.get(conversationId, user.userId);\n const isNewConversation = !conversation;\n\n if (isNewConversation) {\n conversation = {\n id: conversationId,\n user_id: user.userId,\n title: titleFromMessage(message),\n };\n statements.insertConversation.run({\n id: conversationId,\n userId: user.userId,\n title: conversation.title,\n provider: 'deepseek',\n model: requestedModel,\n createdAt,\n updatedAt: createdAt,\n });\n }\n\n statements.insertMessage.run({\n id: crypto.randomUUID(),\n conversationId,\n role: 'user',\n content: message,\n promptTokens: 0,\n completionTokens: 0,\n totalTokens: 0,\n createdAt,\n });\n\n const codexRuntime = createCodexDeepSeekRuntime(user);\n const libreChatRuntime = createLibreChatAgentRuntime();\n const initialOpenClawRuntime = createOpenClawRuntime();\n let provider = initialOpenClawRuntime.enabled\n ? initialOpenClawRuntime.provider\n : libreChatRuntime.enabled\n ? libreChatRuntime.provider\n : codexRuntime.enabled\n ? codexRuntime.provider\n : 'deepseek';\n let responseModel = initialOpenClawRuntime.enabled\n ? initialOpenClawRuntime.model\n : libreChatRuntime.enabled\n ? libreChatRuntime.model\n : requestedModel;\n writeSse(res, 'meta', {\n conversationId,\n provider,\n model: responseModel,\n });\n const skillResult = await runResourceFinderSkill({\n message,\n user,\n onStep: (step) => writeSse(res, 'step', step),\n });\n const skillContext = resourceContextMessage(skillResult);\n const historyRows = statements.listMessages.all(conversationId).slice(-14);\n const modelMessages = [\n { role: 'system', content: systemPrompt },\n ...(clientContext ? [{ role: 'system', content: clientContext }] : []),\n ...(skillContext ? [{ role: 'system', content: skillContext }] : []),\n ...normalizeMessages(historyRows),\n ];\n\n let aiResult;\n const openClawRuntime = initialOpenClawRuntime;\n try {\n if (openClawRuntime.enabled) {\n aiResult = await callOpenClawAgent({\n messages: modelMessages,\n user,\n conversationId,\n mode: normalizeAgentMode(req.body?.mode),\n });\n provider = aiResult.provider || provider;\n responseModel = aiResult.model || responseModel;\n writeSse(res, 'delta', { text: aiResult.message });\n } else if (libreChatRuntime.enabled) {\n aiResult = await callLibreChatAgentStream(modelMessages, {\n onToken: (token) => writeSse(res, 'delta', { text: token }),\n });\n } else if (codexRuntime.enabled) {\n aiResult = await callCodexSdkDeepSeek(modelMessages, {\n user,\n maxCompletionTokens,\n onToken: (token) => writeSse(res, 'delta', { text: token }),\n onStep: (step) => writeSse(res, 'step', step),\n });\n } else {\n aiResult = await callDeepSeekStream(modelMessages, {\n model: requestedModel,\n maxCompletionTokens,\n onToken: (token) => writeSse(res, 'delta', { text: token }),\n });\n }\n } catch (error) {\n if (libreChatRuntime.enabled) {\n logger.warn({ error: String(error) }, 'LibreChat Agent streaming chat failed, falling back to Codex SDK or DeepSeek direct');\n provider = codexRuntime.enabled ? codexRuntime.provider : 'deepseek';\n responseModel = requestedModel;\n writeSse(res, 'meta', {\n conversationId,\n provider,\n model: responseModel,\n });\n if (codexRuntime.enabled) {\n try {\n aiResult = await callCodexSdkDeepSeek(modelMessages, {\n user,\n maxCompletionTokens,\n onToken: (token) => writeSse(res, 'delta', { text: token }),\n onStep: (step) => writeSse(res, 'step', step),\n });\n } catch (codexError) {\n if (isDeepSeekQuotaError(codexError)) throw codexError;\n logger.warn({ error: String(codexError) }, 'Codex SDK streaming chat failed, falling back to DeepSeek direct');\n provider = 'deepseek';\n responseModel = requestedModel;\n writeSse(res, 'meta', {\n conversationId,\n provider,\n model: responseModel,\n });\n aiResult = await callDeepSeekStream(modelMessages, {\n model: requestedModel,\n maxCompletionTokens,\n onToken: (token) => writeSse(res, 'delta', { text: token }),\n });\n }\n } else {\n aiResult = await callDeepSeekStream(modelMessages, {\n model: requestedModel,\n maxCompletionTokens,\n onToken: (token) => writeSse(res, 'delta', { text: token }),\n });\n }\n } else {\n if (!codexRuntime.enabled || isDeepSeekQuotaError(error)) throw error;\n logger.warn({ error: String(error) }, 'Codex SDK streaming chat failed, falling back to DeepSeek direct');\n provider = 'deepseek';\n responseModel = requestedModel;\n writeSse(res, 'meta', {\n conversationId,\n provider,\n model: responseModel,\n });\n aiResult = await callDeepSeekStream(modelMessages, {\n model: requestedModel,\n maxCompletionTokens,\n onToken: (token) => writeSse(res, 'delta', { text: token }),\n });\n }\n }\n\n const usage = {\n promptTokens: aiResult.usage.promptTokens || estimateTokens(JSON.stringify(modelMessages)),\n completionTokens: aiResult.usage.completionTokens || estimateTokens(aiResult.message),\n totalTokens:\n aiResult.usage.totalTokens ||\n estimateTokens(JSON.stringify(modelMessages)) + estimateTokens(aiResult.message),\n };\n if (!aiResult.usageAlreadyRecorded) recordUsage(user.userId, usage.totalTokens);\n\n const answeredAt = nowIso();\n statements.insertMessage.run({\n id: crypto.randomUUID(),\n conversationId,\n role: 'assistant',\n content: aiResult.message,\n promptTokens: usage.promptTokens,\n completionTokens: usage.completionTokens,\n totalTokens: usage.totalTokens,\n createdAt: answeredAt,\n });\n statements.updateConversation.run({\n id: conversationId,\n userId: user.userId,\n title: isNewConversation ? titleFromMessage(message) : conversation.title,\n updatedAt: answeredAt,\n });\n\n const latestUsage = usageFor(user.userId);\n const monthlyLimit = budget.limit;\n const action = determineClientAction(skillResult, message);\n writeSse(res, 'done', {\n conversationId,\n provider,\n model: responseModel,\n message: aiResult.message,\n usage: {\n ...usage,\n monthlyLimit,\n remainingTokens: Math.max(0, monthlyLimit - latestUsage.used),\n },\n ...(action ? { clientAction: action } : {}),\n });\n res.end();\n } catch (error) {\n logger.error({ error: error.stack || String(error) }, 'streaming chat failed');\n writeSse(res, 'error', {\n message: publicAiErrorMessage(error),\n details: error.details,\n });\n res.end();\n }\n});\n\napp.get(\n '/api/ai/conversations',\n asyncHandler(async (req, res) => {\n const user = await resolveUser(req, { username: req.query.username });\n const items = statements.listConversations.all(user.userId).map((item) => ({\n id: item.id,\n title: item.title,\n updatedAt: item.updatedAt,\n }));\n jsonResponse(res, 200, { success: true, items });\n }),\n);\n\napp.get(\n '/api/ai/conversations/:id',\n asyncHandler(async (req, res) => {\n const user = await resolveUser(req);\n const conversation = statements.getConversation.get(req.params.id, user.userId);\n if (!conversation) {\n return jsonResponse(res, 404, { success: false, message: 'conversation not found' });\n }\n const messages = statements.listMessages.all(req.params.id).map((item) => ({\n role: item.role,\n content: item.content,\n createdAt: item.createdAt,\n }));\n jsonResponse(res, 200, {\n success: true,\n conversation: {\n id: conversation.id,\n title: conversation.title,\n updatedAt: conversation.updated_at,\n },\n messages,\n });\n }),\n);\n\nfunction buildUrl(base, endpoint, params = {}) {\n const url = new URL(endpoint, `${base}/`);\n for (const [key, value] of Object.entries(params)) {\n if (Array.isArray(value)) {\n for (const item of value) {\n if (item !== undefined && item !== null && item !== '') {\n url.searchParams.append(key, String(item));\n }\n }\n } else if (value !== undefined && value !== null && value !== '') {\n url.searchParams.set(key, String(value));\n }\n }\n return url;\n}\n\nasync function fetchJson(url) {\n const response = await fetch(url, {\n headers: { Accept: 'application/json' },\n signal: AbortSignal.timeout(15_000),\n });\n if (!response.ok) {\n throw new Error(`Request failed: ${response.status}`);\n }\n return response.json();\n}\n\nfunction stripHtml(value) {\n return String(value || '')\n .replace(/<script[\\s\\S]*?<\\/script>/gi, ' ')\n .replace(/<style[\\s\\S]*?<\\/style>/gi, ' ')\n .replace(/<[^>]+>/g, ' ')\n .replace(/ /g, ' ')\n .replace(/</g, '<')\n .replace(/>/g, '>')\n .replace(/&/g, '&')\n .replace(/&#(\\d+);/g, (_match, code) => String.fromCharCode(Number(code)))\n .replace(/\\s+/g, ' ')\n .trim();\n}\n\nfunction cleanExtractedText(value) {\n let text = String(value || '')\n .replace(/\\b[A-Z]{1,3}\\d{2}n\\d{4,5}[a-z]?_p?\\d{4}[a-z]\\d{2}\\b/g, ' ')\n .replace(/\\uFFFD/g, '')\n .replace(/\\s+([,。;:!?、」』)》】])/g, '$1')\n .replace(/([「『《(【])\\s+/g, '$1');\n for (let i = 0; i < 3; i += 1) {\n text = text.replace(/([\\u3400-\\u9fff])\\s+([\\u3400-\\u9fff])/g, '$1$2');\n }\n return text.replace(/[ \\t]{2,}/g, ' ').trim();\n}\n\nfunction compactText(value, limit = 140) {\n const text = stripHtml(value);\n return text.length > limit ? `${text.slice(0, limit)}...` : text;\n}\n\nfunction collectSearchResults(data) {\n if (!data || typeof data !== 'object') return [];\n if (Array.isArray(data.results)) return data.results;\n if (Array.isArray(data?.results?.docs)) return data.results.docs;\n if (Array.isArray(data?.response?.docs)) return data.response.docs;\n if (Array.isArray(data.docs)) return data.docs;\n return [];\n}\n\nfunction firstText(item, keys) {\n for (const key of keys) {\n const value = item?.[key];\n if (Array.isArray(value)) {\n const first = value.map((entry) => safeUserText(String(entry))).find(Boolean);\n if (first) return first;\n }\n if (typeof value === 'string' && value.trim()) return value.trim();\n if (typeof value === 'number') return String(value);\n }\n return '';\n}\n\nfunction firstNumber(item, keys, fallback = undefined) {\n for (const key of keys) {\n const value = item?.[key];\n if (typeof value === 'number') return value;\n if (typeof value === 'string' && value.trim() && Number.isFinite(Number(value))) {\n return Number(value);\n }\n }\n return fallback;\n}\n\nasync function searchCbetaTitle(query, limit) {\n const url = buildUrl(cbetaApiRoot, 'search/title', { q: query, start: 0, rows: limit });\n const data = await fetchJson(url);\n return collectSearchResults(data).flatMap((item) => {\n const work = firstText(item, ['work', 'work_id', 'workId']);\n const title = firstText(item, ['content', 'title', 'work_title', 'workTitle']);\n if (!work || !title) return [];\n const juan = firstNumber(item, ['juan', 'juan_num', 'juanNum'], 1);\n return [\n {\n id: `cbeta-title-${work}-${juan}`,\n title,\n sourceName: 'CBETA 佛典',\n url: `cbeta:${work}:${juan}`,\n snippet: firstText(item, ['byline', 'creators', 'time_dynasty']) || '可下载经文正文并用于全球法布施。',\n resourceType: 'scripture',\n work,\n juan,\n },\n ];\n });\n}\n\nasync function searchCbetaContent(query, limit) {\n const endpoints = ['search/fulltext', 'search/content', 'search/all_in_one'];\n const settled = await Promise.allSettled(\n endpoints.map((endpoint) =>\n fetchJson(buildUrl(cbetaApiRoot, endpoint, { q: query, start: 0, rows: limit, field: 'content' })),\n ),\n );\n const results = [];\n for (const response of settled) {\n if (response.status !== 'fulfilled') continue;\n for (const item of collectSearchResults(response.value)) {\n const work = firstText(item, ['work', 'work_id', 'workId', 'sutra', 'id']);\n const snippet = compactText(firstText(item, ['kwic', 'highlight', 'snippet', 'content', 'text', 'body', 'p']));\n if (!work || !snippet) continue;\n const juan = firstNumber(item, ['juan', 'juan_num', 'juanNum', '卷'], 1);\n const title = firstText(item, ['title', 'work_title', 'workTitle', 'sutra_name', 'book', 'name']) || work;\n results.push({\n id: `cbeta-content-${work}-${juan}-${sha256(snippet).slice(0, 8)}`,\n title,\n sourceName: 'CBETA 佛典',\n url: `cbeta:${work}:${juan}`,\n snippet,\n resourceType: 'scripture',\n work,\n juan,\n });\n if (results.length >= limit) return results;\n }\n }\n return results;\n}\n\nfunction directUrlResource(query) {\n const url = safeUserText(query);\n let parsed;\n try {\n parsed = new URL(url);\n } catch {\n return null;\n }\n if (!['http:', 'https:'].includes(parsed.protocol)) return null;\n return {\n id: `url-${sha256(url).slice(0, 12)}`,\n title: parsed.hostname,\n sourceName: '网页资源',\n url,\n snippet: '直接下载并提取这个链接的可读正文。',\n resourceType: 'web',\n };\n}\n\nfunction looksLikeAudioQuery(query) {\n return /音乐|音樂|音频|音訊|mp3|audio|念诵|念誦|诵经|誦經|唱诵|唱誦|读诵|讀誦/.test(\n safeUserText(query).toLowerCase(),\n );\n}\n\nfunction archiveSearchTerm(query) {\n return (\n safeUserText(query)\n .replace(/下载|查找|寻找|找一?个|资源|音频|音訊|音乐|音樂|mp3|audio|念诵|念誦|诵经|誦經|唱诵|唱誦|读诵|讀誦/gi, ' ')\n .replace(/\\s+/g, ' ')\n .trim() || safeUserText(query)\n );\n}\n\nfunction solrPhrase(value) {\n return String(value).replace(/\\\\/g, '\\\\\\\\').replace(/\"/g, '\\\\\"');\n}\n\nasync function searchInternetArchiveAudio(query, limit) {\n if (!looksLikeAudioQuery(query)) return [];\n const term = archiveSearchTerm(query);\n if (!term) return [];\n const variants = Array.from(\n new Set([\n term,\n term.replaceAll('金刚经', '金剛經'),\n term.replaceAll('金剛經', '金刚经'),\n ].filter(Boolean)),\n );\n const phraseQuery = variants\n .flatMap((item) => [`title:\"${solrPhrase(item)}\"`, `description:\"${solrPhrase(item)}\"`])\n .join(' OR ');\n const url = buildUrl('https://archive.org', 'advancedsearch.php', {\n q: `(${phraseQuery}) AND mediatype:audio`,\n rows: Math.min(Math.max(limit, 1), 10),\n output: 'json',\n 'fl[]': ['identifier', 'title', 'description'],\n });\n const data = await fetchJson(url);\n const docs = Array.isArray(data?.response?.docs) ? data.response.docs : [];\n return docs\n .map((item) => {\n const identifier = firstText(item, ['identifier']);\n const title = firstText(item, ['title']) || identifier;\n if (!identifier || !title) return null;\n return {\n id: `ia-audio-${identifier}`,\n title,\n sourceName: 'Internet Archive 音频',\n url: `ia:${identifier}`,\n snippet: compactText(firstText(item, ['description'])) || '公开可访问的音频资源,将下载第一个可用 MP3 文件。',\n resourceType: 'audio',\n identifier,\n };\n })\n .filter(Boolean);\n}\n\nconst curatedCbetaWorks = [\n {\n work: 'T0235',\n juan: 1,\n title: '金剛般若波羅蜜經',\n keywords: ['金剛般若波羅蜜經', '金刚般若波罗蜜经', '金剛經', '金刚经'],\n snippet: '姚秦 鳩摩羅什譯。常用于读诵、共修与法布施的般若经典。',\n },\n {\n work: 'T0251',\n juan: 1,\n title: '般若波羅蜜多心經',\n keywords: ['般若波羅蜜多心經', '般若波罗蜜多心经', '心經', '心经'],\n snippet: '唐 玄奘譯。篇幅短,适合加入禅室功课本与日常持诵。',\n },\n {\n work: 'T0262',\n juan: 1,\n title: '妙法蓮華經',\n keywords: ['妙法蓮華經', '妙法莲华经', '法華經', '法华经'],\n snippet: '姚秦 鳩摩羅什譯。可按卷下载正文,用于长期功课与分享。',\n },\n {\n work: 'T0279',\n juan: 1,\n title: '大方廣佛華嚴經',\n keywords: ['大方廣佛華嚴經', '大方广佛华严经', '華嚴經', '华严经'],\n snippet: '唐 實叉難陀譯。可按卷下载正文,用于系统研读与法布施。',\n },\n {\n work: 'T0360',\n juan: 1,\n title: '佛說無量壽經',\n keywords: ['佛說無量壽經', '佛说无量寿经', '無量壽經', '无量寿经'],\n snippet: '净土经典。可下载正文,用于净土功课本与全球法布施。',\n },\n {\n work: 'T0365',\n juan: 1,\n title: '佛說觀無量壽佛經',\n keywords: ['佛說觀無量壽佛經', '佛说观无量寿佛经', '觀無量壽經', '观无量寿经'],\n snippet: '净土三经之一。适合查找下载后加入净土功课。',\n },\n {\n work: 'T0366',\n juan: 1,\n title: '佛說阿彌陀經',\n keywords: ['佛說阿彌陀經', '佛说阿弥陀经', '阿彌陀經', '阿弥陀经'],\n snippet: '姚秦 鳩摩羅什譯。篇幅适中,适合共修、持诵与分享。',\n },\n {\n work: 'T0475',\n juan: 1,\n title: '維摩詰所說經',\n keywords: ['維摩詰所說經', '维摩诘所说经', '維摩詰經', '维摩诘经'],\n snippet: '姚秦 鳩摩羅什譯。大乘经典,可按卷下载用于研读。',\n },\n {\n work: 'T0676',\n juan: 1,\n title: '解深密經',\n keywords: ['解深密經', '解深密经'],\n snippet: '唯识相关大乘经典。适合研读、整理笔记与法布施。',\n },\n {\n work: 'T1666',\n juan: 1,\n title: '大乘起信論',\n keywords: ['大乘起信論', '大乘起信论', '起信論', '起信论'],\n snippet: '大乘论典。适合下载后作为研习功课资料。',\n },\n];\n\nfunction localCbetaResults(query, limit) {\n const normalizedQuery = safeUserText(query).toLowerCase();\n if (!normalizedQuery) return [];\n return curatedCbetaWorks\n .filter((item) =>\n item.keywords.some((keyword) => {\n const normalizedKeyword = keyword.toLowerCase();\n return normalizedKeyword.includes(normalizedQuery) || normalizedQuery.includes(normalizedKeyword);\n }),\n )\n .slice(0, limit)\n .map((item) => ({\n id: `cbeta-curated-${item.work}-${item.juan}`,\n title: item.title,\n sourceName: 'CBETA 佛典',\n url: `cbeta:${item.work}:${item.juan}`,\n snippet: item.snippet,\n resourceType: 'scripture',\n work: item.work,\n juan: item.juan,\n }));\n}\n\nfunction uniqueResources(items, limit) {\n const seen = new Set();\n const results = [];\n for (const item of items) {\n const key = `${item.url}:${item.title}`;\n if (seen.has(key)) continue;\n seen.add(key);\n results.push(item);\n if (results.length >= limit) break;\n }\n return results;\n}\n\nasync function findResourceCandidates(query, limit) {\n const direct = directUrlResource(query);\n const results = [];\n if (direct) results.push(direct);\n try {\n results.push(...(await searchInternetArchiveAudio(query, limit)));\n } catch (error) {\n logger.warn({ error: String(error), query }, 'Internet Archive audio search skipped');\n }\n results.push(...localCbetaResults(query, limit));\n\n const [titleResults, contentResults] = await Promise.allSettled([\n searchCbetaTitle(query, limit),\n searchCbetaContent(query, limit),\n ]);\n if (titleResults.status === 'fulfilled') results.push(...titleResults.value);\n if (contentResults.status === 'fulfilled') results.push(...contentResults.value);\n\n return uniqueResources(results, limit);\n}\n\nfunction looksLikeResourceTask(message) {\n const text = safeUserText(message).toLowerCase();\n if (!text) return false;\n return /下载|查找|寻找|找一?个|资源|經文|经文|佛经|佛經|仪轨|儀軌|音乐|音樂|mp3|audio|念诵|念誦|诵经|誦經/.test(text);\n}\n\nfunction extractResourceQuery(message) {\n const text = safeUserText(message)\n .replace(/请|請|帮我|幫我|麻烦|麻煩|可以|能不能|需要|我要|想要/g, ' ')\n .replace(/下载|下載|查找|寻找|尋找|找一个|找一個|找|资源|資源|并|並|然后|然後|用于|用於|全球法布施|加入功课本|加入功課本/g, ' ')\n .replace(/\\s+/g, ' ')\n .trim();\n return text || safeUserText(message);\n}\n\nfunction resourceContextMessage(skillResult) {\n if (!skillResult) return '';\n const lines = [\n '本轮资源检索/下载已经由大乘 App 后端完成。',\n '不要再次调用 search_dharma_resources、download_dharma_resource 或其他资源 MCP 工具;请直接基于以下结果回答用户。',\n `后端已调用 skill: ${skillResult.skillName}`,\n `执行目标: ${skillResult.query}`,\n ];\n if (skillResult.selected) {\n lines.push(`候选资源: ${skillResult.selected.title}`);\n lines.push(`来源: ${skillResult.selected.sourceName}`);\n lines.push(`资源地址: ${skillResult.selected.url}`);\n }\n if (skillResult.downloaded) {\n lines.push(`下载标题: ${skillResult.downloaded.title}`);\n lines.push(`下载文件: ${skillResult.downloaded.fileName}`);\n const text = safeUserText(skillResult.downloaded.contentText);\n if (text) {\n lines.push('下载正文摘录:');\n lines.push(text.slice(0, 8000));\n }\n }\n return lines.join('\\n');\n}\n\nfunction determineClientAction(skillResult, message) {\n if (skillResult && skillResult.downloaded) {\n const text = safeUserText(message);\n if (/法布施|全球法布施|分享/.test(text)) {\n return {\n type: 'prepare_dharma_share_text',\n title: skillResult.downloaded.title,\n text: skillResult.downloaded.contentText || '',\n };\n }\n }\n return null;\n}\n\nasync function runResourceFinderSkill({ message, user, onStep }) {\n if (!looksLikeResourceTask(message)) return null;\n\n const skillName = 'resource-finder-downloader';\n const query = extractResourceQuery(message);\n const audioTask = looksLikeAudioQuery(message) || looksLikeAudioQuery(query);\n onStep?.({\n type: 'skill.step',\n skillName,\n stage: 'start',\n title: '调用资源查找下载 skill',\n message: `识别到资源任务: ${query}`,\n });\n\n onStep?.({\n type: 'skill.step',\n skillName,\n stage: 'search',\n title: '搜索可分享资源',\n message: audioTask\n ? '优先检索公开可访问音频资源,失败时再回落到佛典正文资源。'\n : '优先检索 CBETA 佛典正文接口,搜索索引不可用时使用内置经典映射。',\n });\n const candidates = await findResourceCandidates(query, 8);\n if (candidates.length === 0) {\n onStep?.({\n type: 'skill.step',\n skillName,\n stage: 'empty',\n title: '没有找到可下载资源',\n message: '后端没有找到明确可下载且可分享的资源。',\n });\n return {\n skillName,\n query,\n candidates: [],\n selected: null,\n downloaded: null,\n };\n }\n\n const selected = candidates[0];\n onStep?.({\n type: 'skill.step',\n skillName,\n stage: 'select',\n title: '选择可信候选资源',\n message: `${selected.title} / ${selected.sourceName}`,\n });\n\n onStep?.({\n type: 'skill.step',\n skillName,\n stage: 'download',\n title: '下载并提取资源',\n message: selected.url,\n });\n const cbeta = parseCbetaResource(selected);\n const internetArchive = parseInternetArchiveResource(selected);\n const downloaded = cbeta\n ? await downloadCbetaResource({ ...selected, ...cbeta })\n : internetArchive\n ? await downloadInternetArchiveAudio({ ...selected, ...internetArchive })\n : await downloadWebResource(selected);\n const persisted = persistDownloadedResource(user, downloaded);\n\n onStep?.({\n type: 'skill.step',\n skillName,\n stage: 'done',\n title: '资源已准备完成',\n message: downloaded.fileName,\n });\n\n return {\n skillName,\n query,\n candidates,\n selected,\n downloaded: {\n ...downloaded,\n downloadId: persisted.id,\n },\n };\n}\n\napp.post(\n '/api/resources/search',\n asyncHandler(async (req, res) => {\n const query = safeUserText(req.body?.query);\n const limit = Math.min(Math.max(Number(req.body?.limit || 12), 1), 20);\n if (query.length < 2) {\n return jsonResponse(res, 400, { success: false, message: 'query is required' });\n }\n\n const items = await findResourceCandidates(query, limit);\n\n jsonResponse(res, 200, {\n success: true,\n source: 'resource-index',\n items,\n });\n }),\n);\n\nfunction parseCbetaResource(input) {\n const work = safeUserText(input.work);\n const juan = Number(input.juan || 1);\n if (work) return { work, juan: Number.isFinite(juan) ? juan : 1 };\n\n const url = safeUserText(input.url);\n const match = /^cbeta:([^:]+):(\\d+)$/i.exec(url);\n if (!match) return null;\n return {\n work: match[1].toUpperCase(),\n juan: Number(match[2] || 1),\n };\n}\n\nfunction parseInternetArchiveResource(input) {\n const url = safeUserText(input.url);\n const match = /^ia:([^:]+)$/i.exec(url);\n const identifier = match?.[1] || safeUserText(input.identifier);\n if (!identifier) return null;\n return { identifier };\n}\n\nfunction safeFileName(value) {\n const normalized = String(value || 'dharma-resource')\n .replace(/[\\\\/:*?\"<>|]+/g, '-')\n .replace(/\\s+/g, '-')\n .replace(/-+/g, '-')\n .replace(/^-|-$/g, '')\n .slice(0, 80);\n return normalized || 'dharma-resource';\n}\n\nfunction extractCbetaHtml(data) {\n const first = Array.isArray(data?.results) ? data.results[0] : null;\n if (typeof first === 'string') return first;\n if (first && typeof first === 'object' && typeof first.html === 'string') return first.html;\n if (typeof data?.html === 'string') return data.html;\n return '';\n}\n\nasync function downloadCbetaResource(resource) {\n const url = buildUrl(cbetaApiRoot, 'juans', {\n work: resource.work,\n juan: resource.juan,\n work_info: 1,\n toc: 1,\n });\n const data = await fetchJson(url);\n const workInfo = data.work_info || {};\n const title = safeUserText(workInfo.title) || safeUserText(resource.title) || resource.work;\n const html = extractCbetaHtml(data);\n const text = cleanExtractedText(stripHtml(html));\n if (!text) throw new Error('CBETA resource returned empty content');\n return {\n title,\n sourceName: 'CBETA 佛典',\n url: `cbeta:${resource.work}:${resource.juan}`,\n contentText: text.slice(0, maxResourceTextChars),\n fileName: `${resource.work}_${resource.juan}_${safeFileName(title)}.txt`,\n };\n}\n\nasync function downloadInternetArchiveAudio(resource) {\n const identifier = safeUserText(resource.identifier);\n if (!identifier) throw new Error('Internet Archive identifier is required');\n const metadata = await fetchJson(`https://archive.org/metadata/${encodeURIComponent(identifier)}`);\n const title = safeUserText(metadata?.metadata?.title) || safeUserText(resource.title) || identifier;\n const files = Array.isArray(metadata?.files) ? metadata.files : [];\n const audioFile = files.find((file) => {\n const name = safeUserText(file?.name);\n const format = safeUserText(file?.format).toLowerCase();\n return /\\.(mp3|m4a|ogg)$/i.test(name) || format.includes('mp3');\n });\n if (!audioFile?.name) {\n throw new Error('Internet Archive audio item has no downloadable audio file');\n }\n const sourceFileName = audioFile.name;\n const fileUrl = `https://archive.org/download/${encodeURIComponent(identifier)}/${sourceFileName\n .split('/')\n .map((part) => encodeURIComponent(part))\n .join('/')}`;\n const response = await fetch(fileUrl, {\n headers: {\n Accept: 'audio/mpeg, audio/*;q=0.9, */*;q=0.5',\n 'User-Agent': 'DachengResourceDownloader/1.0 (+https://ombhrum.com)',\n },\n signal: AbortSignal.timeout(60_000),\n });\n if (!response.ok) {\n throw new Error(`Internet Archive audio download failed: ${response.status}`);\n }\n const binaryContent = Buffer.from(await response.arrayBuffer());\n if (!binaryContent.length) throw new Error('Internet Archive audio returned empty content');\n const sizeMb = (binaryContent.length / 1024 / 1024).toFixed(1);\n return {\n title,\n sourceName: 'Internet Archive 音频',\n url: fileUrl,\n contentText: `已下载音频资源: ${title}\\n文件名: ${sourceFileName}\\n大小: ${sizeMb} MB\\n来源: ${fileUrl}`,\n binaryContent,\n fileName: safeFileName(sourceFileName),\n };\n}\n\nasync function downloadWebResource(input) {\n const url = safeUserText(input.url);\n const parsed = new URL(url);\n if (!['http:', 'https:'].includes(parsed.protocol)) {\n throw new Error('Only http/https resources can be downloaded');\n }\n\n const response = await fetch(parsed, {\n headers: {\n Accept: 'text/html, text/plain, application/xhtml+xml, application/xml;q=0.8, */*;q=0.5',\n 'User-Agent': 'DachengResourceDownloader/1.0 (+https://ombhrum.com)',\n },\n signal: AbortSignal.timeout(25_000),\n });\n if (!response.ok) throw new Error(`Resource download failed: ${response.status}`);\n const contentType = response.headers.get('content-type') || '';\n const body = await response.text();\n const title = safeUserText(input.title) || parsed.hostname;\n const contentText = contentType.includes('html') ? cleanExtractedText(stripHtml(body)) : body.trim();\n if (!contentText) throw new Error('Resource returned empty content');\n return {\n title,\n sourceName: safeUserText(input.sourceName) || parsed.hostname,\n url,\n contentText: contentText.slice(0, maxResourceTextChars),\n fileName: `${safeFileName(title)}.txt`,\n };\n}\n\nfunction persistDownloadedResource(user, content) {\n const id = crypto.randomUUID();\n const fileName = `${id}-${safeFileName(content.fileName)}`;\n const filePath = path.join(resourcesDir, fileName);\n if (content.binaryContent) {\n fs.writeFileSync(filePath, content.binaryContent);\n } else {\n fs.writeFileSync(filePath, content.contentText, 'utf8');\n }\n statements.insertResourceDownload.run({\n id,\n userId: user.userId,\n title: content.title,\n sourceName: content.sourceName,\n sourceUrl: content.url,\n fileName: content.fileName,\n filePath,\n createdAt: nowIso(),\n });\n return { id, fileName, filePath };\n}\n\nfunction createLibreChatMcpServer() {\n const server = new McpServer({\n name: 'dacheng-ai-tools',\n version: '0.1.0',\n });\n\n server.registerTool(\n 'search_dharma_resources',\n {\n title: 'Search dharma resources',\n description:\n 'Search for legally shareable Buddhist scripture, text, web, or audio resources that can be prepared for Fabushi sharing.',\n inputSchema: {\n query: z.string().min(2).max(200),\n limit: z.number().int().min(1).max(12).optional(),\n },\n outputSchema: {\n query: z.string(),\n items: z.array(\n z.object({\n id: z.string(),\n title: z.string(),\n sourceName: z.string(),\n url: z.string(),\n snippet: z.string(),\n resourceType: z.string(),\n work: z.string().optional(),\n juan: z.number().optional(),\n }),\n ),\n },\n annotations: {\n readOnlyHint: true,\n destructiveHint: false,\n openWorldHint: true,\n },\n },\n async ({ query, limit }) => {\n const items = await findResourceCandidates(query, limit ?? 8);\n return textToolResult(`Found ${items.length} candidate resources for \"${query}\".`, {\n query,\n items,\n });\n },\n );\n\n server.registerTool(\n 'download_dharma_resource',\n {\n title: 'Download dharma resource',\n description:\n 'Download and extract a selected resource. Use the url/work/juan returned by search_dharma_resources whenever possible.',\n inputSchema: {\n url: z.string().min(1).max(1000),\n title: z.string().max(300).optional(),\n sourceName: z.string().max(120).optional(),\n work: z.string().max(40).optional(),\n juan: z.number().int().min(1).optional(),\n identifier: z.string().max(300).optional(),\n },\n outputSchema: {\n title: z.string(),\n sourceName: z.string(),\n url: z.string(),\n fileName: z.string(),\n downloadId: z.string(),\n contentText: z.string(),\n },\n annotations: {\n readOnlyHint: false,\n destructiveHint: false,\n openWorldHint: true,\n },\n },\n async (input) => {\n const user = { userId: 'librechat:mcp' };\n const cbeta = parseCbetaResource(input);\n const internetArchive = parseInternetArchiveResource(input);\n const content = cbeta\n ? await downloadCbetaResource({ ...input, ...cbeta })\n : internetArchive\n ? await downloadInternetArchiveAudio({ ...input, ...internetArchive })\n : await downloadWebResource(input);\n const persisted = persistDownloadedResource(user, content);\n const { binaryContent: _binaryContent, ...publicContent } = content;\n return textToolResult(`Downloaded ${publicContent.title} as ${publicContent.fileName}.`, {\n ...publicContent,\n downloadId: persisted.id,\n });\n },\n );\n\n server.registerTool(\n 'prepare_dharma_share_text',\n {\n title: 'Prepare dharma sharing text',\n description:\n 'Prepare a text payload for the Fabushi app to confirm and add to global dharma sharing. This returns a clientAction; it does not start sending.',\n inputSchema: {\n title: z.string().min(1).max(120),\n text: z.string().min(1).max(20000),\n },\n outputSchema: {\n clientAction: z.object({\n type: z.literal('prepare_dharma_share_text'),\n title: z.string(),\n text: z.string(),\n }),\n },\n annotations: {\n readOnlyHint: false,\n destructiveHint: false,\n openWorldHint: false,\n },\n },\n async ({ title, text }) =>\n textToolResult(`Prepared \"${title}\" for user confirmation in the Fabushi app.`, {\n clientAction: {\n type: 'prepare_dharma_share_text',\n title: safeUserText(title).slice(0, 120),\n text: String(text || '').trim().slice(0, 20000),\n },\n }),\n );\n\n server.registerTool(\n 'prepare_practice_book_item',\n {\n title: 'Prepare practice book item',\n description:\n 'Prepare a scripture, chant, or practice item for the Fabushi app practice book. This returns a clientAction; it does not mutate the app by itself.',\n inputSchema: {\n title: z.string().min(1).max(120),\n sourceName: z.string().max(120).optional(),\n text: z.string().min(1).max(20000),\n },\n outputSchema: {\n clientAction: z.object({\n type: z.literal('prepare_practice_book_item'),\n title: z.string(),\n sourceName: z.string(),\n text: z.string(),\n }),\n },\n annotations: {\n readOnlyHint: false,\n destructiveHint: false,\n openWorldHint: false,\n },\n },\n async ({ title, sourceName, text }) =>\n textToolResult(`Prepared \"${title}\" for the Fabushi practice book confirmation flow.`, {\n clientAction: {\n type: 'prepare_practice_book_item',\n title: safeUserText(title).slice(0, 120),\n sourceName: safeUserText(sourceName || '大乘 AI').slice(0, 120),\n text: String(text || '').trim().slice(0, 20000),\n },\n }),\n );\n\n return server;\n}\n\nasync function handleMcpRequest(req, res) {\n const server = createLibreChatMcpServer();\n const transport = new StreamableHTTPServerTransport({\n sessionIdGenerator: undefined,\n enableJsonResponse: true,\n });\n\n res.on('close', () => {\n transport.close();\n server.close();\n });\n\n await server.connect(transport);\n await transport.handleRequest(req, res, req.body);\n}\n\napp.all('/mcp', async (req, res) => {\n try {\n await handleMcpRequest(req, res);\n } catch (error) {\n logger.error({ error: error.stack || String(error) }, 'MCP request failed');\n if (!res.headersSent) {\n res.status(500).json({\n error: 'MCP request failed',\n message: publicAiErrorMessage(error),\n });\n }\n }\n});\n\napp.post(\n '/api/resources/download',\n asyncHandler(async (req, res) => {\n const user = await resolveUser(req, req.body || {});\n const cbeta = parseCbetaResource(req.body || {});\n const internetArchive = parseInternetArchiveResource(req.body || {});\n const content = cbeta\n ? await downloadCbetaResource({ ...req.body, ...cbeta })\n : internetArchive\n ? await downloadInternetArchiveAudio({ ...req.body, ...internetArchive })\n : await downloadWebResource(req.body || {});\n const persisted = persistDownloadedResource(user, content);\n const { binaryContent: _binaryContent, ...publicContent } = content;\n jsonResponse(res, 200, {\n success: true,\n downloadId: persisted.id,\n ...publicContent,\n });\n }),\n);\n\napp.post(\n '/api/codex/resource-task',\n asyncHandler(async (req, res) => {\n if (process.env.ENABLE_CODEX_SDK !== 'true') {\n return jsonResponse(res, 403, {\n success: false,\n message: 'Codex SDK task runner is disabled. Set ENABLE_CODEX_SDK=true on the server to enable it.',\n });\n }\n const prompt = safeUserText(req.body?.prompt);\n if (!prompt) {\n return jsonResponse(res, 400, { success: false, message: 'prompt is required' });\n }\n const user = await resolveUser(req, req.body || {});\n const { Codex } = await import('@openai/codex-sdk');\n const codexRuntime = createCodexDeepSeekRuntime(user);\n const codex = new Codex(codexRuntime.enabled ? codexRuntime.options : {});\n const thread = codex.startThread(\n codexRuntime.enabled\n ? codexRuntime.threadOptions\n : { skipGitRepoCheck: true, networkAccessEnabled: true },\n );\n const result = await thread.run(prompt);\n const usage = {\n promptTokens: Number(result.usage?.input_tokens || 0),\n completionTokens: Number(result.usage?.output_tokens || 0),\n totalTokens: Number((result.usage?.input_tokens || 0) + (result.usage?.output_tokens || 0)),\n };\n if (!codexRuntime.enabled) {\n recordUsage(user.userId, usage.totalTokens || estimateTokens(JSON.stringify(result)));\n }\n jsonResponse(res, 200, {\n success: true,\n threadId: thread.id,\n provider: codexRuntime.provider,\n model: deepseekModel,\n usage,\n result,\n });\n }),\n);\n\nregisterPlatformApi({ app, db, resolveUser, asyncHandler });\n\napp.use((error, _req, res, _next) => {\n const status = Number(error.statusCode || error.status || 500);\n const safeStatus = status >= 400 && status < 600 ? status : 500;\n logger.error({ error: error.stack || String(error) }, 'request failed');\n jsonResponse(res, safeStatus, {\n success: false,\n message: error.message || 'Internal Server Error',\n details: error.details,\n });\n});\n\nconst port = Number(process.env.PORT || 8788);\napp.listen(port, '0.0.0.0', () => {\n logger.info(\n {\n port,\n dbPath,\n dataDir,\n deepseekModel,\n cbetaApiRoot,\n },\n 'Dacheng AI backend started',\n );\n});\n"}, "8d3bca9070fe3716ec5377799e431e8a00c73d8f": {"t": 1785757945.0055866, "n": 144923, "h": "55cea71410263f42", "c": "import crypto from 'node:crypto';\nimport fs from 'node:fs';\nimport path from 'node:path';\nimport process from 'node:process';\nimport { fileURLToPath } from 'node:url';\n\nimport { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';\nimport { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';\nimport Database from 'better-sqlite3';\nimport cors from 'cors';\nimport dotenv from 'dotenv';\nimport express from 'express';\nimport rateLimit from 'express-rate-limit';\nimport helmet from 'helmet';\nimport pino from 'pino';\nimport { z } from 'zod';\n\nimport { registerPlatformApi } from './platform_api.js';\nimport {\n codexResponsesMessages,\n codexResponsesTools,\n deepSeekResultToResponseItems,\n} from './codex_deepseek_adapter.js';\nimport {\n createPluginCodexPolicy,\n pluginConversationNamespace,\n pluginMcpTokenEnv,\n} from './plugin_codex_policy.js';\nimport {\n handleOfficialMcpRequest,\n officialMcpApps,\n} from './official_mcp_apps.js';\n\ndotenv.config();\n\nconst __dirname = path.dirname(fileURLToPath(import.meta.url));\nconst rootDir = path.resolve(__dirname, '..');\nconst dataDir = process.env.DATA_DIR || path.join(rootDir, 'data');\nconst resourcesDir = path.join(dataDir, 'resources');\nconst openClawRuntimeUpdatesDir =\n process.env.OPENCLAW_RUNTIME_UPDATES_DIR || path.join(dataDir, 'openclaw-runtime');\nconst openClawRuntimeEnableMacosUpdates =\n process.env.OPENCLAW_RUNTIME_ENABLE_MACOS_UPDATES === 'true';\nconst codexHomeDir = process.env.CODEX_HOME || path.join(dataDir, 'codex-home');\nconst codexPluginSessionsDir = path.join(dataDir, 'codex-plugin-sessions');\nconst codexTempDir = process.env.CODEX_TMPDIR || path.join(dataDir, 'codex-tmp');\nconst codexRuntimeDir = process.env.XDG_RUNTIME_DIR || path.join(dataDir, 'codex-runtime');\nconst dbPath = process.env.SQLITE_PATH || path.join(dataDir, 'dacheng-ai.sqlite');\n\nfs.mkdirSync(resourcesDir, { recursive: true });\nfs.mkdirSync(openClawRuntimeUpdatesDir, { recursive: true });\nfs.mkdirSync(codexHomeDir, { recursive: true });\nfs.mkdirSync(codexPluginSessionsDir, { recursive: true });\nfs.mkdirSync(codexTempDir, { recursive: true });\nfs.mkdirSync(codexRuntimeDir, { recursive: true });\n\nconst logger = pino({\n level: process.env.LOG_LEVEL || 'info',\n});\n\nconst app = express();\napp.disable('x-powered-by');\napp.set('trust proxy', Number(process.env.TRUST_PROXY_HOPS || 1));\napp.use(helmet());\nconst corsOrigin =\n !process.env.CORS_ORIGIN || process.env.CORS_ORIGIN.trim() === '*'\n ? true\n : process.env.CORS_ORIGIN.split(',').map((item) => item.trim()).filter(Boolean);\napp.use(cors({\n origin: corsOrigin,\n credentials: true,\n exposedHeaders: ['mcp-session-id', 'mcp-protocol-version'],\n}));\napp.use(express.json({ limit: '1mb' }));\napp.use(\n rateLimit({\n windowMs: 60_000,\n limit: Number(process.env.RATE_LIMIT_PER_MINUTE || 90),\n standardHeaders: true,\n legacyHeaders: false,\n }),\n);\n\nconst db = new Database(dbPath);\ndb.pragma('journal_mode = WAL');\ndb.pragma('foreign_keys = ON');\n\ndb.exec(`\n CREATE TABLE IF NOT EXISTS conversations (\n id TEXT PRIMARY KEY,\n user_id TEXT NOT NULL,\n title TEXT NOT NULL,\n provider TEXT NOT NULL,\n model TEXT NOT NULL,\n created_at TEXT NOT NULL,\n updated_at TEXT NOT NULL\n );\n\n CREATE INDEX IF NOT EXISTS idx_conversations_user_updated\n ON conversations (user_id, updated_at DESC);\n\n CREATE TABLE IF NOT EXISTS messages (\n id TEXT PRIMARY KEY,\n conversation_id TEXT NOT NULL,\n role TEXT NOT NULL,\n content TEXT NOT NULL,\n prompt_tokens INTEGER NOT NULL DEFAULT 0,\n completion_tokens INTEGER NOT NULL DEFAULT 0,\n total_tokens INTEGER NOT NULL DEFAULT 0,\n created_at TEXT NOT NULL,\n FOREIGN KEY (conversation_id) REFERENCES conversations(id) ON DELETE CASCADE\n );\n\n CREATE INDEX IF NOT EXISTS idx_messages_conversation_created\n ON messages (conversation_id, created_at ASC);\n\n CREATE TABLE IF NOT EXISTS usage_months (\n user_id TEXT NOT NULL,\n month TEXT NOT NULL,\n total_tokens INTEGER NOT NULL DEFAULT 0,\n updated_at TEXT NOT NULL,\n PRIMARY KEY (user_id, month)\n );\n\n CREATE TABLE IF NOT EXISTS resource_downloads (\n id TEXT PRIMARY KEY,\n user_id TEXT NOT NULL,\n title TEXT NOT NULL,\n source_name TEXT NOT NULL,\n source_url TEXT NOT NULL,\n file_name TEXT NOT NULL,\n file_path TEXT NOT NULL,\n created_at TEXT NOT NULL\n );\n\n CREATE TABLE IF NOT EXISTS agent_runs (\n id TEXT PRIMARY KEY,\n user_id TEXT NOT NULL,\n conversation_id TEXT NOT NULL,\n message_id TEXT NOT NULL,\n openclaw_run_id TEXT,\n openclaw_session_key TEXT NOT NULL,\n status TEXT NOT NULL,\n mode TEXT NOT NULL,\n provider TEXT NOT NULL,\n model TEXT,\n input_tokens INTEGER NOT NULL DEFAULT 0,\n output_tokens INTEGER NOT NULL DEFAULT 0,\n tool_call_count INTEGER NOT NULL DEFAULT 0,\n cost_microusd INTEGER NOT NULL DEFAULT 0,\n started_at TEXT NOT NULL,\n completed_at TEXT,\n failed_at TEXT,\n error_code TEXT,\n error_message TEXT\n );\n\n CREATE INDEX IF NOT EXISTS idx_agent_runs_user_started\n ON agent_runs (user_id, started_at DESC);\n\n CREATE INDEX IF NOT EXISTS idx_agent_runs_status_started\n ON agent_runs (status, started_at DESC);\n\n CREATE TABLE IF NOT EXISTS agent_message_feedback (\n id TEXT PRIMARY KEY,\n message_id TEXT NOT NULL,\n user_id TEXT NOT NULL,\n rating TEXT NOT NULL,\n reason TEXT,\n comment TEXT,\n created_at TEXT NOT NULL\n );\n`);\n\nconst conversationColumns = new Set(\n db.prepare('PRAGMA table_info(conversations)').all().map((column) => column.name),\n);\nif (!conversationColumns.has('namespace')) {\n db.exec('ALTER TABLE conversations ADD COLUMN namespace TEXT');\n}\nif (!conversationColumns.has('thread_id')) {\n db.exec('ALTER TABLE conversations ADD COLUMN thread_id TEXT');\n}\ndb.exec(`\n CREATE INDEX IF NOT EXISTS idx_conversations_user_namespace_updated\n ON conversations (user_id, namespace, updated_at DESC)\n`);\n\nconst statements = {\n insertConversation: db.prepare(`\n INSERT INTO conversations (id, user_id, title, provider, model, created_at, updated_at)\n VALUES (@id, @userId, @title, @provider, @model, @createdAt, @updatedAt)\n `),\n updateConversation: db.prepare(`\n UPDATE conversations\n SET title = @title, updated_at = @updatedAt\n WHERE id = @id AND user_id = @userId\n `),\n getConversation: db.prepare(`\n SELECT * FROM conversations WHERE id = ? AND user_id = ? LIMIT 1\n `),\n listConversations: db.prepare(`\n SELECT id, title, updated_at AS updatedAt\n FROM conversations\n WHERE user_id = ? AND (namespace IS NULL OR namespace = '')\n ORDER BY updated_at DESC\n LIMIT 30\n `),\n insertPluginConversation: db.prepare(`\n INSERT INTO conversations (\n id, user_id, title, provider, model, namespace, thread_id, created_at, updated_at\n )\n VALUES (\n @id, @userId, @title, @provider, @model, @namespace, NULL, @createdAt, @updatedAt\n )\n `),\n getPluginConversation: db.prepare(`\n SELECT * FROM conversations\n WHERE id = ? AND user_id = ? AND namespace = ?\n LIMIT 1\n `),\n updatePluginConversation: db.prepare(`\n UPDATE conversations\n SET title = @title, thread_id = @threadId, updated_at = @updatedAt\n WHERE id = @id AND user_id = @userId AND namespace = @namespace\n `),\n insertMessage: db.prepare(`\n INSERT INTO messages (\n id, conversation_id, role, content,\n prompt_tokens, completion_tokens, total_tokens, created_at\n )\n VALUES (\n @id, @conversationId, @role, @content,\n @promptTokens, @completionTokens, @totalTokens, @createdAt\n )\n `),\n listMessages: db.prepare(`\n SELECT role, content, prompt_tokens AS promptTokens,\n completion_tokens AS completionTokens, total_tokens AS totalTokens,\n created_at AS createdAt\n FROM messages\n WHERE conversation_id = ?\n ORDER BY created_at ASC\n `),\n getUsage: db.prepare(`\n SELECT total_tokens AS totalTokens FROM usage_months WHERE user_id = ? AND month = ?\n `),\n upsertUsage: db.prepare(`\n INSERT INTO usage_months (user_id, month, total_tokens, updated_at)\n VALUES (@userId, @month, @totalTokens, @updatedAt)\n ON CONFLICT(user_id, month)\n DO UPDATE SET total_tokens = usage_months.total_tokens + excluded.total_tokens,\n updated_at = excluded.updated_at\n `),\n insertResourceDownload: db.prepare(`\n INSERT INTO resource_downloads (\n id, user_id, title, source_name, source_url, file_name, file_path, created_at\n )\n VALUES (\n @id, @userId, @title, @sourceName, @sourceUrl, @fileName, @filePath, @createdAt\n )\n `),\n insertAgentRun: db.prepare(`\n INSERT INTO agent_runs (\n id, user_id, conversation_id, message_id, openclaw_run_id, openclaw_session_key,\n status, mode, provider, model, started_at\n )\n VALUES (\n @id, @userId, @conversationId, @messageId, @openClawRunId, @openClawSessionKey,\n @status, @mode, @provider, @model, @startedAt\n )\n `),\n getAgentRun: db.prepare(`\n SELECT * FROM agent_runs WHERE id = ? AND user_id = ? LIMIT 1\n `),\n updateAgentRunStatus: db.prepare(`\n UPDATE agent_runs\n SET status = @status,\n openclaw_run_id = COALESCE(@openClawRunId, openclaw_run_id),\n input_tokens = COALESCE(@inputTokens, input_tokens),\n output_tokens = COALESCE(@outputTokens, output_tokens),\n tool_call_count = COALESCE(@toolCallCount, tool_call_count),\n completed_at = @completedAt,\n failed_at = @failedAt,\n error_code = @errorCode,\n error_message = @errorMessage\n WHERE id = @id AND user_id = @userId\n `),\n insertAgentFeedback: db.prepare(`\n INSERT INTO agent_message_feedback (id, message_id, user_id, rating, reason, comment, created_at)\n VALUES (@id, @messageId, @userId, @rating, @reason, @comment, @createdAt)\n `),\n};\n\nconst deepseekApiKey = process.env.DEEPSEEK_API_KEY || '';\nconst deepseekBaseUrl = (process.env.DEEPSEEK_BASE_URL || 'https://api.deepseek.com').replace(/\\/+$/, '');\nconst deepseekModel = process.env.DEEPSEEK_MODEL || 'deepseek-chat';\nconst deepseekAvailableModels = Array.from(\n new Set(\n (process.env.DEEPSEEK_MODELS || `${deepseekModel},deepseek-chat,deepseek-reasoner`)\n .split(',')\n .map((item) => normalizeDeepSeekModelName(item))\n .filter(Boolean),\n ),\n);\nconst cbetaApiRoot = (process.env.CBETA_API_ROOT || 'https://api.ombhrum.com/api/cbeta').replace(/\\/+$/, '');\nconst fabushiApiBaseUrl = (process.env.FABUSHI_API_BASE_URL || 'https://api.ombhrum.com').replace(/\\/+$/, '');\nconst memberMonthlyLimit = Number(process.env.MEMBER_MONTHLY_TOKEN_LIMIT || 1_000_000);\nconst freeMonthlyLimit = Number(process.env.FREE_MONTHLY_TOKEN_LIMIT || 1_000);\nconst requireMemberForAi = process.env.REQUIRE_MEMBER_FOR_AI === 'true';\nconst deepseekMaxCompletionTokens = Math.max(1, Number(process.env.DEEPSEEK_MAX_COMPLETION_TOKENS || 1400));\nconst botFatherMaxCompletionTokens = Math.max(\n deepseekMaxCompletionTokens,\n Math.min(12_000, Number(process.env.BOT_FATHER_MAX_COMPLETION_TOKENS || 6000)),\n);\nconst maxResourceTextChars = Number(process.env.MAX_RESOURCE_TEXT_CHARS || 80_000);\nconst openClawRuntimeManifestPath =\n process.env.OPENCLAW_RUNTIME_MANIFEST_PATH || path.join(openClawRuntimeUpdatesDir, 'manifest.json');\nconst openClawRuntimePublicBaseUrl = (process.env.OPENCLAW_RUNTIME_PUBLIC_BASE_URL || '').replace(/\\/+$/, '');\nconst enableLibreChatAgentChat = process.env.ENABLE_LIBRECHAT_AGENT_CHAT === 'true';\nconst libreChatAgentsBaseUrl = (\n process.env.LIBRECHAT_AGENTS_BASE_URL || 'http://127.0.0.1:3080/api/agents/v1'\n).replace(/\\/+$/, '');\nconst libreChatAgentApiKey = process.env.LIBRECHAT_AGENT_API_KEY || '';\nconst libreChatAgentId = process.env.LIBRECHAT_AGENT_ID || '';\nconst enableCodexSdkChat = process.env.ENABLE_CODEX_SDK_CHAT === 'true';\nconst enableOpenClawAgentChat = process.env.ENABLE_OPENCLAW_AGENT_CHAT === 'true';\nconst openClawGatewayUrl = (process.env.OPENCLAW_GATEWAY_URL || 'http://127.0.0.1:18789').replace(/\\/+$/, '');\nconst openClawGatewayToken = process.env.OPENCLAW_GATEWAY_TOKEN || '';\nconst openClawAgentId = process.env.OPENCLAW_AGENT_ID || 'fabushi-public-agent';\nconst openClawRunsEndpoint = process.env.OPENCLAW_RUNS_ENDPOINT || '';\nconst codexDeepSeekProviderId = 'dacheng-deepseek-proxy';\nconst codexResponsesBaseUrl = (\n process.env.CODEX_DEEPSEEK_RESPONSES_BASE_URL ||\n `http://127.0.0.1:${process.env.PORT || 8788}/codex-deepseek/v1`\n).replace(/\\/+$/, '');\nconst codexAdapterTokenPrefix = 'dacheng-codex-proxy.';\nconst codexAdapterTokenTtlMs = Math.max(\n 60_000,\n Number(process.env.CODEX_DEEPSEEK_ADAPTER_TOKEN_TTL_MS || 15 * 60_000),\n);\nconst codexAdapterSecret =\n process.env.CODEX_DEEPSEEK_ADAPTER_SECRET ||\n deepseekApiKey ||\n openClawGatewayToken ||\n 'dacheng-codex-local-dev-secret';\n\nconst systemPrompt = [\n '你是“大乘”App 的 AI 助手。',\n '你的核心任务是帮助用户查找、整理、理解并全球法布施合法可分享的佛法资源。',\n '回答要庄重、简洁、可执行;涉及经典、仪轨或资源时,提醒用户尊重版权、来源和当地法规。',\n '如果用户想找资源、下载经文、寻找音频或准备可分享资料,你必须先在后端自动执行搜索、验证和下载步骤;不要要求用户去前端 + 菜单手动查找。',\n '你可以把执行进度总结给用户,但不要输出隐藏推理原文。',\n].join('\\n');\n\nfunction nowIso() {\n return new Date().toISOString();\n}\n\nfunction monthKey(date = new Date()) {\n return `${date.getUTCFullYear()}-${String(date.getUTCMonth() + 1).padStart(2, '0')}`;\n}\n\nfunction jsonResponse(res, status, payload) {\n res.status(status).json(payload);\n}\n\nasync function readJsonFile(filePath) {\n const raw = await fs.promises.readFile(filePath, 'utf8');\n return JSON.parse(raw);\n}\n\nfunction requestOrigin(req) {\n if (openClawRuntimePublicBaseUrl) return openClawRuntimePublicBaseUrl;\n const proto = req.get('X-Forwarded-Proto') || req.protocol || 'https';\n const host = req.get('X-Forwarded-Host') || req.get('Host');\n return host ? `${proto}://${host}` : '';\n}\n\nfunction absolutizeOpenClawRuntimeUrl(req, value) {\n const text = readText(value);\n if (!text) return '';\n if (/^https?:\\/\\//i.test(text)) return text;\n const normalized = text.startsWith('/')\n ? text\n : `/api/openclaw/runtime/files/${text.replace(/^\\/+/, '')}`;\n const origin = requestOrigin(req);\n return origin ? new URL(normalized, `${origin}/`).toString() : normalized;\n}\n\nfunction textToolResult(text, structuredContent = {}) {\n return {\n structuredContent,\n content: [{ type: 'text', text }],\n };\n}\n\nfunction sseHeaders(res) {\n res.status(200);\n res.setHeader('Content-Type', 'text/event-stream; charset=utf-8');\n res.setHeader('Cache-Control', 'no-cache, no-transform');\n res.setHeader('Connection', 'keep-alive');\n res.setHeader('X-Accel-Buffering', 'no');\n if (typeof res.flushHeaders === 'function') res.flushHeaders();\n}\n\nfunction writeSse(res, event, payload) {\n res.write(`event: ${event}\\n`);\n res.write(`data: ${JSON.stringify(payload)}\\n\\n`);\n}\n\nfunction asyncHandler(fn) {\n return (req, res, next) => {\n Promise.resolve(fn(req, res, next)).catch(next);\n };\n}\n\nfunction bearerToken(req) {\n const delegated = readText(req.get('x-dacheng-auth-token'));\n if (\n delegated &&\n delegated !== 'DACHENG_AUTH_TOKEN' &&\n !delegated.startsWith('secretref-env:') &&\n delegated !== 'dacheng-openclaw-proxy'\n ) {\n return delegated;\n }\n const header = req.get('Authorization') || '';\n if (!header.startsWith('Bearer ')) return '';\n const token = header.slice('Bearer '.length).trim();\n return token === 'dacheng-openclaw-proxy' ? '' : token;\n}\n\nfunction sha256(value) {\n return crypto.createHash('sha256').update(value).digest('hex');\n}\n\nfunction safeUserText(value, fallback = '') {\n if (typeof value !== 'string') return fallback;\n return value.replace(/\\u0000/g, '').trim();\n}\n\nfunction estimateTokens(text) {\n const normalized = String(text || '').trim();\n if (!normalized) return 0;\n return Math.max(1, Math.ceil(normalized.length / 3));\n}\n\nfunction readText(value) {\n if (value === undefined || value === null) return '';\n return String(value).replace(/\\u0000/g, '').trim();\n}\n\nfunction clientContextMessage(client) {\n if (!client || typeof client !== 'object') return '';\n const selectedModel = readText(client.selectedModel || client.model);\n const project = client.project && typeof client.project === 'object' ? client.project : {};\n const projectName = readText(project.name);\n const projectPath = readText(project.path);\n const lines = ['桌面端上下文:'];\n if (selectedModel) lines.push(`用户选择的 DeepSeek 模型:${normalizeDeepSeekModelName(selectedModel)}`);\n if (projectPath) {\n lines.push(`当前项目名称:${projectName || projectPath}`);\n lines.push(`当前项目路径:${projectPath}`);\n lines.push('如果本轮任务涉及读取、修改、运行、分析或整理项目,请优先围绕当前项目路径工作;不要擅自操作其他目录。');\n }\n return lines.length > 1 ? lines.join('\\n') : '';\n}\nfunction unwrapPayload(payload) {\n if (!payload || typeof payload !== 'object') return {};\n return payload.data && typeof payload.data === 'object' ? payload.data : payload;\n}\n\nfunction readFirstText(values) {\n for (const value of values) {\n const text = readText(value);\n if (text) return text;\n }\n return '';\n}\n\nfunction activeFlag(value) {\n if (value === true) return true;\n if (value === 1) return true;\n if (typeof value !== 'string') return false;\n return ['true', '1', 'yes', 'active'].includes(value.trim().toLowerCase());\n}\n\nfunction futureDate(value) {\n const text = readText(value);\n if (!text) return false;\n const timestamp = Date.parse(text);\n return Number.isFinite(timestamp) && timestamp > Date.now();\n}\n\nfunction normalizeMembershipPayload(payload) {\n if (!payload || typeof payload !== 'object') return false;\n const data = unwrapPayload(payload);\n const membership = data.membership && typeof data.membership === 'object' ? data.membership : data;\n const type = readText(membership.type || membership.membershipType || data.membershipType).toLowerCase();\n const status = readText(membership.status || data.status).toLowerCase();\n const expiry = membership.expiresAt ||\n membership.expires_at ||\n membership.membershipExpiry ||\n membership.membership_expires_at ||\n data.membershipExpiry ||\n data.membership_expires_at;\n const hasExplicitActiveFlag =\n membership.isActive !== undefined ||\n membership.active !== undefined ||\n membership.hasPremiumAccess !== undefined ||\n membership.hasPremiumMembership !== undefined ||\n membership.isPremium !== undefined;\n\n if (hasExplicitActiveFlag) {\n return Boolean(\n activeFlag(membership.isActive) ||\n activeFlag(membership.active) ||\n activeFlag(membership.hasPremiumAccess) ||\n activeFlag(membership.hasPremiumMembership) ||\n activeFlag(membership.isPremium),\n );\n }\n\n return Boolean(\n status === 'active' ||\n type === 'paid' ||\n type === 'premium' ||\n type === 'trial' ||\n futureDate(expiry),\n );\n}\n\nfunction extractRemoteAccount(payload) {\n const data = unwrapPayload(payload);\n const nestedUser = data.user && typeof data.user === 'object' ? data.user : {};\n const id = readFirstText([\n data.userId,\n data.user_id,\n data.userNo,\n data.user_no,\n data.id,\n nestedUser.userId,\n nestedUser.user_id,\n nestedUser.userNo,\n nestedUser.user_no,\n nestedUser.id,\n ]);\n const username = readFirstText([data.username, data.name, nestedUser.username, nestedUser.name]);\n const email = readFirstText([data.email, nestedUser.email]);\n const membership = data.membership && typeof data.membership === 'object' ? data.membership : null;\n return {\n authenticated: Boolean(id || username || email || membership || data.success === true),\n id,\n username,\n email,\n isMember: normalizeMembershipPayload(payload),\n membership,\n };\n}\n\nasync function resolveRemoteAccount(token) {\n const fallback = {\n authenticated: false,\n id: '',\n username: '',\n email: '',\n isMember: false,\n membership: null,\n };\n if (!token || !fabushiApiBaseUrl) return fallback;\n\n const endpoints = [\n '/api/stripe/membership-status',\n '/api/alipay/check-membership',\n '/api/auth/user-info',\n ];\n\n for (const endpoint of endpoints) {\n try {\n const response = await fetch(`${fabushiApiBaseUrl}${endpoint}`, {\n headers: {\n Accept: 'application/json',\n Authorization: `Bearer ${token}`,\n },\n signal: AbortSignal.timeout(8_000),\n });\n if (response.status === 401 || response.status === 403) return fallback;\n if (!response.ok) continue;\n const payload = await response.json();\n const account = extractRemoteAccount(payload);\n if (account.authenticated || account.isMember) {\n return {\n ...fallback,\n ...account,\n authenticated: true,\n };\n }\n } catch (error) {\n logger.debug({ endpoint, error: String(error) }, 'membership lookup skipped');\n }\n }\n\n return fallback;\n}\n\nasync function resolveUser(req, body = {}) {\n const token = bearerToken(req);\n const usernameHint = safeUserText(body.username || req.query.username);\n\n const internalMcpAccount = resolveCodexAdapterToken(token);\n if (\n internalMcpAccount?.audience === 'mcp-plugin' &&\n req.path.startsWith('/api/mcp/apps/')\n ) {\n return internalMcpAccount.user;\n }\n\n const testToken = readText(process.env.TEST_ACCOUNT_TOKEN);\n if (testToken && token && token === testToken) {\n return {\n userId: 'user:test_account',\n username: 'TestAccount',\n tokenHash: sha256(token).slice(0, 24),\n isAuthenticated: true,\n isMember: true,\n isTestAccount: true,\n membership: { type: 'lifetime', active: true },\n };\n }\n\n const tokenHash = token ? sha256(token).slice(0, 24) : '';\n const anonymousHash = sha256(`${req.ip}|${req.get('User-Agent') || ''}`).slice(0, 24);\n const remoteAccount = await resolveRemoteAccount(token);\n const remoteIdentity = remoteAccount.id ||\n remoteAccount.username ||\n remoteAccount.email ||\n (remoteAccount.authenticated ? tokenHash : '');\n const userId = remoteAccount.authenticated\n ? `user:${remoteIdentity}`\n : tokenHash\n ? `token:${tokenHash}`\n : `anon:${anonymousHash}`;\n return {\n userId,\n username: remoteAccount.username || usernameHint,\n tokenHash,\n isAuthenticated: remoteAccount.authenticated,\n isMember: remoteAccount.isMember,\n membership: remoteAccount.membership,\n };\n}\n\nfunction base64UrlJson(value) {\n return Buffer.from(JSON.stringify(value), 'utf8').toString('base64url');\n}\n\nfunction timingSafeEqualText(left, right) {\n const leftBuffer = Buffer.from(String(left || ''));\n const rightBuffer = Buffer.from(String(right || ''));\n if (leftBuffer.length !== rightBuffer.length) return false;\n return crypto.timingSafeEqual(leftBuffer, rightBuffer);\n}\n\nfunction signCodexAdapterPayload(encodedPayload) {\n return crypto\n .createHmac('sha256', codexAdapterSecret)\n .update(encodedPayload)\n .digest('base64url');\n}\n\nfunction createCodexAdapterToken(user, options = {}) {\n const requestedMaxCompletionTokens = Number(options.maxCompletionTokens || 0);\n const payload = {\n sub: user.userId,\n username: user.username || '',\n tokenHash: user.tokenHash || '',\n isAuthenticated: Boolean(user.isAuthenticated),\n isMember: Boolean(user.isMember),\n isTestAccount: Boolean(user.isTestAccount),\n membership: user.membership || {},\n aud: safeUserText(options.audience) || 'codex-responses',\n recordUsage: options.recordUsage !== false,\n maxCompletionTokens:\n Number.isFinite(requestedMaxCompletionTokens) && requestedMaxCompletionTokens > 0\n ? Math.floor(requestedMaxCompletionTokens)\n : null,\n exp: Date.now() + codexAdapterTokenTtlMs,\n };\n const encodedPayload = base64UrlJson(payload);\n return `${codexAdapterTokenPrefix}${encodedPayload}.${signCodexAdapterPayload(encodedPayload)}`;\n}\n\nfunction resolveCodexAdapterToken(token) {\n const raw = readText(token);\n if (!raw.startsWith(codexAdapterTokenPrefix)) return null;\n\n const signed = raw.slice(codexAdapterTokenPrefix.length);\n const separator = signed.lastIndexOf('.');\n if (separator < 1) return null;\n\n const encodedPayload = signed.slice(0, separator);\n const signature = signed.slice(separator + 1);\n const expectedSignature = signCodexAdapterPayload(encodedPayload);\n if (!timingSafeEqualText(signature, expectedSignature)) return null;\n\n let payload;\n try {\n payload = JSON.parse(Buffer.from(encodedPayload, 'base64url').toString('utf8'));\n } catch {\n return null;\n }\n\n if (!payload || typeof payload !== 'object') return null;\n if (Number(payload.exp || 0) < Date.now()) return null;\n\n const userId = safeUserText(payload.sub);\n if (!userId) return null;\n\n return {\n user: {\n userId,\n username: safeUserText(payload.username),\n tokenHash: safeUserText(payload.tokenHash),\n isAuthenticated: Boolean(payload.isAuthenticated),\n isMember: Boolean(payload.isMember),\n isTestAccount: Boolean(payload.isTestAccount),\n membership:\n payload.membership && typeof payload.membership === 'object'\n ? payload.membership\n : {},\n },\n audience: safeUserText(payload.aud) || 'codex-responses',\n recordUsage: payload.recordUsage !== false,\n maxCompletionTokens:\n Number.isFinite(Number(payload.maxCompletionTokens)) && Number(payload.maxCompletionTokens) > 0\n ? Math.floor(Number(payload.maxCompletionTokens))\n : null,\n };\n}\n\nasync function resolveCodexResponsesBilling(req) {\n const token = bearerToken(req);\n const adapter = resolveCodexAdapterToken(token);\n if (adapter) return adapter;\n\n if (deepseekApiKey && token === deepseekApiKey) {\n const error = new Error('Codex DeepSeek adapter now requires first-party user billing');\n error.statusCode = 401;\n throw error;\n }\n\n return {\n user: await resolveUser(req, req.body || {}),\n recordUsage: true,\n };\n}\n\nfunction usageFor(userId) {\n const month = monthKey();\n const row = statements.getUsage.get(userId, month);\n return {\n month,\n used: Number(row?.totalTokens || 0),\n };\n}\n\nfunction enforceTokenBudget(user, estimatedTokensForRequest) {\n if (user && user.isTestAccount) {\n return { limit: 999999999, usage: { month: monthKey(), used: 0 } };\n }\n if (requireMemberForAi && !user.isMember) {\n const error = new Error('大乘 AI 需要开通会员后使用');\n error.statusCode = 403;\n error.code = 'AI_MEMBERSHIP_REQUIRED';\n error.details = {\n requireMemberForAi,\n monthlyLimit: memberMonthlyLimit,\n remainingTokens: 0,\n };\n throw error;\n }\n\n const limit = user.isMember ? memberMonthlyLimit : freeMonthlyLimit;\n const usage = usageFor(user.userId);\n if (usage.used + estimatedTokensForRequest > limit) {\n const error = new Error('本月 AI token 额度已不足');\n error.statusCode = 429;\n error.code = 'USER_TOKEN_QUOTA_EXCEEDED';\n error.details = {\n monthlyLimit: limit,\n usedTokens: usage.used,\n remainingTokens: Math.max(0, limit - usage.used),\n };\n throw error;\n }\n return { limit, usage };\n}\n\nfunction completionBudgetFor(\n budget,\n estimatedPromptTokens,\n maximumCompletionTokens = deepseekMaxCompletionTokens,\n) {\n const remainingAfterPrompt = budget.limit - budget.usage.used - estimatedPromptTokens;\n return Math.max(1, Math.min(maximumCompletionTokens, Math.floor(remainingAfterPrompt)));\n}\n\nfunction recordUsage(userId, totalTokens) {\n if (!totalTokens || totalTokens < 1) return;\n statements.upsertUsage.run({\n userId,\n month: monthKey(),\n totalTokens,\n updatedAt: nowIso(),\n });\n}\n\nfunction titleFromMessage(message) {\n const compact = message.replace(/\\s+/g, ' ').trim();\n if (!compact) return '新对话';\n return compact.length > 22 ? `${compact.slice(0, 22)}...` : compact;\n}\n\nfunction normalizeMessages(rows) {\n return rows.map((row) => ({\n role: row.role,\n content: row.content,\n }));\n}\n\nfunction normalizeMaxCompletionTokens(\n value,\n maximumCompletionTokens = deepseekMaxCompletionTokens,\n) {\n const numeric = Number(value || maximumCompletionTokens);\n if (!Number.isFinite(numeric) || numeric < 1) return maximumCompletionTokens;\n return Math.max(1, Math.min(maximumCompletionTokens, Math.floor(numeric)));\n}\n\nfunction normalizeDeepSeekModelName(value) {\n const raw = readText(value || deepseekModel);\n if (!raw) return deepseekModel;\n const modelId = raw.includes('/') ? raw.split('/').pop() : raw;\n return modelId || deepseekModel;\n}\n\nasync function callDeepSeek(messages, options = {}) {\n if (!deepseekApiKey) {\n const error = new Error('DeepSeek API key is not configured');\n error.statusCode = 503;\n throw error;\n }\n\n const model = normalizeDeepSeekModelName(options.model);\n const response = await fetch(`${deepseekBaseUrl}/chat/completions`, {\n method: 'POST',\n headers: {\n Accept: 'application/json',\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${deepseekApiKey}`,\n },\n body: JSON.stringify({\n model,\n messages,\n temperature: 0.4,\n max_tokens: normalizeMaxCompletionTokens(\n options.maxCompletionTokens,\n options.maximumCompletionTokens || deepseekMaxCompletionTokens,\n ),\n stream: false,\n ...(options.tools?.length ? { tools: options.tools, tool_choice: 'auto' } : {}),\n }),\n signal: AbortSignal.timeout(60_000),\n });\n\n const bodyText = await response.text();\n let payload;\n try {\n payload = bodyText ? JSON.parse(bodyText) : {};\n } catch {\n payload = { error: bodyText };\n }\n\n if (!response.ok) {\n const upstreamMessage = payload?.error?.message || payload?.message || '';\n const friendlyMessage =\n response.status === 402 || /insufficient\\s+balance/i.test(upstreamMessage)\n ? '大乘 AI 服务额度暂不可用,请联系管理员充值 DeepSeek 账户后重试。'\n : upstreamMessage || `DeepSeek request failed: ${response.status}`;\n const error = new Error(friendlyMessage);\n error.statusCode = response.status;\n if (response.status === 402 || /insufficient\\s+balance/i.test(upstreamMessage)) {\n error.code = 'DEEPSEEK_PROVIDER_QUOTA';\n }\n throw error;\n }\n\n const upstreamMessage = payload?.choices?.[0]?.message || {};\n const message = typeof upstreamMessage.content === 'string'\n ? upstreamMessage.content.trim()\n : '';\n const toolCalls = Array.isArray(upstreamMessage.tool_calls)\n ? upstreamMessage.tool_calls\n : [];\n if (!message && toolCalls.length === 0) {\n const error = new Error('DeepSeek returned an empty response');\n error.statusCode = 502;\n throw error;\n }\n\n return {\n message,\n toolCalls,\n model,\n usage: {\n promptTokens: Number(payload?.usage?.prompt_tokens || 0),\n completionTokens: Number(payload?.usage?.completion_tokens || 0),\n totalTokens: Number(payload?.usage?.total_tokens || 0),\n },\n };\n}\n\nasync function callDeepSeekStream(messages, callbacks = {}) {\n if (!deepseekApiKey) {\n const error = new Error('DeepSeek API key is not configured');\n error.statusCode = 503;\n throw error;\n }\n\n const model = normalizeDeepSeekModelName(callbacks.model);\n const response = await fetch(`${deepseekBaseUrl}/chat/completions`, {\n method: 'POST',\n headers: {\n Accept: 'text/event-stream',\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${deepseekApiKey}`,\n },\n body: JSON.stringify({\n model,\n messages,\n temperature: 0.4,\n max_tokens: normalizeMaxCompletionTokens(\n callbacks.maxCompletionTokens,\n callbacks.maximumCompletionTokens || deepseekMaxCompletionTokens,\n ),\n stream: true,\n stream_options: { include_usage: true },\n }),\n signal: callbacks.signal || AbortSignal.timeout(90_000),\n });\n\n if (!response.ok) {\n const bodyText = await response.text();\n let payload;\n try {\n payload = bodyText ? JSON.parse(bodyText) : {};\n } catch {\n payload = { error: bodyText };\n }\n const upstreamMessage = payload?.error?.message || payload?.message || '';\n const friendlyMessage =\n response.status === 402 || /insufficient\\s+balance/i.test(upstreamMessage)\n ? '大乘 AI 服务额度暂不可用,请联系管理员充值 DeepSeek 账户后重试。'\n : upstreamMessage || `DeepSeek request failed: ${response.status}`;\n const error = new Error(friendlyMessage);\n error.statusCode = response.status;\n if (response.status === 402 || /insufficient\\s+balance/i.test(upstreamMessage)) {\n error.code = 'DEEPSEEK_PROVIDER_QUOTA';\n }\n throw error;\n }\n\n let buffer = '';\n let message = '';\n let usage = {\n promptTokens: 0,\n completionTokens: 0,\n totalTokens: 0,\n };\n const decoder = new TextDecoder();\n\n for await (const chunk of response.body) {\n buffer += decoder.decode(chunk, { stream: true });\n const lines = buffer.split(/\\r?\\n/);\n buffer = lines.pop() || '';\n\n for (const rawLine of lines) {\n const line = rawLine.trim();\n if (!line.startsWith('data:')) continue;\n const data = line.slice('data:'.length).trim();\n if (!data || data === '[DONE]') continue;\n\n let payload;\n try {\n payload = JSON.parse(data);\n } catch {\n continue;\n }\n\n const delta = payload?.choices?.[0]?.delta?.content || '';\n if (delta) {\n message += delta;\n callbacks.onToken?.(delta);\n }\n\n if (payload?.usage) {\n usage = {\n promptTokens: Number(payload.usage.prompt_tokens || usage.promptTokens || 0),\n completionTokens: Number(payload.usage.completion_tokens || usage.completionTokens || 0),\n totalTokens: Number(payload.usage.total_tokens || usage.totalTokens || 0),\n };\n }\n }\n }\n\n message = message.trim();\n if (!message) {\n const error = new Error('DeepSeek returned an empty response');\n error.statusCode = 502;\n throw error;\n }\n\n return { message, model, usage };\n}\n\nfunction createCodexDeepSeekRuntime(user = null, runtimeOptions = {}) {\n if (!enableCodexSdkChat || !deepseekApiKey) {\n return {\n enabled: false,\n provider: codexDeepSeekProviderId,\n reason: enableCodexSdkChat ? 'DeepSeek API key is not configured' : 'ENABLE_CODEX_SDK_CHAT is not true',\n };\n }\n\n const adapterToken = user\n ? createCodexAdapterToken(user, {\n maxCompletionTokens: runtimeOptions.maxCompletionTokens,\n })\n : '';\n return {\n enabled: true,\n provider: codexDeepSeekProviderId,\n options: {\n apiKey: adapterToken,\n baseUrl: codexResponsesBaseUrl,\n env: {\n ...process.env,\n DEEPSEEK_API_KEY: adapterToken,\n HOME: runtimeOptions.codexHomeDir || codexHomeDir,\n CODEX_HOME: runtimeOptions.codexHomeDir || codexHomeDir,\n TMPDIR: codexTempDir,\n XDG_RUNTIME_DIR: codexRuntimeDir,\n XDG_CACHE_HOME: path.join(codexHomeDir, '.cache'),\n XDG_CONFIG_HOME: path.join(codexHomeDir, '.config'),\n XDG_DATA_HOME: path.join(codexHomeDir, '.local', 'share'),\n PATH: process.env.PATH || '/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin',\n ...(runtimeOptions.env || {}),\n },\n config: {\n model_provider: codexDeepSeekProviderId,\n model_providers: {\n [codexDeepSeekProviderId]: {\n name: 'Dacheng DeepSeek Proxy',\n base_url: codexResponsesBaseUrl,\n env_key: 'DEEPSEEK_API_KEY',\n wire_api: 'responses',\n query_params: {},\n },\n },\n ...(runtimeOptions.config || {}),\n },\n },\n threadOptions: {\n model: deepseekModel,\n approvalPolicy: runtimeOptions.approvalPolicy || 'never',\n sandboxMode: runtimeOptions.sandboxMode || 'read-only',\n ...(runtimeOptions.workingDirectory\n ? { workingDirectory: runtimeOptions.workingDirectory }\n : {}),\n skipGitRepoCheck: true,\n networkAccessEnabled: runtimeOptions.networkAccessEnabled ?? true,\n webSearchMode: runtimeOptions.webSearchMode || 'disabled',\n },\n };\n}\n\nfunction codexPromptFromMessages(messages) {\n return messages\n .map((message) => {\n const role = String(message.role || 'user').toUpperCase();\n return `${role}:\\n${message.content}`;\n })\n .join('\\n\\n');\n}\n\nfunction responseContentText(content) {\n if (typeof content === 'string') return content;\n if (!Array.isArray(content)) return '';\n return content\n .map((part) => {\n if (typeof part === 'string') return part;\n if (!part || typeof part !== 'object') return '';\n return String(part.text || part.input_text || part.output_text || '');\n })\n .filter(Boolean)\n .join('\\n');\n}\n\nfunction codexResponsesPrompt(body) {\n const input = Array.isArray(body?.input) ? body.input : [];\n const userItems = input.filter((item) => item?.role === 'user');\n const source = userItems[userItems.length - 1] || input[input.length - 1];\n const prompt = responseContentText(source?.content).trim();\n if (prompt) return prompt;\n if (typeof body?.input === 'string') return body.input.trim();\n return '';\n}\n\nfunction writeResponsesEvent(res, event, data) {\n res.write(`event: ${event}\\n`);\n res.write(`data: ${JSON.stringify(data)}\\n\\n`);\n}\n\nfunction responseUsage(usage = {}) {\n return {\n input_tokens: Number(usage.promptTokens || usage.input_tokens || 0),\n cached_input_tokens: 0,\n output_tokens: Number(usage.completionTokens || usage.output_tokens || 0),\n reasoning_output_tokens: 0,\n total_tokens: Number(usage.totalTokens || usage.total_tokens || 0),\n };\n}\n\nfunction createLibreChatAgentRuntime() {\n if (!enableLibreChatAgentChat || !libreChatAgentApiKey || !libreChatAgentId) {\n return {\n enabled: false,\n provider: 'librechat-agent',\n reason: !enableLibreChatAgentChat\n ? 'ENABLE_LIBRECHAT_AGENT_CHAT is not true'\n : !libreChatAgentApiKey\n ? 'LIBRECHAT_AGENT_API_KEY is not configured'\n : 'LIBRECHAT_AGENT_ID is not configured',\n };\n }\n\n return {\n enabled: true,\n provider: 'librechat-agent',\n model: libreChatAgentId,\n chatCompletionsUrl: `${libreChatAgentsBaseUrl}/chat/completions`,\n };\n}\n\nfunction libreChatUsage(usage = {}) {\n return {\n promptTokens: Number(usage.prompt_tokens || usage.input_tokens || usage.promptTokens || 0),\n completionTokens: Number(usage.completion_tokens || usage.output_tokens || usage.completionTokens || 0),\n totalTokens: Number(usage.total_tokens || usage.totalTokens || 0),\n };\n}\n\nasync function callLibreChatAgent(messages) {\n const runtime = createLibreChatAgentRuntime();\n if (!runtime.enabled) {\n const error = new Error(runtime.reason || 'LibreChat Agent chat is not enabled');\n error.statusCode = 503;\n throw error;\n }\n\n const response = await fetch(runtime.chatCompletionsUrl, {\n method: 'POST',\n headers: {\n Accept: 'application/json',\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${libreChatAgentApiKey}`,\n },\n body: JSON.stringify({\n model: runtime.model,\n messages,\n temperature: 0.4,\n stream: false,\n }),\n signal: AbortSignal.timeout(120_000),\n });\n\n const bodyText = await response.text();\n let payload;\n try {\n payload = bodyText ? JSON.parse(bodyText) : {};\n } catch {\n payload = { error: bodyText };\n }\n\n if (!response.ok) {\n const upstreamMessage = payload?.error?.message || payload?.message || bodyText || '';\n const error = new Error(upstreamMessage || `LibreChat Agent request failed: ${response.status}`);\n error.statusCode = response.status;\n throw error;\n }\n\n const message = payload?.choices?.[0]?.message?.content?.trim();\n if (!message) {\n const error = new Error('LibreChat Agent returned an empty response');\n error.statusCode = 502;\n throw error;\n }\n\n return {\n message,\n usage: libreChatUsage(payload?.usage),\n };\n}\n\nasync function callLibreChatAgentStream(messages, callbacks = {}) {\n const runtime = createLibreChatAgentRuntime();\n if (!runtime.enabled) {\n const error = new Error(runtime.reason || 'LibreChat Agent chat is not enabled');\n error.statusCode = 503;\n throw error;\n }\n\n const response = await fetch(runtime.chatCompletionsUrl, {\n method: 'POST',\n headers: {\n Accept: 'text/event-stream',\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${libreChatAgentApiKey}`,\n },\n body: JSON.stringify({\n model: runtime.model,\n messages,\n temperature: 0.4,\n stream: true,\n }),\n signal: callbacks.signal || AbortSignal.timeout(180_000),\n });\n\n if (!response.ok) {\n const bodyText = await response.text();\n let payload;\n try {\n payload = bodyText ? JSON.parse(bodyText) : {};\n } catch {\n payload = { error: bodyText };\n }\n const upstreamMessage = payload?.error?.message || payload?.message || bodyText || '';\n const error = new Error(upstreamMessage || `LibreChat Agent request failed: ${response.status}`);\n error.statusCode = response.status;\n throw error;\n }\n\n let buffer = '';\n let message = '';\n let usage = { promptTokens: 0, completionTokens: 0, totalTokens: 0 };\n const decoder = new TextDecoder();\n\n for await (const chunk of response.body) {\n buffer += decoder.decode(chunk, { stream: true });\n const lines = buffer.split(/\\r?\\n/);\n buffer = lines.pop() || '';\n\n for (const rawLine of lines) {\n const line = rawLine.trim();\n if (!line.startsWith('data:')) continue;\n const data = line.slice('data:'.length).trim();\n if (!data || data === '[DONE]') continue;\n\n let payload;\n try {\n payload = JSON.parse(data);\n } catch {\n continue;\n }\n\n const delta = payload?.choices?.[0]?.delta?.content || '';\n if (delta) {\n message += delta;\n callbacks.onToken?.(delta);\n }\n\n if (payload?.usage) {\n usage = libreChatUsage(payload.usage);\n }\n }\n }\n\n message = message.trim();\n if (!message) {\n const error = new Error('LibreChat Agent returned an empty response');\n error.statusCode = 502;\n throw error;\n }\n\n return { message, usage };\n}\n\nfunction isDeepSeekQuotaError(error) {\n if (error?.code === 'DEEPSEEK_PROVIDER_QUOTA') return true;\n if (error?.code === 'USER_TOKEN_QUOTA_EXCEEDED' || error?.code === 'AI_MEMBERSHIP_REQUIRED') return false;\n const message = String(error?.message || error || '');\n return /insufficient\\s+balance|balance/i.test(message);\n}\n\nfunction publicAiErrorMessage(error) {\n if (isDeepSeekQuotaError(error)) {\n return '大乘 AI 服务额度暂不可用,请联系管理员充值 DeepSeek 账户后重试。';\n }\n return error?.message || 'Internal Server Error';\n}\n\nfunction responsesPayload({\n responseId,\n itemId,\n status,\n model,\n text = '',\n usage = null,\n error = null,\n outputItems = null,\n}) {\n return {\n id: responseId,\n object: 'response',\n status,\n model,\n output: status === 'completed'\n ? outputItems || [\n {\n id: itemId,\n type: 'message',\n role: 'assistant',\n status: 'completed',\n content: [{ type: 'output_text', text }],\n },\n ]\n : [],\n ...(usage ? { usage: responseUsage(usage) } : {}),\n ...(error\n ? {\n error: {\n message: error.message || String(error),\n type: 'server_error',\n },\n }\n : {}),\n };\n}\n\nasync function callCodexSdkDeepSeek(messages, callbacks = {}) {\n const codexRuntime = createCodexDeepSeekRuntime(callbacks.user, {\n maxCompletionTokens: callbacks.maxCompletionTokens,\n workingDirectory: callbacks.workingDirectory,\n sandboxMode: callbacks.sandboxMode,\n approvalPolicy: callbacks.approvalPolicy,\n networkAccessEnabled: callbacks.networkAccessEnabled,\n webSearchMode: callbacks.webSearchMode,\n codexHomeDir: callbacks.codexHomeDir,\n env: callbacks.env,\n config: callbacks.config,\n });\n if (!codexRuntime.enabled) {\n const error = new Error(codexRuntime.reason || 'Codex SDK chat is not enabled');\n error.statusCode = 503;\n throw error;\n }\n if (!callbacks.user) {\n const error = new Error('Codex SDK DeepSeek proxy requires a resolved user for billing');\n error.statusCode = 401;\n throw error;\n }\n\n const { Codex } = await import('@openai/codex-sdk');\n const codex = new Codex(codexRuntime.options);\n const thread = callbacks.threadId\n ? codex.resumeThread(callbacks.threadId, codexRuntime.threadOptions)\n : codex.startThread(codexRuntime.threadOptions);\n const prompt = codexPromptFromMessages(messages);\n const turnOptions = {\n ...(callbacks.outputSchema ? { outputSchema: callbacks.outputSchema } : {}),\n ...(callbacks.signal ? { signal: callbacks.signal } : {}),\n };\n\n if (callbacks.onToken || callbacks.onStep) {\n const { events } = await thread.runStreamed(prompt, turnOptions);\n let finalResponse = '';\n let usage = null;\n let threadId = callbacks.threadId || null;\n\n for await (const event of events) {\n if (event.type === 'thread.started') {\n threadId = event.thread_id || threadId;\n } else if (event.type === 'item.completed') {\n const item = event.item;\n if (item.type === 'agent_message') {\n finalResponse = item.text || finalResponse;\n callbacks.onToken?.(item.text || '');\n } else if (item.type === 'todo_list') {\n callbacks.onStep?.({\n title: 'Codex SDK 执行计划',\n message: item.items.map((todo) => `${todo.completed ? '✓' : '•'} ${todo.text}`).join('\\n'),\n });\n } else if (item.type === 'command_execution') {\n callbacks.onStep?.({\n title: 'Codex SDK 执行命令',\n message: item.command,\n });\n } else if (item.type === 'file_change') {\n callbacks.onStep?.({\n title: 'Codex SDK 修改文件',\n message: item.changes.map((change) => `${change.kind}: ${change.path}`).join('\\n'),\n });\n } else if (item.type === 'mcp_tool_call') {\n callbacks.onStep?.({\n title: 'Codex SDK 调用工具',\n message: `${item.server}.${item.tool}`,\n });\n }\n } else if (event.type === 'turn.completed') {\n usage = event.usage;\n } else if (event.type === 'turn.failed') {\n throw new Error(event.error?.message || 'Codex SDK turn failed');\n } else if (event.type === 'error') {\n throw new Error(event.message || 'Codex SDK stream failed');\n }\n }\n\n if (!finalResponse.trim()) {\n const error = new Error('Codex SDK returned an empty response');\n error.statusCode = 502;\n throw error;\n }\n\n return {\n message: finalResponse.trim(),\n usage: {\n promptTokens: Number(usage?.input_tokens || 0),\n completionTokens: Number(usage?.output_tokens || 0),\n totalTokens: Number((usage?.input_tokens || 0) + (usage?.output_tokens || 0)),\n },\n usageAlreadyRecorded: true,\n threadId: thread.id || threadId,\n };\n }\n\n const turn = await thread.run(prompt, turnOptions);\n if (!turn.finalResponse?.trim()) {\n const error = new Error('Codex SDK returned an empty response');\n error.statusCode = 502;\n throw error;\n }\n\n return {\n message: turn.finalResponse.trim(),\n usage: {\n promptTokens: Number(turn.usage?.input_tokens || 0),\n completionTokens: Number(turn.usage?.output_tokens || 0),\n totalTokens: Number((turn.usage?.input_tokens || 0) + (turn.usage?.output_tokens || 0)),\n },\n usageAlreadyRecorded: true,\n threadId: thread.id || callbacks.threadId || null,\n };\n}\n\nconst activeAgentRuns = new Map();\n\nfunction normalizeAgentMode(value) {\n const mode = safeUserText(value || 'dharma_guide');\n return /^[a-z][a-z0-9_:-]{0,48}$/i.test(mode) ? mode : 'dharma_guide';\n}\n\nfunction openClawSessionKey(user, conversationId) {\n const env = process.env.NODE_ENV === 'production' ? 'prod' : 'dev';\n return `fabushi:env:${env}:user:${user.userId}:conversation:${conversationId}`;\n}\n\nfunction createOpenClawRuntime() {\n if (!enableOpenClawAgentChat || !openClawGatewayToken || !openClawGatewayUrl) {\n return {\n enabled: false,\n provider: 'openclaw-gateway',\n reason: !enableOpenClawAgentChat\n ? 'ENABLE_OPENCLAW_AGENT_CHAT is not true'\n : !openClawGatewayToken\n ? 'OPENCLAW_GATEWAY_TOKEN is not configured'\n : 'OPENCLAW_GATEWAY_URL is not configured',\n };\n }\n\n return {\n enabled: true,\n provider: 'openclaw-gateway',\n model: openClawAgentId,\n runsUrl: openClawRunsEndpoint || `${openClawGatewayUrl}/v1/agents/${encodeURIComponent(openClawAgentId)}/runs`,\n };\n}\n\nfunction extractOpenClawText(payload) {\n if (!payload || typeof payload !== 'object') return '';\n const candidates = [\n payload.message,\n payload.content,\n payload.output_text,\n payload.finalResponse,\n payload.final_response,\n payload.result?.message,\n payload.result?.content,\n payload.output?.message,\n payload.output?.content,\n payload.choices?.[0]?.message?.content,\n ];\n for (const candidate of candidates) {\n if (typeof candidate === 'string' && candidate.trim()) return candidate.trim();\n }\n return '';\n}\n\nfunction extractOpenClawUsage(payload) {\n const usage = payload?.usage || payload?.result?.usage || {};\n return {\n promptTokens: Number(usage.input_tokens || usage.prompt_tokens || usage.promptTokens || 0),\n completionTokens: Number(usage.output_tokens || usage.completion_tokens || usage.completionTokens || 0),\n totalTokens: Number(usage.total_tokens || usage.totalTokens || 0),\n };\n}\n\nasync function callOpenClawAgent({ messages, user, conversationId, mode, signal }) {\n const runtime = createOpenClawRuntime();\n if (!runtime.enabled) {\n const error = new Error(runtime.reason || 'OpenClaw Agent chat is not enabled');\n error.statusCode = 503;\n throw error;\n }\n\n const sessionKey = openClawSessionKey(user, conversationId);\n const response = await fetch(runtime.runsUrl, {\n method: 'POST',\n headers: {\n Accept: 'application/json',\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${openClawGatewayToken}`,\n },\n body: JSON.stringify({\n agentId: openClawAgentId,\n sessionKey,\n mode,\n stream: false,\n input: { messages },\n messages,\n metadata: {\n app: 'fabushi',\n userId: user.userId,\n conversationId,\n toolPolicy: 'public-user-read-mostly',\n },\n }),\n signal: signal || AbortSignal.timeout(180_000),\n });\n\n const bodyText = await response.text();\n let payload;\n try {\n payload = bodyText ? JSON.parse(bodyText) : {};\n } catch {\n payload = { message: bodyText };\n }\n\n if (!response.ok) {\n const error = new Error(payload?.error?.message || payload?.message || `OpenClaw request failed: ${response.status}`);\n error.statusCode = response.status;\n throw error;\n }\n\n const message = extractOpenClawText(payload);\n if (!message) {\n const error = new Error('OpenClaw returned an empty response');\n error.statusCode = 502;\n throw error;\n }\n\n return {\n message,\n runId: payload.runId || payload.id || payload.result?.runId || null,\n provider: runtime.provider,\n model: runtime.model,\n usage: extractOpenClawUsage(payload),\n };\n}\n\nasync function runAgentModel({\n messages,\n user,\n conversationId,\n mode,\n callbacks = {},\n signal,\n maxCompletionTokens,\n preferCodex = false,\n requireCodex = false,\n workingDirectory,\n sandboxMode,\n outputSchema,\n threadId,\n}) {\n const codexRuntime = createCodexDeepSeekRuntime(user, {\n workingDirectory,\n sandboxMode,\n maxCompletionTokens,\n });\n if (requireCodex && !codexRuntime.enabled) {\n const error = new Error(codexRuntime.reason || 'Codex SDK chat is not enabled');\n error.statusCode = 503;\n throw error;\n }\n\n let preferredCodexFailed = false;\n if ((preferCodex || requireCodex) && codexRuntime.enabled) {\n try {\n const result = await callCodexSdkDeepSeek(messages, {\n ...callbacks,\n user,\n signal,\n maxCompletionTokens,\n workingDirectory,\n sandboxMode,\n outputSchema,\n threadId,\n });\n return {\n ...result,\n provider: codexRuntime.provider,\n model: deepseekModel,\n };\n } catch (error) {\n if (requireCodex || isDeepSeekQuotaError(error)) throw error;\n logger.warn(\n { error: String(error), mode },\n 'Preferred Codex SDK model failed, continuing through agent fallbacks',\n );\n preferredCodexFailed = true;\n }\n }\n\n const openClawRuntime = createOpenClawRuntime();\n if (openClawRuntime.enabled && !callbacks.onToken) {\n return await callOpenClawAgent({ messages, user, conversationId, mode, signal });\n }\n\n const libreChatRuntime = createLibreChatAgentRuntime();\n let provider = libreChatRuntime.enabled\n ? libreChatRuntime.provider\n : codexRuntime.enabled && !preferredCodexFailed\n ? codexRuntime.provider\n : 'deepseek';\n let model = libreChatRuntime.enabled ? libreChatRuntime.model : deepseekModel;\n\n try {\n if (libreChatRuntime.enabled) {\n const result = callbacks.onToken\n ? await callLibreChatAgentStream(messages, { ...callbacks, signal })\n : await callLibreChatAgent(messages);\n return { ...result, provider, model };\n }\n if (codexRuntime.enabled && !preferredCodexFailed) {\n const result = await callCodexSdkDeepSeek(messages, {\n ...callbacks,\n user,\n signal,\n maxCompletionTokens,\n workingDirectory,\n sandboxMode,\n outputSchema,\n threadId,\n });\n return { ...result, provider, model };\n }\n const result = callbacks.onToken\n ? await callDeepSeekStream(messages, { ...callbacks, signal, maxCompletionTokens })\n : await callDeepSeek(messages, { maxCompletionTokens });\n return { ...result, provider, model };\n } catch (error) {\n if (!codexRuntime.enabled || preferredCodexFailed || isDeepSeekQuotaError(error)) throw error;\n logger.warn({ error: String(error) }, 'Agent model failed, falling back to DeepSeek direct');\n const result = callbacks.onToken\n ? await callDeepSeekStream(messages, { ...callbacks, signal, maxCompletionTokens })\n : await callDeepSeek(messages, { maxCompletionTokens });\n return { ...result, provider: 'deepseek', model: deepseekModel };\n }\n}\n\nfunction updateAgentRunStatus(run, status, fields = {}) {\n statements.updateAgentRunStatus.run({\n id: run.id,\n userId: run.user_id || run.userId,\n status,\n openClawRunId: fields.openClawRunId ?? null,\n inputTokens: fields.inputTokens ?? null,\n outputTokens: fields.outputTokens ?? null,\n toolCallCount: fields.toolCallCount ?? null,\n completedAt: status === 'completed' || status === 'cancelled' ? nowIso() : null,\n failedAt: status === 'failed' ? nowIso() : null,\n errorCode: fields.errorCode ?? null,\n errorMessage: fields.errorMessage ?? null,\n });\n}\n\nfunction buildAgentMessages(conversationId) {\n const historyRows = statements.listMessages.all(conversationId).slice(-14);\n return [{ role: 'system', content: systemPrompt }, ...normalizeMessages(historyRows)];\n}\n\nfunction agentUsagePayload(usage, user, budget) {\n const promptTokens = usage.promptTokens || usage.inputTokens || 0;\n const completionTokens = usage.completionTokens || usage.outputTokens || 0;\n const totalTokens = usage.totalTokens || promptTokens + completionTokens;\n const latestUsage = usageFor(user.userId);\n return {\n inputTokens: promptTokens,\n outputTokens: completionTokens,\n totalTokens,\n monthlyLimit: budget.limit,\n remainingTokens: Math.max(0, budget.limit - latestUsage.used),\n };\n}\n\nfunction openAiContentToText(content) {\n if (typeof content === 'string') return content;\n if (Array.isArray(content)) {\n return content\n .map((part) => {\n if (typeof part === 'string') return part;\n if (!part || typeof part !== 'object') return '';\n return readText(part.text || part.content || part.value);\n })\n .filter(Boolean)\n .join('\\n');\n }\n return readText(content);\n}\n\nfunction normalizeOpenAiMessages(value) {\n if (!Array.isArray(value)) return [];\n const allowedRoles = new Set(['system', 'user', 'assistant', 'tool']);\n return value\n .map((item) => {\n if (!item || typeof item !== 'object') return null;\n const role = allowedRoles.has(item.role) ? item.role : 'user';\n const content = openAiContentToText(item.content);\n if (!content) return null;\n return { role, content };\n })\n .filter(Boolean);\n}\n\nfunction truncateForDeepSeekMessage(text, maxChars = 12_000) {\n const value = readText(text);\n if (value.length <= maxChars) return value;\n return `${value.slice(0, maxChars)}\\n\\n[内容已截断]`;\n}\n\nfunction truncateTailForDeepSeekMessage(text, maxChars = 2400) {\n const value = readText(text);\n if (value.length <= maxChars) return value;\n return `[前文已省略]\\n\\n${value.slice(-maxChars)}`;\n}\n\nfunction stripOpenClawPromptNoise(text) {\n const value = readText(text);\n if (!value) return '';\n\n const finalPromptMatch = value.match(/finalPromptText[\"'\\s::]+([\\s\\S]{1,4000})$/i);\n if (finalPromptMatch?.[1]) {\n return readText(finalPromptMatch[1].replace(/^[\"'`]+|[\"'`,}\\]]+$/g, ''));\n }\n\n const markers = [\n 'final prompt:',\n 'final user prompt:',\n 'user prompt:',\n '用户问题:',\n '用户消息:',\n '用户输入:',\n ];\n const lower = value.toLowerCase();\n for (const marker of markers) {\n const index = lower.lastIndexOf(marker.toLowerCase());\n if (index >= 0) {\n const tail = value.slice(index + marker.length).trim();\n if (tail) return tail;\n }\n }\n\n return value;\n}\n\nfunction compactOpenClawMessagesForDeepSeek(messages) {\n const conversationalMessages = messages\n .filter((message) => message.role === 'user' || message.role === 'assistant')\n .map((message) => ({\n role: message.role,\n content: truncateTailForDeepSeekMessage(stripOpenClawPromptNoise(message.content)),\n }))\n .filter((message) => message.content);\n\n const recent = conversationalMessages.slice(-6);\n while (recent.length > 0 && recent[0].role !== 'user') {\n recent.shift();\n }\n\n if (recent.length === 0) {\n const fallback = messages\n .slice()\n .reverse()\n .map((message) => truncateTailForDeepSeekMessage(stripOpenClawPromptNoise(message.content), 2400))\n .find(Boolean);\n if (!fallback) return messages;\n recent.push({ role: 'user', content: fallback });\n }\n\n return [\n {\n role: 'system',\n content: '你是大乘 App 的桌面 AI 助手。请直接回答用户问题,默认使用简洁、准确的中文。',\n },\n ...recent,\n ];\n}\n\nfunction isDeepSeekBlockedError(error) {\n return (\n Number(error?.statusCode) === 403 &&\n /blocked|被拦截|forbidden/i.test(String(error?.message || error || ''))\n );\n}\n\nfunction shouldRetryWithCompactedOpenClawMessages(error, messages, compactedMessages) {\n if (!isDeepSeekBlockedError(error)) return false;\n return JSON.stringify(messages) !== JSON.stringify(compactedMessages);\n}\n\nfunction openClawBlockedFallbackText(messages) {\n const lastUser = messages\n .slice()\n .reverse()\n .find((message) => message.role === 'user')?.content || '';\n const localActionIntent =\n /(file|folder|directory|project|workspace|path|文件|文件夹|目录|项目|工程|路径|本地)/i.test(lastUser) &&\n /(create|make|mkdir|write|edit|modify|rename|delete|move|copy|save|download|read|list|生成|创建|新建|写入|编辑|修改|重命名|删除|移动|复制|保存|下载|读取|列出)/i.test(lastUser);\n\n if (localActionIntent) {\n return [\n '本机工具规划被模型服务拦截了,所以这次没有实际读取、创建、修改或下载本机文件。',\n '如果只是新建文件夹,请先在底部选择项目后重新发送“新建文件夹”;桌面端会直接在已选项目里创建。',\n '如果是更复杂的本地文件操作,请在“连应用”里重启本机 AI 后重试,或把要操作的文件路径和目标说得更具体。',\n ].join('\\n');\n }\n\n return '模型服务拦截了这次回复,未能生成结果。请换一种更具体、更短的表述后重试。';\n}\nfunction usageWithFallback(usage, messages, text) {\n const promptTokens = Number(usage?.promptTokens || usage?.inputTokens || 0);\n const completionTokens = Number(usage?.completionTokens || usage?.outputTokens || 0);\n const totalTokens = Number(usage?.totalTokens || 0);\n if (totalTokens > 0) {\n return {\n promptTokens,\n completionTokens,\n totalTokens,\n };\n }\n const estimatedPrompt = promptTokens || estimateTokens(JSON.stringify(messages));\n const estimatedCompletion = completionTokens || estimateTokens(text);\n return {\n promptTokens: estimatedPrompt,\n completionTokens: estimatedCompletion,\n totalTokens: estimatedPrompt + estimatedCompletion,\n };\n}\n\nfunction openAiUsagePayload(usage, user, budget) {\n const promptTokens = Number(usage.promptTokens || 0);\n const completionTokens = Number(usage.completionTokens || 0);\n const totalTokens = Number(usage.totalTokens || promptTokens + completionTokens);\n const latestUsage = usageFor(user.userId);\n const remainingTokens = Math.max(0, budget.limit - latestUsage.used);\n return {\n prompt_tokens: promptTokens,\n completion_tokens: completionTokens,\n total_tokens: totalTokens,\n promptTokens,\n completionTokens,\n totalTokens,\n monthlyLimit: budget.limit,\n remainingTokens,\n };\n}\n\nfunction writeOpenAiSse(res, payload) {\n res.write(`data: ${JSON.stringify(payload)}\\n\\n`);\n}\n\napp.post(\n '/api/openclaw/deepseek/v1/chat/completions',\n asyncHandler(async (req, res) => {\n const user = await resolveUser(req, req.body || {});\n const incomingMessages = normalizeOpenAiMessages(req.body?.messages);\n if (incomingMessages.length === 0) {\n res.status(400).json({ error: { message: 'messages is required' } });\n return;\n }\n\n const messages = compactOpenClawMessagesForDeepSeek(incomingMessages);\n const promptEstimate = estimateTokens(JSON.stringify(messages));\n const estimated = promptEstimate + 600;\n const budget = enforceTokenBudget(user, estimated);\n const requestCompletionLimit = deepseekMaxCompletionTokens;\n const maxCompletionTokens = Math.min(\n completionBudgetFor(budget, promptEstimate, requestCompletionLimit),\n normalizeMaxCompletionTokens(req.body?.max_tokens, requestCompletionLimit),\n );\n const model = normalizeDeepSeekModelName(req.body?.model);\n const wantsStream =\n req.body?.stream === true ||\n String(req.get('accept') || '').includes('text/event-stream');\n const id = `chatcmpl_${crypto.randomUUID().replaceAll('-', '')}`;\n const created = Math.floor(Date.now() / 1000);\n const compactedMessages = messages;\n\n if (!wantsStream) {\n let result;\n let usageMessages = messages;\n try {\n result = await callDeepSeek(messages, {\n model,\n maxCompletionTokens,\n maximumCompletionTokens: requestCompletionLimit,\n });\n } catch (error) {\n if (!shouldRetryWithCompactedOpenClawMessages(error, messages, compactedMessages)) {\n throw error;\n }\n logger.warn(\n {\n statusCode: error?.statusCode,\n messageCount: messages.length,\n compactedMessageCount: compactedMessages.length,\n },\n 'OpenClaw DeepSeek proxy retrying with compacted messages',\n );\n usageMessages = compactedMessages;\n result = await callDeepSeek(compactedMessages, {\n model,\n maxCompletionTokens,\n maximumCompletionTokens: requestCompletionLimit,\n });\n }\n const usage = usageWithFallback(result.usage, usageMessages, result.message);\n recordUsage(user.userId, usage.totalTokens);\n res.json({\n id,\n object: 'chat.completion',\n created,\n model: result.model || model,\n choices: [\n {\n index: 0,\n message: { role: 'assistant', content: result.message },\n finish_reason: 'stop',\n },\n ],\n usage: openAiUsagePayload(usage, user, budget),\n });\n return;\n }\n\n const abortController = new AbortController();\n req.on('close', () => abortController.abort());\n\n res.status(200);\n res.set({\n 'Content-Type': 'text/event-stream; charset=utf-8',\n 'Cache-Control': 'no-cache, no-transform',\n Connection: 'keep-alive',\n 'X-Accel-Buffering': 'no',\n });\n res.flushHeaders?.();\n\n const baseChunk = {\n id,\n object: 'chat.completion.chunk',\n created,\n model,\n system_fingerprint: 'fabushi-openclaw-deepseek-proxy',\n };\n writeOpenAiSse(res, {\n ...baseChunk,\n choices: [{ index: 0, delta: { role: 'assistant' }, finish_reason: null }],\n });\n\n let text = '';\n let usageMessages = messages;\n try {\n let result;\n try {\n result = await callDeepSeekStream(messages, {\n model,\n maxCompletionTokens,\n maximumCompletionTokens: requestCompletionLimit,\n signal: abortController.signal,\n onToken: (delta) => {\n text += delta;\n writeOpenAiSse(res, {\n ...baseChunk,\n choices: [{ index: 0, delta: { content: delta }, finish_reason: null }],\n });\n },\n });\n } catch (error) {\n if (!shouldRetryWithCompactedOpenClawMessages(error, messages, compactedMessages)) {\n throw error;\n }\n logger.warn(\n {\n statusCode: error?.statusCode,\n messageCount: messages.length,\n compactedMessageCount: compactedMessages.length,\n },\n 'OpenClaw DeepSeek proxy stream retrying with compacted messages',\n );\n text = '';\n usageMessages = compactedMessages;\n result = await callDeepSeekStream(compactedMessages, {\n model,\n maxCompletionTokens,\n maximumCompletionTokens: requestCompletionLimit,\n signal: abortController.signal,\n onToken: (delta) => {\n text += delta;\n writeOpenAiSse(res, {\n ...baseChunk,\n choices: [{ index: 0, delta: { content: delta }, finish_reason: null }],\n });\n },\n });\n }\n text = result.message || text;\n const usage = usageWithFallback(result.usage, usageMessages, text);\n recordUsage(user.userId, usage.totalTokens);\n writeOpenAiSse(res, {\n ...baseChunk,\n model: result.model || model,\n choices: [{ index: 0, delta: {}, finish_reason: 'stop' }],\n });\n writeOpenAiSse(res, {\n ...baseChunk,\n model: result.model || model,\n choices: [],\n usage: openAiUsagePayload(usage, user, budget),\n });\n res.write('data: [DONE]\\n\\n');\n } catch (error) {\n if (isDeepSeekBlockedError(error)) {\n const fallbackMessage = openClawBlockedFallbackText(usageMessages);\n const usage = usageWithFallback(null, usageMessages, fallbackMessage);\n recordUsage(user.userId, usage.totalTokens);\n logger.warn(\n { statusCode: error?.statusCode, code: error?.code },\n 'OpenClaw DeepSeek proxy stream blocked; returning local fallback message',\n );\n writeOpenAiSse(res, {\n ...baseChunk,\n choices: [{ index: 0, delta: { content: fallbackMessage }, finish_reason: null }],\n });\n writeOpenAiSse(res, {\n ...baseChunk,\n choices: [{ index: 0, delta: {}, finish_reason: 'stop' }],\n });\n writeOpenAiSse(res, {\n ...baseChunk,\n choices: [],\n usage: openAiUsagePayload(usage, user, budget),\n });\n res.write('data: [DONE]\\n\\n');\n return;\n }\n logger.error(\n { error: String(error), code: error?.code, statusCode: error?.statusCode },\n 'OpenClaw DeepSeek proxy stream failed',\n );\n writeOpenAiSse(res, {\n ...baseChunk,\n choices: [],\n error: {\n message: publicAiErrorMessage(error),\n code: error?.code || 'OPENCLAW_DEEPSEEK_PROXY_ERROR',\n },\n });\n res.write('data: [DONE]\\n\\n');\n } finally {\n res.end();\n }\n }),\n);\n\napp.post(\n '/codex-deepseek/v1/responses',\n asyncHandler(async (req, res) => {\n if (!deepseekApiKey) {\n res.status(401).json({\n error: { message: 'Unauthorized Codex DeepSeek adapter request' },\n });\n return;\n }\n\n const messages = codexResponsesMessages(req.body);\n if (messages.length === 0) {\n res.status(400).json({ error: { message: 'Responses input is required' } });\n return;\n }\n const { tools: responseTools, kinds: responseToolKinds } = codexResponsesTools(req.body);\n\n const {\n user,\n recordUsage: shouldRecordUsage,\n maxCompletionTokens: adapterMaxCompletionTokens,\n } = await resolveCodexResponsesBilling(req);\n const promptEstimate = estimateTokens(JSON.stringify({ messages, tools: responseTools }));\n const budget = enforceTokenBudget(user, promptEstimate + 600);\n const responseCompletionLimit = adapterMaxCompletionTokens || deepseekMaxCompletionTokens;\n const maxCompletionTokens = Math.min(\n completionBudgetFor(budget, promptEstimate, responseCompletionLimit),\n normalizeMaxCompletionTokens(\n req.body?.max_output_tokens || req.body?.max_tokens,\n responseCompletionLimit,\n ),\n );\n const model = normalizeDeepSeekModelName(req.body?.model);\n const responseId = `resp_${crypto.randomUUID().replaceAll('-', '')}`;\n const itemId = `msg_${crypto.randomUUID().replaceAll('-', '')}`;\n const wantsStream =\n req.body?.stream !== false ||\n String(req.get('accept') || '').includes('text/event-stream');\n\n if (!wantsStream) {\n const result = await callDeepSeek(messages, {\n model,\n maxCompletionTokens,\n maximumCompletionTokens: responseCompletionLimit,\n tools: responseTools,\n });\n const outputItems = deepSeekResultToResponseItems(\n result,\n responseToolKinds,\n (prefix) => `${prefix}_${crypto.randomUUID().replaceAll('-', '')}`,\n );\n const usage = usageWithFallback(\n result.usage,\n messages,\n result.message || JSON.stringify(result.toolCalls || []),\n );\n if (shouldRecordUsage) recordUsage(user.userId, usage.totalTokens);\n res.json(\n responsesPayload({\n responseId,\n itemId,\n status: 'completed',\n model: result.model || model,\n text: result.message,\n usage,\n outputItems,\n }),\n );\n return;\n }\n\n if (responseTools.length > 0) {\n res.status(200);\n res.set({\n 'Content-Type': 'text/event-stream; charset=utf-8',\n 'Cache-Control': 'no-cache, no-transform',\n Connection: 'keep-alive',\n 'X-Accel-Buffering': 'no',\n });\n res.flushHeaders?.();\n writeResponsesEvent(res, 'response.created', {\n type: 'response.created',\n response: responsesPayload({ responseId, itemId, status: 'in_progress', model }),\n });\n try {\n const result = await callDeepSeek(messages, {\n model,\n maxCompletionTokens,\n maximumCompletionTokens: responseCompletionLimit,\n tools: responseTools,\n });\n const outputItems = deepSeekResultToResponseItems(\n result,\n responseToolKinds,\n (prefix) => `${prefix}_${crypto.randomUUID().replaceAll('-', '')}`,\n );\n outputItems.forEach((item, outputIndex) => {\n const addedItem = item.type === 'message'\n ? { ...item, status: 'in_progress', content: [] }\n : { ...item, status: 'in_progress' };\n writeResponsesEvent(res, 'response.output_item.added', {\n type: 'response.output_item.added',\n output_index: outputIndex,\n item: addedItem,\n });\n if (item.type === 'message') {\n const text = item.content?.find((part) => part.type === 'output_text')?.text || '';\n if (text) {\n writeResponsesEvent(res, 'response.output_text.delta', {\n type: 'response.output_text.delta',\n item_id: item.id,\n output_index: outputIndex,\n content_index: 0,\n delta: text,\n });\n }\n }\n writeResponsesEvent(res, 'response.output_item.done', {\n type: 'response.output_item.done',\n output_index: outputIndex,\n item,\n });\n });\n const usage = usageWithFallback(\n result.usage,\n messages,\n result.message || JSON.stringify(result.toolCalls || []),\n );\n if (shouldRecordUsage) recordUsage(user.userId, usage.totalTokens);\n writeResponsesEvent(res, 'response.completed', {\n type: 'response.completed',\n response: responsesPayload({\n responseId,\n itemId,\n status: 'completed',\n model: result.model || model,\n text: result.message,\n usage,\n outputItems,\n }),\n });\n } catch (error) {\n writeResponsesEvent(res, 'response.failed', {\n type: 'response.failed',\n response: responsesPayload({\n responseId,\n itemId,\n status: 'failed',\n model,\n error,\n }),\n });\n }\n res.end();\n return;\n }\n\n res.status(200);\n res.set({\n 'Content-Type': 'text/event-stream; charset=utf-8',\n 'Cache-Control': 'no-cache, no-transform',\n Connection: 'keep-alive',\n 'X-Accel-Buffering': 'no',\n });\n res.flushHeaders?.();\n\n writeResponsesEvent(res, 'response.created', {\n type: 'response.created',\n response: responsesPayload({\n responseId,\n itemId,\n status: 'in_progress',\n model,\n }),\n });\n writeResponsesEvent(res, 'response.output_item.added', {\n type: 'response.output_item.added',\n output_index: 0,\n item: {\n id: itemId,\n type: 'message',\n role: 'assistant',\n status: 'in_progress',\n content: [],\n },\n });\n writeResponsesEvent(res, 'response.content_part.added', {\n type: 'response.content_part.added',\n item_id: itemId,\n output_index: 0,\n content_index: 0,\n part: { type: 'output_text', text: '' },\n });\n\n let text = '';\n try {\n const result = await callDeepSeekStream(messages, {\n model,\n maxCompletionTokens,\n maximumCompletionTokens: responseCompletionLimit,\n onToken: (delta) => {\n text += delta;\n writeResponsesEvent(res, 'response.output_text.delta', {\n type: 'response.output_text.delta',\n item_id: itemId,\n output_index: 0,\n content_index: 0,\n delta,\n });\n },\n });\n text = result.message || text;\n const usage = usageWithFallback(result.usage, messages, text);\n if (shouldRecordUsage) recordUsage(user.userId, usage.totalTokens);\n writeResponsesEvent(res, 'response.output_text.done', {\n type: 'response.output_text.done',\n item_id: itemId,\n output_index: 0,\n content_index: 0,\n text,\n });\n writeResponsesEvent(res, 'response.content_part.done', {\n type: 'response.content_part.done',\n item_id: itemId,\n output_index: 0,\n content_index: 0,\n part: { type: 'output_text', text },\n });\n writeResponsesEvent(res, 'response.output_item.done', {\n type: 'response.output_item.done',\n output_index: 0,\n item: {\n id: itemId,\n type: 'message',\n role: 'assistant',\n status: 'completed',\n content: [{ type: 'output_text', text }],\n },\n });\n writeResponsesEvent(res, 'response.completed', {\n type: 'response.completed',\n response: responsesPayload({\n responseId,\n itemId,\n status: 'completed',\n model: result.model || model,\n text,\n usage,\n }),\n });\n res.end();\n } catch (error) {\n writeResponsesEvent(res, 'response.failed', {\n type: 'response.failed',\n response: responsesPayload({\n responseId,\n itemId,\n status: 'failed',\n model,\n text,\n error,\n }),\n });\n res.end();\n }\n }),\n);\n\napp.use(\n '/api/openclaw/runtime/files',\n express.static(openClawRuntimeUpdatesDir, {\n fallthrough: false,\n immutable: true,\n maxAge: '7d',\n setHeaders(res) {\n res.setHeader('X-Content-Type-Options', 'nosniff');\n },\n }),\n);\n\napp.get(\n '/api/openclaw/runtime/manifest',\n asyncHandler(async (req, res) => {\n const platform = readText(req.query.platform);\n const currentVersion = readText(req.query.currentVersion);\n const appVersion = readText(req.query.appVersion);\n\n let manifest;\n try {\n manifest = await readJsonFile(openClawRuntimeManifestPath);\n } catch (error) {\n logger.info(\n { manifestPath: openClawRuntimeManifestPath, error: String(error) },\n 'OpenClaw runtime manifest unavailable',\n );\n return jsonResponse(res, 200, {\n success: true,\n schema: 1,\n updateAvailable: false,\n reason: 'manifest-unavailable',\n platform,\n currentVersion,\n appVersion,\n });\n }\n\n const latest = manifest.latest && typeof manifest.latest === 'object'\n ? manifest.latest\n : manifest;\n const platforms = latest.platforms && typeof latest.platforms === 'object'\n ? latest.platforms\n : {};\n const platformRelease = platform ? platforms[platform] : null;\n const version = readText(latest.version || manifest.version);\n\n if (!platform || !platformRelease || typeof platformRelease !== 'object') {\n return jsonResponse(res, 200, {\n success: true,\n schema: Number(manifest.schema || 1),\n channel: readText(manifest.channel || latest.channel || 'stable'),\n updateAvailable: false,\n reason: platform ? 'platform-unavailable' : 'platform-required',\n platform,\n currentVersion,\n appVersion,\n latestVersion: version,\n });\n }\n\n if (platform.startsWith('macos-') && !openClawRuntimeEnableMacosUpdates) {\n return jsonResponse(res, 200, {\n success: true,\n schema: Number(manifest.schema || 1),\n channel: readText(manifest.channel || latest.channel || 'stable'),\n updateAvailable: false,\n reason: 'macos-runtime-updates-disabled',\n platform,\n currentVersion,\n appVersion,\n latestVersion: version,\n });\n }\n\n const downloadUrl = absolutizeOpenClawRuntimeUrl(\n req,\n platformRelease.archiveUrl || platformRelease.url || platformRelease.fileName,\n );\n const release = {\n version,\n channel: readText(manifest.channel || latest.channel || 'stable'),\n minAppVersion: readText(latest.minAppVersion || platformRelease.minAppVersion),\n notes: readText(latest.notes || platformRelease.notes),\n nodeExecutable: readText(\n platformRelease.nodeExecutable || latest.nodeExecutable || 'node/bin/node',\n ),\n cliEntrypoint: readText(\n platformRelease.cliEntrypoint || latest.cliEntrypoint || 'openclaw/openclaw.mjs',\n ),\n defaultPort: Number(latest.defaultPort || manifest.defaultPort || 18789),\n defaultModel: readText(latest.defaultModel || manifest.defaultModel || 'openclaw/default'),\n defaultModelOverride: readText(latest.defaultModelOverride || manifest.defaultModelOverride),\n gatewayArgs: Array.isArray(latest.gatewayArgs)\n ? latest.gatewayArgs.map((item) => String(item))\n : ['gateway', '--port', '{port}', '--force'],\n archiveUrl: downloadUrl,\n sha256: readText(platformRelease.sha256),\n size: Number(platformRelease.size || 0),\n };\n\n jsonResponse(res, 200, {\n success: true,\n schema: Number(manifest.schema || 1),\n updateAvailable: Boolean(version && downloadUrl && version !== currentVersion),\n platform,\n currentVersion,\n appVersion,\n latest: release,\n generatedAt: readText(manifest.generatedAt || latest.generatedAt),\n });\n }),\n);\n\napp.get('/health', (_req, res) => {\n const codexRuntime = createCodexDeepSeekRuntime();\n const libreChatRuntime = createLibreChatAgentRuntime();\n const openClawRuntime = createOpenClawRuntime();\n jsonResponse(res, 200, {\n status: 'ok',\n service: 'dacheng-ai-backend',\n provider: libreChatRuntime.enabled ? libreChatRuntime.provider : 'deepseek',\n model: libreChatRuntime.enabled ? libreChatRuntime.model : deepseekModel,\n deepseekModel,\n deepseekAvailableModels,\n libreChatAgentChatEnabled: libreChatRuntime.enabled,\n libreChatAgentProvider: libreChatRuntime.provider,\n libreChatAgentReason: libreChatRuntime.enabled ? undefined : libreChatRuntime.reason,\n codexSdkAvailable: true,\n codexSdkChatEnabled: codexRuntime.enabled,\n codexSdkProvider: codexRuntime.provider,\n openClawAgentChatEnabled: openClawRuntime.enabled,\n openClawAgentProvider: openClawRuntime.provider,\n openClawAgentReason: openClawRuntime.enabled ? undefined : openClawRuntime.reason,\n requireMemberForAi,\n memberMonthlyLimit,\n freeMonthlyLimit,\n deepseekMaxCompletionTokens,\n botFatherMaxCompletionTokens,\n openClawRuntimeUpdatesDir,\n openClawRuntimeManifestConfigured: fs.existsSync(openClawRuntimeManifestPath),\n cbetaApiRoot,\n timestamp: nowIso(),\n });\n});\n\napp.get(\n '/v1/ai/usage',\n asyncHandler(async (req, res) => {\n const user = await resolveUser(req, {});\n const now = new Date();\n const windowStart = Math.floor(Date.UTC(\n now.getUTCFullYear(), now.getUTCMonth(), 1,\n ) / 1000);\n const windowEnd = Math.floor(Date.UTC(\n now.getUTCFullYear(), now.getUTCMonth() + 1, 1,\n ) / 1000);\n const tokenLimit = user.isTestAccount\n ? 999_999_999\n : user.isMember ? memberMonthlyLimit : freeMonthlyLimit;\n const usedTokens = user.isTestAccount ? 0 : usageFor(user.userId).used;\n jsonResponse(res, 200, {\n windowStart,\n windowEnd,\n tokenLimit,\n usedTokens,\n reservedTokens: 0,\n remainingTokens: Math.max(0, tokenLimit - usedTokens),\n });\n }),\n);\n\napp.get('/api/plugins/registry', (_req, res) => {\n jsonResponse(res, 200, {\n success: true,\n schemaVersion: 1,\n protocol: 'mcp',\n plugins: officialMcpApps,\n updatedAt: nowIso(),\n });\n});\n\napp.all('/api/mcp/apps/:pluginId', async (req, res) => {\n try {\n const user = await resolveUser(req, req.body || {});\n await handleOfficialMcpRequest(req.params.pluginId, req, res, user.userId);\n } catch (error) {\n logger.error({ error: error.stack || String(error) }, 'official MCP app request failed');\n if (!res.headersSent) {\n res.status(500).json({ error: 'MCP request failed', message: publicAiErrorMessage(error) });\n }\n }\n});\n\napp.post(\n '/api/codex/apps/:pluginId/turns',\n asyncHandler(async (req, res) => {\n const pluginId = safeUserText(req.params.pluginId).replace(/^official\\./, '');\n const plugin = officialMcpApps.find((candidate) => candidate.id === pluginId);\n if (!plugin) {\n return jsonResponse(res, 404, { success: false, message: 'plugin not found' });\n }\n\n const message = safeUserText(req.body?.message);\n if (!message) {\n return jsonResponse(res, 400, { success: false, message: 'message is required' });\n }\n\n const user = await resolveUser(req, req.body || {});\n const promptEstimate = estimateTokens(message);\n const budget = enforceTokenBudget(user, promptEstimate + 600);\n const maxCompletionTokens = completionBudgetFor(budget, promptEstimate);\n const namespace = pluginConversationNamespace(pluginId);\n const requestedConversationId = safeUserText(req.body?.conversationId);\n if (requestedConversationId && !requestedConversationId.startsWith(`${namespace}:`)) {\n return jsonResponse(res, 400, {\n success: false,\n message: 'conversation does not belong to the selected plugin namespace',\n });\n }\n\n const conversationId = requestedConversationId || `${namespace}:${crypto.randomUUID()}`;\n const createdAt = nowIso();\n let conversation = statements.getPluginConversation.get(\n conversationId,\n user.userId,\n namespace,\n );\n if (requestedConversationId && !conversation) {\n return jsonResponse(res, 404, {\n success: false,\n message: 'plugin conversation not found for this account',\n });\n }\n const isNewConversation = !conversation;\n if (isNewConversation) {\n conversation = {\n id: conversationId,\n user_id: user.userId,\n title: titleFromMessage(message),\n namespace,\n thread_id: null,\n };\n }\n\n const accountKey = sha256(user.userId).slice(0, 24);\n const pluginCodexHome = path.join(codexPluginSessionsDir, accountKey, pluginId);\n fs.mkdirSync(pluginCodexHome, { recursive: true });\n const mcpAccountToken = createCodexAdapterToken(user, {\n audience: 'mcp-plugin',\n recordUsage: false,\n });\n const mcpOrigin = (\n process.env.MCP_INTERNAL_BASE_URL || `http://127.0.0.1:${process.env.PORT || 8788}`\n ).replace(/\\/+$/, '');\n const isolationPolicy = createPluginCodexPolicy(pluginId, mcpOrigin);\n const firstTurnMessages = [\n {\n role: 'system',\n content:\n `你正在 ${plugin.title} 插件的隔离 Codex 会话中。` +\n '只能使用 current_plugin MCP Server 暴露的 Tools;禁止 shell、文件系统、Web 搜索和其他插件。' +\n '只读 Tool 可以按需调用。需要写入、外部操作或破坏性操作时,不要自行执行,明确告诉用户改用对应的 /Tool 命令,让宿主显示审批。',\n },\n { role: 'user', content: message },\n ];\n const aiResult = await callCodexSdkDeepSeek(\n conversation?.thread_id ? [{ role: 'user', content: message }] : firstTurnMessages,\n {\n user,\n threadId: conversation?.thread_id || undefined,\n maxCompletionTokens,\n sandboxMode: isolationPolicy.sandboxMode,\n approvalPolicy: isolationPolicy.approvalPolicy,\n networkAccessEnabled: isolationPolicy.networkAccessEnabled,\n webSearchMode: isolationPolicy.webSearchMode,\n codexHomeDir: pluginCodexHome,\n env: { [pluginMcpTokenEnv]: mcpAccountToken },\n config: isolationPolicy.config,\n },\n );\n const usage = {\n promptTokens: aiResult.usage.promptTokens || promptEstimate,\n completionTokens: aiResult.usage.completionTokens || estimateTokens(aiResult.message),\n totalTokens:\n aiResult.usage.totalTokens || promptEstimate + estimateTokens(aiResult.message),\n };\n if (!aiResult.usageAlreadyRecorded) recordUsage(user.userId, usage.totalTokens);\n\n const answeredAt = nowIso();\n if (isNewConversation) {\n statements.insertPluginConversation.run({\n id: conversationId,\n userId: user.userId,\n title: conversation.title,\n provider: codexDeepSeekProviderId,\n model: deepseekModel,\n namespace,\n createdAt,\n updatedAt: answeredAt,\n });\n }\n statements.insertMessage.run({\n id: crypto.randomUUID(),\n conversationId,\n role: 'user',\n content: message,\n promptTokens: 0,\n completionTokens: 0,\n totalTokens: 0,\n createdAt,\n });\n statements.insertMessage.run({\n id: crypto.randomUUID(),\n conversationId,\n role: 'assistant',\n content: aiResult.message,\n promptTokens: usage.promptTokens,\n completionTokens: usage.completionTokens,\n totalTokens: usage.totalTokens,\n createdAt: answeredAt,\n });\n statements.updatePluginConversation.run({\n id: conversationId,\n userId: user.userId,\n namespace,\n title: conversation.title,\n threadId: aiResult.threadId || conversation?.thread_id || null,\n updatedAt: answeredAt,\n });\n\n jsonResponse(res, 200, {\n success: true,\n pluginId,\n conversationId,\n message: aiResult.message,\n provider: codexDeepSeekProviderId,\n model: deepseekModel,\n usage: {\n ...usage,\n monthlyLimit: budget.limit,\n remainingTokens: Math.max(0, budget.limit - usageFor(user.userId).used),\n },\n });\n }),\n);\n\napp.get('/api/ai/models', (_req, res) => {\n jsonResponse(res, 200, {\n success: true,\n provider: 'deepseek',\n defaultModel: deepseekModel,\n models: deepseekAvailableModels.map((model) => ({\n id: model,\n label: model === 'deepseek-reasoner' ? 'DeepSeek Reasoner' : 'DeepSeek Chat',\n })),\n });\n});\napp.get(\n '/api/ai/quota',\n asyncHandler(async (req, res) => {\n const user = await resolveUser(req, { username: req.query.username });\n const budget = enforceTokenBudget(user, 0);\n const usage = usageFor(user.userId);\n jsonResponse(res, 200, {\n success: true,\n isMember: user.isMember,\n monthlyLimit: budget.limit,\n usedTokens: usage.used,\n remainingTokens: Math.max(0, budget.limit - usage.used),\n month: usage.month,\n });\n }),\n);\n\napp.post(\n '/api/agent/chat',\n asyncHandler(async (req, res) => {\n const message = safeUserText(req.body?.message);\n if (!message) {\n return jsonResponse(res, 400, { success: false, message: 'message is required' });\n }\n\n const user = await resolveUser(req, req.body || {});\n const promptEstimate = estimateTokens(message);\n const estimated = promptEstimate + 600;\n const budget = enforceTokenBudget(user, estimated);\n const maxCompletionTokens = completionBudgetFor(budget, promptEstimate);\n const createdAt = nowIso();\n const conversationId = safeUserText(req.body?.conversationId) || crypto.randomUUID();\n const mode = normalizeAgentMode(req.body?.mode);\n const stream = req.body?.stream === true;\n let conversation = statements.getConversation.get(conversationId, user.userId);\n const isNewConversation = !conversation;\n\n if (isNewConversation) {\n conversation = { id: conversationId, user_id: user.userId, title: titleFromMessage(message) };\n statements.insertConversation.run({\n id: conversationId,\n userId: user.userId,\n title: conversation.title,\n provider: 'agent',\n model: openClawAgentId,\n createdAt,\n updatedAt: createdAt,\n });\n }\n\n const messageId = safeUserText(req.body?.messageId) || crypto.randomUUID();\n statements.insertMessage.run({\n id: messageId,\n conversationId,\n role: 'user',\n content: message,\n promptTokens: 0,\n completionTokens: 0,\n totalTokens: 0,\n createdAt,\n });\n\n const runId = `run_${crypto.randomUUID().replaceAll('-', '')}`;\n const sessionKey = openClawSessionKey(user, conversationId);\n const openClawRuntime = createOpenClawRuntime();\n statements.insertAgentRun.run({\n id: runId,\n userId: user.userId,\n conversationId,\n messageId,\n openClawRunId: null,\n openClawSessionKey: sessionKey,\n status: stream ? 'queued' : 'running',\n mode,\n provider: openClawRuntime.enabled ? openClawRuntime.provider : 'dacheng-ai-fallback',\n model: openClawRuntime.enabled ? openClawAgentId : deepseekModel,\n startedAt: createdAt,\n });\n\n if (stream) {\n return jsonResponse(res, 200, {\n success: true,\n runId,\n conversationId,\n streamUrl: `/api/agent/runs/${runId}/events`,\n });\n }\n\n const controller = new AbortController();\n activeAgentRuns.set(runId, controller);\n try {\n const skillResult = await runResourceFinderSkill({ message, user });\n const skillContext = resourceContextMessage(skillResult);\n const historyRows = statements.listMessages.all(conversationId).slice(-14);\n const modelMessages = [\n { role: 'system', content: systemPrompt },\n ...(skillContext ? [{ role: 'system', content: skillContext }] : []),\n ...normalizeMessages(historyRows),\n ];\n\n const aiResult = await runAgentModel({\n messages: modelMessages,\n user,\n conversationId,\n mode,\n signal: controller.signal,\n maxCompletionTokens,\n });\n const usage = {\n promptTokens: aiResult.usage?.promptTokens || estimateTokens(JSON.stringify(modelMessages)),\n completionTokens: aiResult.usage?.completionTokens || estimateTokens(aiResult.message),\n totalTokens:\n aiResult.usage?.totalTokens ||\n estimateTokens(JSON.stringify(modelMessages)) + estimateTokens(aiResult.message),\n };\n if (!aiResult.usageAlreadyRecorded) recordUsage(user.userId, usage.totalTokens);\n const answeredAt = nowIso();\n const assistantMessageId = crypto.randomUUID();\n statements.insertMessage.run({\n id: assistantMessageId,\n conversationId,\n role: 'assistant',\n content: aiResult.message,\n promptTokens: usage.promptTokens,\n completionTokens: usage.completionTokens,\n totalTokens: usage.totalTokens,\n createdAt: answeredAt,\n });\n statements.updateConversation.run({\n id: conversationId,\n userId: user.userId,\n title: isNewConversation ? titleFromMessage(message) : conversation.title,\n updatedAt: answeredAt,\n });\n updateAgentRunStatus({ id: runId, user_id: user.userId }, 'completed', {\n openClawRunId: aiResult.runId,\n inputTokens: usage.promptTokens,\n outputTokens: usage.completionTokens,\n });\n const action = determineClientAction(skillResult, message);\n return jsonResponse(res, 200, {\n success: true,\n runId,\n conversationId,\n messageId: assistantMessageId,\n provider: aiResult.provider,\n model: aiResult.model,\n message: aiResult.message,\n usage: agentUsagePayload(usage, user, budget),\n ...(action ? { clientAction: action } : {}),\n });\n } catch (error) {\n const status = error.name === 'AbortError' ? 'cancelled' : 'failed';\n updateAgentRunStatus({ id: runId, user_id: user.userId }, status, {\n errorCode: error.name || 'AGENT_RUN_FAILED',\n errorMessage: publicAiErrorMessage(error),\n });\n if (status === 'cancelled') {\n return jsonResponse(res, 499, { success: false, runId, status: 'cancelled' });\n }\n throw error;\n } finally {\n activeAgentRuns.delete(runId);\n }\n }),\n);\n\napp.get(\n '/api/agent/runs/:runId/events',\n asyncHandler(async (req, res) => {\n const user = await resolveUser(req, {});\n const run = statements.getAgentRun.get(req.params.runId, user.userId);\n if (!run) {\n return jsonResponse(res, 404, { success: false, message: 'run not found' });\n }\n if (run.status === 'completed' || run.status === 'failed' || run.status === 'cancelled') {\n return jsonResponse(res, 409, { success: false, message: `run is already ${run.status}` });\n }\n\n sseHeaders(res);\n const controller = new AbortController();\n activeAgentRuns.set(run.id, controller);\n updateAgentRunStatus(run, 'running');\n writeSse(res, 'run.started', { runId: run.id, conversationId: run.conversation_id });\n\n try {\n let streamedText = '';\n const modelMessages = buildAgentMessages(run.conversation_id);\n const lastUserMsg = [...modelMessages].reverse().find(m => m.role === 'user');\n const userMsgText = lastUserMsg ? lastUserMsg.content : '';\n const skillResult = userMsgText ? await runResourceFinderSkill({\n message: userMsgText,\n user,\n onStep: (step) => writeSse(res, 'tool.call.completed', {\n tool: 'agent.step',\n displayName: step.title || 'Agent step',\n summary: step.message || '',\n }),\n }) : null;\n const skillContext = resourceContextMessage(skillResult);\n const finalMessages = skillContext ? [\n modelMessages[0],\n { role: 'system', content: skillContext },\n ...modelMessages.slice(1)\n ] : modelMessages;\n\n const promptEstimate = estimateTokens(JSON.stringify(finalMessages));\n const estimated = promptEstimate + 600;\n const budget = enforceTokenBudget(user, estimated);\n const maxCompletionTokens = completionBudgetFor(budget, promptEstimate);\n const aiResult = await runAgentModel({\n messages: finalMessages,\n user,\n conversationId: run.conversation_id,\n mode: run.mode,\n signal: controller.signal,\n maxCompletionTokens,\n callbacks: {\n onStep: (step) => writeSse(res, 'tool.call.completed', {\n tool: 'agent.step',\n displayName: step.title || 'Agent step',\n summary: step.message || '',\n }),\n onToken: (delta) => {\n streamedText += delta;\n writeSse(res, 'assistant.delta', { text: delta });\n },\n },\n });\n const message = aiResult.message || streamedText.trim();\n const usage = {\n promptTokens: aiResult.usage?.promptTokens || estimateTokens(JSON.stringify(finalMessages)),\n completionTokens: aiResult.usage?.completionTokens || estimateTokens(message),\n totalTokens:\n aiResult.usage?.totalTokens || estimateTokens(JSON.stringify(finalMessages)) + estimateTokens(message),\n };\n if (!aiResult.usageAlreadyRecorded) recordUsage(user.userId, usage.totalTokens);\n const assistantMessageId = crypto.randomUUID();\n const answeredAt = nowIso();\n statements.insertMessage.run({\n id: assistantMessageId,\n conversationId: run.conversation_id,\n role: 'assistant',\n content: message,\n promptTokens: usage.promptTokens,\n completionTokens: usage.completionTokens,\n totalTokens: usage.totalTokens,\n createdAt: answeredAt,\n });\n statements.updateConversation.run({\n id: run.conversation_id,\n userId: user.userId,\n title: titleFromMessage(message),\n updatedAt: answeredAt,\n });\n updateAgentRunStatus(run, 'completed', {\n openClawRunId: aiResult.runId,\n inputTokens: usage.promptTokens,\n outputTokens: usage.completionTokens,\n });\n const action = determineClientAction(skillResult, userMsgText);\n writeSse(res, 'assistant.message', { messageId: assistantMessageId, content: message });\n writeSse(res, 'run.completed', {\n usage: agentUsagePayload(usage, user, budget),\n ...(action ? { clientAction: action } : {}),\n });\n res.end();\n } catch (error) {\n const status = error.name === 'AbortError' ? 'cancelled' : 'failed';\n updateAgentRunStatus(run, status, {\n errorCode: error.name || 'AGENT_RUN_FAILED',\n errorMessage: publicAiErrorMessage(error),\n });\n writeSse(res, status === 'cancelled' ? 'run.cancelled' : 'run.failed', {\n runId: run.id,\n message: publicAiErrorMessage(error),\n });\n res.end();\n } finally {\n activeAgentRuns.delete(run.id);\n }\n }),\n);\n\napp.post(\n '/api/agent/runs/:runId/cancel',\n asyncHandler(async (req, res) => {\n const user = await resolveUser(req, req.body || {});\n const run = statements.getAgentRun.get(req.params.runId, user.userId);\n if (!run) {\n return jsonResponse(res, 404, { success: false, message: 'run not found' });\n }\n const controller = activeAgentRuns.get(run.id);\n if (controller) controller.abort();\n updateAgentRunStatus(run, 'cancelled');\n return jsonResponse(res, 200, { success: true, runId: run.id, status: 'cancelled' });\n }),\n);\n\napp.post(\n '/api/agent/messages/:messageId/feedback',\n asyncHandler(async (req, res) => {\n const user = await resolveUser(req, req.body || {});\n const rating = safeUserText(req.body?.rating);\n if (!['up', 'down'].includes(rating)) {\n return jsonResponse(res, 400, { success: false, message: 'rating must be up or down' });\n }\n statements.insertAgentFeedback.run({\n id: crypto.randomUUID(),\n messageId: req.params.messageId,\n userId: user.userId,\n rating,\n reason: safeUserText(req.body?.reason),\n comment: safeUserText(req.body?.comment),\n createdAt: nowIso(),\n });\n return jsonResponse(res, 200, { success: true });\n }),\n);\n\napp.post(\n '/api/ai/chat',\n asyncHandler(async (req, res) => {\n const message = safeUserText(req.body?.message);\n if (!message) {\n return jsonResponse(res, 400, { success: false, message: 'message is required' });\n }\n\n const user = await resolveUser(req, req.body || {});\n const promptEstimate = estimateTokens(message);\n const estimated = promptEstimate + 600;\n const budget = enforceTokenBudget(user, estimated);\n const maxCompletionTokens = completionBudgetFor(budget, promptEstimate);\n const createdAt = nowIso();\n const conversationId = safeUserText(req.body?.conversationId) || crypto.randomUUID();\n const requestedModel = normalizeDeepSeekModelName(req.body?.model);\n const clientContext = clientContextMessage(req.body?.client);\n let conversation = statements.getConversation.get(conversationId, user.userId);\n const isNewConversation = !conversation;\n\n if (isNewConversation) {\n conversation = {\n id: conversationId,\n user_id: user.userId,\n title: titleFromMessage(message),\n };\n statements.insertConversation.run({\n id: conversationId,\n userId: user.userId,\n title: conversation.title,\n provider: 'deepseek',\n model: requestedModel,\n createdAt,\n updatedAt: createdAt,\n });\n }\n\n statements.insertMessage.run({\n id: crypto.randomUUID(),\n conversationId,\n role: 'user',\n content: message,\n promptTokens: 0,\n completionTokens: 0,\n totalTokens: 0,\n createdAt,\n });\n\n const libreChatRuntime = createLibreChatAgentRuntime();\n const skillResult = await runResourceFinderSkill({ message, user });\n const skillContext = resourceContextMessage(skillResult);\n const historyRows = statements.listMessages.all(conversationId).slice(-14);\n const modelMessages = [\n { role: 'system', content: systemPrompt },\n ...(clientContext ? [{ role: 'system', content: clientContext }] : []),\n ...(skillContext ? [{ role: 'system', content: skillContext }] : []),\n ...normalizeMessages(historyRows),\n ];\n\n const openClawRuntime = createOpenClawRuntime();\n const codexRuntime = createCodexDeepSeekRuntime(user);\n let provider = openClawRuntime.enabled\n ? openClawRuntime.provider\n : libreChatRuntime.enabled\n ? libreChatRuntime.provider\n : 'deepseek';\n let responseModel = openClawRuntime.enabled\n ? openClawRuntime.model\n : libreChatRuntime.enabled\n ? libreChatRuntime.model\n : requestedModel;\n let aiResult;\n try {\n if (openClawRuntime.enabled) {\n aiResult = await callOpenClawAgent({\n messages: modelMessages,\n user,\n conversationId,\n mode: normalizeAgentMode(req.body?.mode),\n });\n provider = aiResult.provider || provider;\n responseModel = aiResult.model || responseModel;\n } else if (libreChatRuntime.enabled) {\n aiResult = await callLibreChatAgent(modelMessages);\n } else if (codexRuntime.enabled) {\n provider = codexRuntime.provider;\n aiResult = await callCodexSdkDeepSeek(modelMessages, { user, maxCompletionTokens });\n } else {\n aiResult = await callDeepSeek(modelMessages, { model: requestedModel, maxCompletionTokens });\n }\n } catch (error) {\n if (libreChatRuntime.enabled) {\n logger.warn({ error: String(error) }, 'LibreChat Agent chat failed, falling back to Codex SDK or DeepSeek direct');\n provider = codexRuntime.enabled ? codexRuntime.provider : 'deepseek';\n responseModel = requestedModel;\n if (codexRuntime.enabled) {\n try {\n aiResult = await callCodexSdkDeepSeek(modelMessages, { user, maxCompletionTokens });\n } catch (codexError) {\n if (isDeepSeekQuotaError(codexError)) throw codexError;\n logger.warn({ error: String(codexError) }, 'Codex SDK chat failed, falling back to DeepSeek direct');\n provider = 'deepseek';\n aiResult = await callDeepSeek(modelMessages, { model: requestedModel, maxCompletionTokens });\n }\n } else {\n aiResult = await callDeepSeek(modelMessages, { model: requestedModel, maxCompletionTokens });\n }\n } else {\n if (!codexRuntime.enabled || isDeepSeekQuotaError(error)) throw error;\n logger.warn({ error: String(error) }, 'Codex SDK chat failed, falling back to DeepSeek direct');\n provider = 'deepseek';\n responseModel = requestedModel;\n aiResult = await callDeepSeek(modelMessages, { model: requestedModel, maxCompletionTokens });\n }\n }\n const usage = {\n promptTokens: aiResult.usage.promptTokens || estimateTokens(JSON.stringify(modelMessages)),\n completionTokens: aiResult.usage.completionTokens || estimateTokens(aiResult.message),\n totalTokens:\n aiResult.usage.totalTokens ||\n estimateTokens(JSON.stringify(modelMessages)) + estimateTokens(aiResult.message),\n };\n if (!aiResult.usageAlreadyRecorded) recordUsage(user.userId, usage.totalTokens);\n\n const answeredAt = nowIso();\n statements.insertMessage.run({\n id: crypto.randomUUID(),\n conversationId,\n role: 'assistant',\n content: aiResult.message,\n promptTokens: usage.promptTokens,\n completionTokens: usage.completionTokens,\n totalTokens: usage.totalTokens,\n createdAt: answeredAt,\n });\n statements.updateConversation.run({\n id: conversationId,\n userId: user.userId,\n title: isNewConversation ? titleFromMessage(message) : conversation.title,\n updatedAt: answeredAt,\n });\n\n const latestUsage = usageFor(user.userId);\n const monthlyLimit = budget.limit;\n const action = determineClientAction(skillResult, message);\n jsonResponse(res, 200, {\n success: true,\n conversationId,\n provider,\n model: responseModel,\n message: aiResult.message,\n usage: {\n ...usage,\n monthlyLimit,\n remainingTokens: Math.max(0, monthlyLimit - latestUsage.used),\n },\n ...(action ? { clientAction: action } : {}),\n });\n }),\n);\n\napp.post('/api/ai/chat/stream', async (req, res) => {\n sseHeaders(res);\n\n try {\n const message = safeUserText(req.body?.message);\n if (!message) {\n writeSse(res, 'error', { message: 'message is required' });\n res.end();\n return;\n }\n\n const user = await resolveUser(req, req.body || {});\n const promptEstimate = estimateTokens(message);\n const estimated = promptEstimate + 600;\n const budget = enforceTokenBudget(user, estimated);\n const maxCompletionTokens = completionBudgetFor(budget, promptEstimate);\n const createdAt = nowIso();\n const conversationId = safeUserText(req.body?.conversationId) || crypto.randomUUID();\n const requestedModel = normalizeDeepSeekModelName(req.body?.model);\n const clientContext = clientContextMessage(req.body?.client);\n let conversation = statements.getConversation.get(conversationId, user.userId);\n const isNewConversation = !conversation;\n\n if (isNewConversation) {\n conversation = {\n id: conversationId,\n user_id: user.userId,\n title: titleFromMessage(message),\n };\n statements.insertConversation.run({\n id: conversationId,\n userId: user.userId,\n title: conversation.title,\n provider: 'deepseek',\n model: requestedModel,\n createdAt,\n updatedAt: createdAt,\n });\n }\n\n statements.insertMessage.run({\n id: crypto.randomUUID(),\n conversationId,\n role: 'user',\n content: message,\n promptTokens: 0,\n completionTokens: 0,\n totalTokens: 0,\n createdAt,\n });\n\n const codexRuntime = createCodexDeepSeekRuntime(user);\n const libreChatRuntime = createLibreChatAgentRuntime();\n const initialOpenClawRuntime = createOpenClawRuntime();\n let provider = initialOpenClawRuntime.enabled\n ? initialOpenClawRuntime.provider\n : libreChatRuntime.enabled\n ? libreChatRuntime.provider\n : codexRuntime.enabled\n ? codexRuntime.provider\n : 'deepseek';\n let responseModel = initialOpenClawRuntime.enabled\n ? initialOpenClawRuntime.model\n : libreChatRuntime.enabled\n ? libreChatRuntime.model\n : requestedModel;\n writeSse(res, 'meta', {\n conversationId,\n provider,\n model: responseModel,\n });\n const skillResult = await runResourceFinderSkill({\n message,\n user,\n onStep: (step) => writeSse(res, 'step', step),\n });\n const skillContext = resourceContextMessage(skillResult);\n const historyRows = statements.listMessages.all(conversationId).slice(-14);\n const modelMessages = [\n { role: 'system', content: systemPrompt },\n ...(clientContext ? [{ role: 'system', content: clientContext }] : []),\n ...(skillContext ? [{ role: 'system', content: skillContext }] : []),\n ...normalizeMessages(historyRows),\n ];\n\n let aiResult;\n const openClawRuntime = initialOpenClawRuntime;\n try {\n if (openClawRuntime.enabled) {\n aiResult = await callOpenClawAgent({\n messages: modelMessages,\n user,\n conversationId,\n mode: normalizeAgentMode(req.body?.mode),\n });\n provider = aiResult.provider || provider;\n responseModel = aiResult.model || responseModel;\n writeSse(res, 'delta', { text: aiResult.message });\n } else if (libreChatRuntime.enabled) {\n aiResult = await callLibreChatAgentStream(modelMessages, {\n onToken: (token) => writeSse(res, 'delta', { text: token }),\n });\n } else if (codexRuntime.enabled) {\n aiResult = await callCodexSdkDeepSeek(modelMessages, {\n user,\n maxCompletionTokens,\n onToken: (token) => writeSse(res, 'delta', { text: token }),\n onStep: (step) => writeSse(res, 'step', step),\n });\n } else {\n aiResult = await callDeepSeekStream(modelMessages, {\n model: requestedModel,\n maxCompletionTokens,\n onToken: (token) => writeSse(res, 'delta', { text: token }),\n });\n }\n } catch (error) {\n if (libreChatRuntime.enabled) {\n logger.warn({ error: String(error) }, 'LibreChat Agent streaming chat failed, falling back to Codex SDK or DeepSeek direct');\n provider = codexRuntime.enabled ? codexRuntime.provider : 'deepseek';\n responseModel = requestedModel;\n writeSse(res, 'meta', {\n conversationId,\n provider,\n model: responseModel,\n });\n if (codexRuntime.enabled) {\n try {\n aiResult = await callCodexSdkDeepSeek(modelMessages, {\n user,\n maxCompletionTokens,\n onToken: (token) => writeSse(res, 'delta', { text: token }),\n onStep: (step) => writeSse(res, 'step', step),\n });\n } catch (codexError) {\n if (isDeepSeekQuotaError(codexError)) throw codexError;\n logger.warn({ error: String(codexError) }, 'Codex SDK streaming chat failed, falling back to DeepSeek direct');\n provider = 'deepseek';\n responseModel = requestedModel;\n writeSse(res, 'meta', {\n conversationId,\n provider,\n model: responseModel,\n });\n aiResult = await callDeepSeekStream(modelMessages, {\n model: requestedModel,\n maxCompletionTokens,\n onToken: (token) => writeSse(res, 'delta', { text: token }),\n });\n }\n } else {\n aiResult = await callDeepSeekStream(modelMessages, {\n model: requestedModel,\n maxCompletionTokens,\n onToken: (token) => writeSse(res, 'delta', { text: token }),\n });\n }\n } else {\n if (!codexRuntime.enabled || isDeepSeekQuotaError(error)) throw error;\n logger.warn({ error: String(error) }, 'Codex SDK streaming chat failed, falling back to DeepSeek direct');\n provider = 'deepseek';\n responseModel = requestedModel;\n writeSse(res, 'meta', {\n conversationId,\n provider,\n model: responseModel,\n });\n aiResult = await callDeepSeekStream(modelMessages, {\n model: requestedModel,\n maxCompletionTokens,\n onToken: (token) => writeSse(res, 'delta', { text: token }),\n });\n }\n }\n\n const usage = {\n promptTokens: aiResult.usage.promptTokens || estimateTokens(JSON.stringify(modelMessages)),\n completionTokens: aiResult.usage.completionTokens || estimateTokens(aiResult.message),\n totalTokens:\n aiResult.usage.totalTokens ||\n estimateTokens(JSON.stringify(modelMessages)) + estimateTokens(aiResult.message),\n };\n if (!aiResult.usageAlreadyRecorded) recordUsage(user.userId, usage.totalTokens);\n\n const answeredAt = nowIso();\n statements.insertMessage.run({\n id: crypto.randomUUID(),\n conversationId,\n role: 'assistant',\n content: aiResult.message,\n promptTokens: usage.promptTokens,\n completionTokens: usage.completionTokens,\n totalTokens: usage.totalTokens,\n createdAt: answeredAt,\n });\n statements.updateConversation.run({\n id: conversationId,\n userId: user.userId,\n title: isNewConversation ? titleFromMessage(message) : conversation.title,\n updatedAt: answeredAt,\n });\n\n const latestUsage = usageFor(user.userId);\n const monthlyLimit = budget.limit;\n const action = determineClientAction(skillResult, message);\n writeSse(res, 'done', {\n conversationId,\n provider,\n model: responseModel,\n message: aiResult.message,\n usage: {\n ...usage,\n monthlyLimit,\n remainingTokens: Math.max(0, monthlyLimit - latestUsage.used),\n },\n ...(action ? { clientAction: action } : {}),\n });\n res.end();\n } catch (error) {\n logger.error({ error: error.stack || String(error) }, 'streaming chat failed');\n writeSse(res, 'error', {\n message: publicAiErrorMessage(error),\n details: error.details,\n });\n res.end();\n }\n});\n\napp.get(\n '/api/ai/conversations',\n asyncHandler(async (req, res) => {\n const user = await resolveUser(req, { username: req.query.username });\n const items = statements.listConversations.all(user.userId).map((item) => ({\n id: item.id,\n title: item.title,\n updatedAt: item.updatedAt,\n }));\n jsonResponse(res, 200, { success: true, items });\n }),\n);\n\napp.get(\n '/api/ai/conversations/:id',\n asyncHandler(async (req, res) => {\n const user = await resolveUser(req);\n const conversation = statements.getConversation.get(req.params.id, user.userId);\n if (!conversation) {\n return jsonResponse(res, 404, { success: false, message: 'conversation not found' });\n }\n const messages = statements.listMessages.all(req.params.id).map((item) => ({\n role: item.role,\n content: item.content,\n createdAt: item.createdAt,\n }));\n jsonResponse(res, 200, {\n success: true,\n conversation: {\n id: conversation.id,\n title: conversation.title,\n updatedAt: conversation.updated_at,\n },\n messages,\n });\n }),\n);\n\nfunction buildUrl(base, endpoint, params = {}) {\n const url = new URL(endpoint, `${base}/`);\n for (const [key, value] of Object.entries(params)) {\n if (Array.isArray(value)) {\n for (const item of value) {\n if (item !== undefined && item !== null && item !== '') {\n url.searchParams.append(key, String(item));\n }\n }\n } else if (value !== undefined && value !== null && value !== '') {\n url.searchParams.set(key, String(value));\n }\n }\n return url;\n}\n\nasync function fetchJson(url) {\n const response = await fetch(url, {\n headers: { Accept: 'application/json' },\n signal: AbortSignal.timeout(15_000),\n });\n if (!response.ok) {\n throw new Error(`Request failed: ${response.status}`);\n }\n return response.json();\n}\n\nfunction stripHtml(value) {\n return String(value || '')\n .replace(/<script[\\s\\S]*?<\\/script>/gi, ' ')\n .replace(/<style[\\s\\S]*?<\\/style>/gi, ' ')\n .replace(/<[^>]+>/g, ' ')\n .replace(/ /g, ' ')\n .replace(/</g, '<')\n .replace(/>/g, '>')\n .replace(/&/g, '&')\n .replace(/&#(\\d+);/g, (_match, code) => String.fromCharCode(Number(code)))\n .replace(/\\s+/g, ' ')\n .trim();\n}\n\nfunction cleanExtractedText(value) {\n let text = String(value || '')\n .replace(/\\b[A-Z]{1,3}\\d{2}n\\d{4,5}[a-z]?_p?\\d{4}[a-z]\\d{2}\\b/g, ' ')\n .replace(/\\uFFFD/g, '')\n .replace(/\\s+([,。;:!?、」』)》】])/g, '$1')\n .replace(/([「『《(【])\\s+/g, '$1');\n for (let i = 0; i < 3; i += 1) {\n text = text.replace(/([\\u3400-\\u9fff])\\s+([\\u3400-\\u9fff])/g, '$1$2');\n }\n return text.replace(/[ \\t]{2,}/g, ' ').trim();\n}\n\nfunction compactText(value, limit = 140) {\n const text = stripHtml(value);\n return text.length > limit ? `${text.slice(0, limit)}...` : text;\n}\n\nfunction collectSearchResults(data) {\n if (!data || typeof data !== 'object') return [];\n if (Array.isArray(data.results)) return data.results;\n if (Array.isArray(data?.results?.docs)) return data.results.docs;\n if (Array.isArray(data?.response?.docs)) return data.response.docs;\n if (Array.isArray(data.docs)) return data.docs;\n return [];\n}\n\nfunction firstText(item, keys) {\n for (const key of keys) {\n const value = item?.[key];\n if (Array.isArray(value)) {\n const first = value.map((entry) => safeUserText(String(entry))).find(Boolean);\n if (first) return first;\n }\n if (typeof value === 'string' && value.trim()) return value.trim();\n if (typeof value === 'number') return String(value);\n }\n return '';\n}\n\nfunction firstNumber(item, keys, fallback = undefined) {\n for (const key of keys) {\n const value = item?.[key];\n if (typeof value === 'number') return value;\n if (typeof value === 'string' && value.trim() && Number.isFinite(Number(value))) {\n return Number(value);\n }\n }\n return fallback;\n}\n\nasync function searchCbetaTitle(query, limit) {\n const url = buildUrl(cbetaApiRoot, 'search/title', { q: query, start: 0, rows: limit });\n const data = await fetchJson(url);\n return collectSearchResults(data).flatMap((item) => {\n const work = firstText(item, ['work', 'work_id', 'workId']);\n const title = firstText(item, ['content', 'title', 'work_title', 'workTitle']);\n if (!work || !title) return [];\n const juan = firstNumber(item, ['juan', 'juan_num', 'juanNum'], 1);\n return [\n {\n id: `cbeta-title-${work}-${juan}`,\n title,\n sourceName: 'CBETA 佛典',\n url: `cbeta:${work}:${juan}`,\n snippet: firstText(item, ['byline', 'creators', 'time_dynasty']) || '可下载经文正文并用于全球法布施。',\n resourceType: 'scripture',\n work,\n juan,\n },\n ];\n });\n}\n\nasync function searchCbetaContent(query, limit) {\n const endpoints = ['search/fulltext', 'search/content', 'search/all_in_one'];\n const settled = await Promise.allSettled(\n endpoints.map((endpoint) =>\n fetchJson(buildUrl(cbetaApiRoot, endpoint, { q: query, start: 0, rows: limit, field: 'content' })),\n ),\n );\n const results = [];\n for (const response of settled) {\n if (response.status !== 'fulfilled') continue;\n for (const item of collectSearchResults(response.value)) {\n const work = firstText(item, ['work', 'work_id', 'workId', 'sutra', 'id']);\n const snippet = compactText(firstText(item, ['kwic', 'highlight', 'snippet', 'content', 'text', 'body', 'p']));\n if (!work || !snippet) continue;\n const juan = firstNumber(item, ['juan', 'juan_num', 'juanNum', '卷'], 1);\n const title = firstText(item, ['title', 'work_title', 'workTitle', 'sutra_name', 'book', 'name']) || work;\n results.push({\n id: `cbeta-content-${work}-${juan}-${sha256(snippet).slice(0, 8)}`,\n title,\n sourceName: 'CBETA 佛典',\n url: `cbeta:${work}:${juan}`,\n snippet,\n resourceType: 'scripture',\n work,\n juan,\n });\n if (results.length >= limit) return results;\n }\n }\n return results;\n}\n\nfunction directUrlResource(query) {\n const url = safeUserText(query);\n let parsed;\n try {\n parsed = new URL(url);\n } catch {\n return null;\n }\n if (!['http:', 'https:'].includes(parsed.protocol)) return null;\n return {\n id: `url-${sha256(url).slice(0, 12)}`,\n title: parsed.hostname,\n sourceName: '网页资源',\n url,\n snippet: '直接下载并提取这个链接的可读正文。',\n resourceType: 'web',\n };\n}\n\nfunction looksLikeAudioQuery(query) {\n return /音乐|音樂|音频|音訊|mp3|audio|念诵|念誦|诵经|誦經|唱诵|唱誦|读诵|讀誦/.test(\n safeUserText(query).toLowerCase(),\n );\n}\n\nfunction archiveSearchTerm(query) {\n return (\n safeUserText(query)\n .replace(/下载|查找|寻找|找一?个|资源|音频|音訊|音乐|音樂|mp3|audio|念诵|念誦|诵经|誦經|唱诵|唱誦|读诵|讀誦/gi, ' ')\n .replace(/\\s+/g, ' ')\n .trim() || safeUserText(query)\n );\n}\n\nfunction solrPhrase(value) {\n return String(value).replace(/\\\\/g, '\\\\\\\\').replace(/\"/g, '\\\\\"');\n}\n\nasync function searchInternetArchiveAudio(query, limit) {\n if (!looksLikeAudioQuery(query)) return [];\n const term = archiveSearchTerm(query);\n if (!term) return [];\n const variants = Array.from(\n new Set([\n term,\n term.replaceAll('金刚经', '金剛經'),\n term.replaceAll('金剛經', '金刚经'),\n ].filter(Boolean)),\n );\n const phraseQuery = variants\n .flatMap((item) => [`title:\"${solrPhrase(item)}\"`, `description:\"${solrPhrase(item)}\"`])\n .join(' OR ');\n const url = buildUrl('https://archive.org', 'advancedsearch.php', {\n q: `(${phraseQuery}) AND mediatype:audio`,\n rows: Math.min(Math.max(limit, 1), 10),\n output: 'json',\n 'fl[]': ['identifier', 'title', 'description'],\n });\n const data = await fetchJson(url);\n const docs = Array.isArray(data?.response?.docs) ? data.response.docs : [];\n return docs\n .map((item) => {\n const identifier = firstText(item, ['identifier']);\n const title = firstText(item, ['title']) || identifier;\n if (!identifier || !title) return null;\n return {\n id: `ia-audio-${identifier}`,\n title,\n sourceName: 'Internet Archive 音频',\n url: `ia:${identifier}`,\n snippet: compactText(firstText(item, ['description'])) || '公开可访问的音频资源,将下载第一个可用 MP3 文件。',\n resourceType: 'audio',\n identifier,\n };\n })\n .filter(Boolean);\n}\n\nconst curatedCbetaWorks = [\n {\n work: 'T0235',\n juan: 1,\n title: '金剛般若波羅蜜經',\n keywords: ['金剛般若波羅蜜經', '金刚般若波罗蜜经', '金剛經', '金刚经'],\n snippet: '姚秦 鳩摩羅什譯。常用于读诵、共修与法布施的般若经典。',\n },\n {\n work: 'T0251',\n juan: 1,\n title: '般若波羅蜜多心經',\n keywords: ['般若波羅蜜多心經', '般若波罗蜜多心经', '心經', '心经'],\n snippet: '唐 玄奘譯。篇幅短,适合加入禅室功课本与日常持诵。',\n },\n {\n work: 'T0262',\n juan: 1,\n title: '妙法蓮華經',\n keywords: ['妙法蓮華經', '妙法莲华经', '法華經', '法华经'],\n snippet: '姚秦 鳩摩羅什譯。可按卷下载正文,用于长期功课与分享。',\n },\n {\n work: 'T0279',\n juan: 1,\n title: '大方廣佛華嚴經',\n keywords: ['大方廣佛華嚴經', '大方广佛华严经', '華嚴經', '华严经'],\n snippet: '唐 實叉難陀譯。可按卷下载正文,用于系统研读与法布施。',\n },\n {\n work: 'T0360',\n juan: 1,\n title: '佛說無量壽經',\n keywords: ['佛說無量壽經', '佛说无量寿经', '無量壽經', '无量寿经'],\n snippet: '净土经典。可下载正文,用于净土功课本与全球法布施。',\n },\n {\n work: 'T0365',\n juan: 1,\n title: '佛說觀無量壽佛經',\n keywords: ['佛說觀無量壽佛經', '佛说观无量寿佛经', '觀無量壽經', '观无量寿经'],\n snippet: '净土三经之一。适合查找下载后加入净土功课。',\n },\n {\n work: 'T0366',\n juan: 1,\n title: '佛說阿彌陀經',\n keywords: ['佛說阿彌陀經', '佛说阿弥陀经', '阿彌陀經', '阿弥陀经'],\n snippet: '姚秦 鳩摩羅什譯。篇幅适中,适合共修、持诵与分享。',\n },\n {\n work: 'T0475',\n juan: 1,\n title: '維摩詰所說經',\n keywords: ['維摩詰所說經', '维摩诘所说经', '維摩詰經', '维摩诘经'],\n snippet: '姚秦 鳩摩羅什譯。大乘经典,可按卷下载用于研读。',\n },\n {\n work: 'T0676',\n juan: 1,\n title: '解深密經',\n keywords: ['解深密經', '解深密经'],\n snippet: '唯识相关大乘经典。适合研读、整理笔记与法布施。',\n },\n {\n work: 'T1666',\n juan: 1,\n title: '大乘起信論',\n keywords: ['大乘起信論', '大乘起信论', '起信論', '起信论'],\n snippet: '大乘论典。适合下载后作为研习功课资料。',\n },\n];\n\nfunction localCbetaResults(query, limit) {\n const normalizedQuery = safeUserText(query).toLowerCase();\n if (!normalizedQuery) return [];\n return curatedCbetaWorks\n .filter((item) =>\n item.keywords.some((keyword) => {\n const normalizedKeyword = keyword.toLowerCase();\n return normalizedKeyword.includes(normalizedQuery) || normalizedQuery.includes(normalizedKeyword);\n }),\n )\n .slice(0, limit)\n .map((item) => ({\n id: `cbeta-curated-${item.work}-${item.juan}`,\n title: item.title,\n sourceName: 'CBETA 佛典',\n url: `cbeta:${item.work}:${item.juan}`,\n snippet: item.snippet,\n resourceType: 'scripture',\n work: item.work,\n juan: item.juan,\n }));\n}\n\nfunction uniqueResources(items, limit) {\n const seen = new Set();\n const results = [];\n for (const item of items) {\n const key = `${item.url}:${item.title}`;\n if (seen.has(key)) continue;\n seen.add(key);\n results.push(item);\n if (results.length >= limit) break;\n }\n return results;\n}\n\nasync function findResourceCandidates(query, limit) {\n const direct = directUrlResource(query);\n const results = [];\n if (direct) results.push(direct);\n try {\n results.push(...(await searchInternetArchiveAudio(query, limit)));\n } catch (error) {\n logger.warn({ error: String(error), query }, 'Internet Archive audio search skipped');\n }\n results.push(...localCbetaResults(query, limit));\n\n const [titleResults, contentResults] = await Promise.allSettled([\n searchCbetaTitle(query, limit),\n searchCbetaContent(query, limit),\n ]);\n if (titleResults.status === 'fulfilled') results.push(...titleResults.value);\n if (contentResults.status === 'fulfilled') results.push(...contentResults.value);\n\n return uniqueResources(results, limit);\n}\n\nfunction looksLikeResourceTask(message) {\n const text = safeUserText(message).toLowerCase();\n if (!text) return false;\n return /下载|查找|寻找|找一?个|资源|經文|经文|佛经|佛經|仪轨|儀軌|音乐|音樂|mp3|audio|念诵|念誦|诵经|誦經/.test(text);\n}\n\nfunction extractResourceQuery(message) {\n const text = safeUserText(message)\n .replace(/请|請|帮我|幫我|麻烦|麻煩|可以|能不能|需要|我要|想要/g, ' ')\n .replace(/下载|下載|查找|寻找|尋找|找一个|找一個|找|资源|資源|并|並|然后|然後|用于|用於|全球法布施|加入功课本|加入功課本/g, ' ')\n .replace(/\\s+/g, ' ')\n .trim();\n return text || safeUserText(message);\n}\n\nfunction resourceContextMessage(skillResult) {\n if (!skillResult) return '';\n const lines = [\n '本轮资源检索/下载已经由大乘 App 后端完成。',\n '不要再次调用 search_dharma_resources、download_dharma_resource 或其他资源 MCP 工具;请直接基于以下结果回答用户。',\n `后端已调用 skill: ${skillResult.skillName}`,\n `执行目标: ${skillResult.query}`,\n ];\n if (skillResult.selected) {\n lines.push(`候选资源: ${skillResult.selected.title}`);\n lines.push(`来源: ${skillResult.selected.sourceName}`);\n lines.push(`资源地址: ${skillResult.selected.url}`);\n }\n if (skillResult.downloaded) {\n lines.push(`下载标题: ${skillResult.downloaded.title}`);\n lines.push(`下载文件: ${skillResult.downloaded.fileName}`);\n const text = safeUserText(skillResult.downloaded.contentText);\n if (text) {\n lines.push('下载正文摘录:');\n lines.push(text.slice(0, 8000));\n }\n }\n return lines.join('\\n');\n}\n\nfunction determineClientAction(skillResult, message) {\n if (skillResult && skillResult.downloaded) {\n const text = safeUserText(message);\n if (/法布施|全球法布施|分享/.test(text)) {\n return {\n type: 'prepare_dharma_share_text',\n title: skillResult.downloaded.title,\n text: skillResult.downloaded.contentText || '',\n };\n }\n }\n return null;\n}\n\nasync function runResourceFinderSkill({ message, user, onStep }) {\n if (!looksLikeResourceTask(message)) return null;\n\n const skillName = 'resource-finder-downloader';\n const query = extractResourceQuery(message);\n const audioTask = looksLikeAudioQuery(message) || looksLikeAudioQuery(query);\n onStep?.({\n type: 'skill.step',\n skillName,\n stage: 'start',\n title: '调用资源查找下载 skill',\n message: `识别到资源任务: ${query}`,\n });\n\n onStep?.({\n type: 'skill.step',\n skillName,\n stage: 'search',\n title: '搜索可分享资源',\n message: audioTask\n ? '优先检索公开可访问音频资源,失败时再回落到佛典正文资源。'\n : '优先检索 CBETA 佛典正文接口,搜索索引不可用时使用内置经典映射。',\n });\n const candidates = await findResourceCandidates(query, 8);\n if (candidates.length === 0) {\n onStep?.({\n type: 'skill.step',\n skillName,\n stage: 'empty',\n title: '没有找到可下载资源',\n message: '后端没有找到明确可下载且可分享的资源。',\n });\n return {\n skillName,\n query,\n candidates: [],\n selected: null,\n downloaded: null,\n };\n }\n\n const selected = candidates[0];\n onStep?.({\n type: 'skill.step',\n skillName,\n stage: 'select',\n title: '选择可信候选资源',\n message: `${selected.title} / ${selected.sourceName}`,\n });\n\n onStep?.({\n type: 'skill.step',\n skillName,\n stage: 'download',\n title: '下载并提取资源',\n message: selected.url,\n });\n const cbeta = parseCbetaResource(selected);\n const internetArchive = parseInternetArchiveResource(selected);\n const downloaded = cbeta\n ? await downloadCbetaResource({ ...selected, ...cbeta })\n : internetArchive\n ? await downloadInternetArchiveAudio({ ...selected, ...internetArchive })\n : await downloadWebResource(selected);\n const persisted = persistDownloadedResource(user, downloaded);\n\n onStep?.({\n type: 'skill.step',\n skillName,\n stage: 'done',\n title: '资源已准备完成',\n message: downloaded.fileName,\n });\n\n return {\n skillName,\n query,\n candidates,\n selected,\n downloaded: {\n ...downloaded,\n downloadId: persisted.id,\n },\n };\n}\n\napp.post(\n '/api/resources/search',\n asyncHandler(async (req, res) => {\n const query = safeUserText(req.body?.query);\n const limit = Math.min(Math.max(Number(req.body?.limit || 12), 1), 20);\n if (query.length < 2) {\n return jsonResponse(res, 400, { success: false, message: 'query is required' });\n }\n\n const items = await findResourceCandidates(query, limit);\n\n jsonResponse(res, 200, {\n success: true,\n source: 'resource-index',\n items,\n });\n }),\n);\n\nfunction parseCbetaResource(input) {\n const work = safeUserText(input.work);\n const juan = Number(input.juan || 1);\n if (work) return { work, juan: Number.isFinite(juan) ? juan : 1 };\n\n const url = safeUserText(input.url);\n const match = /^cbeta:([^:]+):(\\d+)$/i.exec(url);\n if (!match) return null;\n return {\n work: match[1].toUpperCase(),\n juan: Number(match[2] || 1),\n };\n}\n\nfunction parseInternetArchiveResource(input) {\n const url = safeUserText(input.url);\n const match = /^ia:([^:]+)$/i.exec(url);\n const identifier = match?.[1] || safeUserText(input.identifier);\n if (!identifier) return null;\n return { identifier };\n}\n\nfunction safeFileName(value) {\n const normalized = String(value || 'dharma-resource')\n .replace(/[\\\\/:*?\"<>|]+/g, '-')\n .replace(/\\s+/g, '-')\n .replace(/-+/g, '-')\n .replace(/^-|-$/g, '')\n .slice(0, 80);\n return normalized || 'dharma-resource';\n}\n\nfunction extractCbetaHtml(data) {\n const first = Array.isArray(data?.results) ? data.results[0] : null;\n if (typeof first === 'string') return first;\n if (first && typeof first === 'object' && typeof first.html === 'string') return first.html;\n if (typeof data?.html === 'string') return data.html;\n return '';\n}\n\nasync function downloadCbetaResource(resource) {\n const url = buildUrl(cbetaApiRoot, 'juans', {\n work: resource.work,\n juan: resource.juan,\n work_info: 1,\n toc: 1,\n });\n const data = await fetchJson(url);\n const workInfo = data.work_info || {};\n const title = safeUserText(workInfo.title) || safeUserText(resource.title) || resource.work;\n const html = extractCbetaHtml(data);\n const text = cleanExtractedText(stripHtml(html));\n if (!text) throw new Error('CBETA resource returned empty content');\n return {\n title,\n sourceName: 'CBETA 佛典',\n url: `cbeta:${resource.work}:${resource.juan}`,\n contentText: text.slice(0, maxResourceTextChars),\n fileName: `${resource.work}_${resource.juan}_${safeFileName(title)}.txt`,\n };\n}\n\nasync function downloadInternetArchiveAudio(resource) {\n const identifier = safeUserText(resource.identifier);\n if (!identifier) throw new Error('Internet Archive identifier is required');\n const metadata = await fetchJson(`https://archive.org/metadata/${encodeURIComponent(identifier)}`);\n const title = safeUserText(metadata?.metadata?.title) || safeUserText(resource.title) || identifier;\n const files = Array.isArray(metadata?.files) ? metadata.files : [];\n const audioFile = files.find((file) => {\n const name = safeUserText(file?.name);\n const format = safeUserText(file?.format).toLowerCase();\n return /\\.(mp3|m4a|ogg)$/i.test(name) || format.includes('mp3');\n });\n if (!audioFile?.name) {\n throw new Error('Internet Archive audio item has no downloadable audio file');\n }\n const sourceFileName = audioFile.name;\n const fileUrl = `https://archive.org/download/${encodeURIComponent(identifier)}/${sourceFileName\n .split('/')\n .map((part) => encodeURIComponent(part))\n .join('/')}`;\n const response = await fetch(fileUrl, {\n headers: {\n Accept: 'audio/mpeg, audio/*;q=0.9, */*;q=0.5',\n 'User-Agent': 'DachengResourceDownloader/1.0 (+https://ombhrum.com)',\n },\n signal: AbortSignal.timeout(60_000),\n });\n if (!response.ok) {\n throw new Error(`Internet Archive audio download failed: ${response.status}`);\n }\n const binaryContent = Buffer.from(await response.arrayBuffer());\n if (!binaryContent.length) throw new Error('Internet Archive audio returned empty content');\n const sizeMb = (binaryContent.length / 1024 / 1024).toFixed(1);\n return {\n title,\n sourceName: 'Internet Archive 音频',\n url: fileUrl,\n contentText: `已下载音频资源: ${title}\\n文件名: ${sourceFileName}\\n大小: ${sizeMb} MB\\n来源: ${fileUrl}`,\n binaryContent,\n fileName: safeFileName(sourceFileName),\n };\n}\n\nasync function downloadWebResource(input) {\n const url = safeUserText(input.url);\n const parsed = new URL(url);\n if (!['http:', 'https:'].includes(parsed.protocol)) {\n throw new Error('Only http/https resources can be downloaded');\n }\n\n const response = await fetch(parsed, {\n headers: {\n Accept: 'text/html, text/plain, application/xhtml+xml, application/xml;q=0.8, */*;q=0.5',\n 'User-Agent': 'DachengResourceDownloader/1.0 (+https://ombhrum.com)',\n },\n signal: AbortSignal.timeout(25_000),\n });\n if (!response.ok) throw new Error(`Resource download failed: ${response.status}`);\n const contentType = response.headers.get('content-type') || '';\n const body = await response.text();\n const title = safeUserText(input.title) || parsed.hostname;\n const contentText = contentType.includes('html') ? cleanExtractedText(stripHtml(body)) : body.trim();\n if (!contentText) throw new Error('Resource returned empty content');\n return {\n title,\n sourceName: safeUserText(input.sourceName) || parsed.hostname,\n url,\n contentText: contentText.slice(0, maxResourceTextChars),\n fileName: `${safeFileName(title)}.txt`,\n };\n}\n\nfunction persistDownloadedResource(user, content) {\n const id = crypto.randomUUID();\n const fileName = `${id}-${safeFileName(content.fileName)}`;\n const filePath = path.join(resourcesDir, fileName);\n if (content.binaryContent) {\n fs.writeFileSync(filePath, content.binaryContent);\n } else {\n fs.writeFileSync(filePath, content.contentText, 'utf8');\n }\n statements.insertResourceDownload.run({\n id,\n userId: user.userId,\n title: content.title,\n sourceName: content.sourceName,\n sourceUrl: content.url,\n fileName: content.fileName,\n filePath,\n createdAt: nowIso(),\n });\n return { id, fileName, filePath };\n}\n\nfunction createLibreChatMcpServer() {\n const server = new McpServer({\n name: 'dacheng-ai-tools',\n version: '0.1.0',\n });\n\n server.registerTool(\n 'search_dharma_resources',\n {\n title: 'Search dharma resources',\n description:\n 'Search for legally shareable Buddhist scripture, text, web, or audio resources that can be prepared for Fabushi sharing.',\n inputSchema: {\n query: z.string().min(2).max(200),\n limit: z.number().int().min(1).max(12).optional(),\n },\n outputSchema: {\n query: z.string(),\n items: z.array(\n z.object({\n id: z.string(),\n title: z.string(),\n sourceName: z.string(),\n url: z.string(),\n snippet: z.string(),\n resourceType: z.string(),\n work: z.string().optional(),\n juan: z.number().optional(),\n }),\n ),\n },\n annotations: {\n readOnlyHint: true,\n destructiveHint: false,\n openWorldHint: true,\n },\n },\n async ({ query, limit }) => {\n const items = await findResourceCandidates(query, limit ?? 8);\n return textToolResult(`Found ${items.length} candidate resources for \"${query}\".`, {\n query,\n items,\n });\n },\n );\n\n server.registerTool(\n 'download_dharma_resource',\n {\n title: 'Download dharma resource',\n description:\n 'Download and extract a selected resource. Use the url/work/juan returned by search_dharma_resources whenever possible.',\n inputSchema: {\n url: z.string().min(1).max(1000),\n title: z.string().max(300).optional(),\n sourceName: z.string().max(120).optional(),\n work: z.string().max(40).optional(),\n juan: z.number().int().min(1).optional(),\n identifier: z.string().max(300).optional(),\n },\n outputSchema: {\n title: z.string(),\n sourceName: z.string(),\n url: z.string(),\n fileName: z.string(),\n downloadId: z.string(),\n contentText: z.string(),\n },\n annotations: {\n readOnlyHint: false,\n destructiveHint: false,\n openWorldHint: true,\n },\n },\n async (input) => {\n const user = { userId: 'librechat:mcp' };\n const cbeta = parseCbetaResource(input);\n const internetArchive = parseInternetArchiveResource(input);\n const content = cbeta\n ? await downloadCbetaResource({ ...input, ...cbeta })\n : internetArchive\n ? await downloadInternetArchiveAudio({ ...input, ...internetArchive })\n : await downloadWebResource(input);\n const persisted = persistDownloadedResource(user, content);\n const { binaryContent: _binaryContent, ...publicContent } = content;\n return textToolResult(`Downloaded ${publicContent.title} as ${publicContent.fileName}.`, {\n ...publicContent,\n downloadId: persisted.id,\n });\n },\n );\n\n server.registerTool(\n 'prepare_dharma_share_text',\n {\n title: 'Prepare dharma sharing text',\n description:\n 'Prepare a text payload for the Fabushi app to confirm and add to global dharma sharing. This returns a clientAction; it does not start sending.',\n inputSchema: {\n title: z.string().min(1).max(120),\n text: z.string().min(1).max(20000),\n },\n outputSchema: {\n clientAction: z.object({\n type: z.literal('prepare_dharma_share_text'),\n title: z.string(),\n text: z.string(),\n }),\n },\n annotations: {\n readOnlyHint: false,\n destructiveHint: false,\n openWorldHint: false,\n },\n },\n async ({ title, text }) =>\n textToolResult(`Prepared \"${title}\" for user confirmation in the Fabushi app.`, {\n clientAction: {\n type: 'prepare_dharma_share_text',\n title: safeUserText(title).slice(0, 120),\n text: String(text || '').trim().slice(0, 20000),\n },\n }),\n );\n\n server.registerTool(\n 'prepare_practice_book_item',\n {\n title: 'Prepare practice book item',\n description:\n 'Prepare a scripture, chant, or practice item for the Fabushi app practice book. This returns a clientAction; it does not mutate the app by itself.',\n inputSchema: {\n title: z.string().min(1).max(120),\n sourceName: z.string().max(120).optional(),\n text: z.string().min(1).max(20000),\n },\n outputSchema: {\n clientAction: z.object({\n type: z.literal('prepare_practice_book_item'),\n title: z.string(),\n sourceName: z.string(),\n text: z.string(),\n }),\n },\n annotations: {\n readOnlyHint: false,\n destructiveHint: false,\n openWorldHint: false,\n },\n },\n async ({ title, sourceName, text }) =>\n textToolResult(`Prepared \"${title}\" for the Fabushi practice book confirmation flow.`, {\n clientAction: {\n type: 'prepare_practice_book_item',\n title: safeUserText(title).slice(0, 120),\n sourceName: safeUserText(sourceName || '大乘 AI').slice(0, 120),\n text: String(text || '').trim().slice(0, 20000),\n },\n }),\n );\n\n return server;\n}\n\nasync function handleMcpRequest(req, res) {\n const server = createLibreChatMcpServer();\n const transport = new StreamableHTTPServerTransport({\n sessionIdGenerator: undefined,\n enableJsonResponse: true,\n });\n\n res.on('close', () => {\n transport.close();\n server.close();\n });\n\n await server.connect(transport);\n await transport.handleRequest(req, res, req.body);\n}\n\napp.all('/mcp', async (req, res) => {\n try {\n await handleMcpRequest(req, res);\n } catch (error) {\n logger.error({ error: error.stack || String(error) }, 'MCP request failed');\n if (!res.headersSent) {\n res.status(500).json({\n error: 'MCP request failed',\n message: publicAiErrorMessage(error),\n });\n }\n }\n});\n\napp.post(\n '/api/resources/download',\n asyncHandler(async (req, res) => {\n const user = await resolveUser(req, req.body || {});\n const cbeta = parseCbetaResource(req.body || {});\n const internetArchive = parseInternetArchiveResource(req.body || {});\n const content = cbeta\n ? await downloadCbetaResource({ ...req.body, ...cbeta })\n : internetArchive\n ? await downloadInternetArchiveAudio({ ...req.body, ...internetArchive })\n : await downloadWebResource(req.body || {});\n const persisted = persistDownloadedResource(user, content);\n const { binaryContent: _binaryContent, ...publicContent } = content;\n jsonResponse(res, 200, {\n success: true,\n downloadId: persisted.id,\n ...publicContent,\n });\n }),\n);\n\napp.post(\n '/api/codex/resource-task',\n asyncHandler(async (req, res) => {\n if (process.env.ENABLE_CODEX_SDK !== 'true') {\n return jsonResponse(res, 403, {\n success: false,\n message: 'Codex SDK task runner is disabled. Set ENABLE_CODEX_SDK=true on the server to enable it.',\n });\n }\n const prompt = safeUserText(req.body?.prompt);\n if (!prompt) {\n return jsonResponse(res, 400, { success: false, message: 'prompt is required' });\n }\n const user = await resolveUser(req, req.body || {});\n const { Codex } = await import('@openai/codex-sdk');\n const codexRuntime = createCodexDeepSeekRuntime(user);\n const codex = new Codex(codexRuntime.enabled ? codexRuntime.options : {});\n const thread = codex.startThread(\n codexRuntime.enabled\n ? codexRuntime.threadOptions\n : { skipGitRepoCheck: true, networkAccessEnabled: true },\n );\n const result = await thread.run(prompt);\n const usage = {\n promptTokens: Number(result.usage?.input_tokens || 0),\n completionTokens: Number(result.usage?.output_tokens || 0),\n totalTokens: Number((result.usage?.input_tokens || 0) + (result.usage?.output_tokens || 0)),\n };\n if (!codexRuntime.enabled) {\n recordUsage(user.userId, usage.totalTokens || estimateTokens(JSON.stringify(result)));\n }\n jsonResponse(res, 200, {\n success: true,\n threadId: thread.id,\n provider: codexRuntime.provider,\n model: deepseekModel,\n usage,\n result,\n });\n }),\n);\n\nregisterPlatformApi({ app, db, resolveUser, asyncHandler });\n\napp.use((error, _req, res, _next) => {\n const status = Number(error.statusCode || error.status || 500);\n const safeStatus = status >= 400 && status < 600 ? status : 500;\n logger.error({ error: error.stack || String(error) }, 'request failed');\n jsonResponse(res, safeStatus, {\n success: false,\n message: error.message || 'Internal Server Error',\n details: error.details,\n });\n});\n\nconst port = Number(process.env.PORT || 8788);\napp.listen(port, '0.0.0.0', () => {\n logger.info(\n {\n port,\n dbPath,\n dataDir,\n deepseekModel,\n cbetaApiRoot,\n },\n 'Dacheng AI backend started',\n );\n});\n"}, "87fdc3b90f5ef3ec5a885d446bfdd7786e9e9fdb": {"t": 1785757948.0852175, "n": 140382, "h": "3eb996b494cb76db", "c": "import crypto from 'node:crypto';\nimport fs from 'node:fs';\nimport path from 'node:path';\nimport process from 'node:process';\nimport { fileURLToPath } from 'node:url';\n\nimport { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';\nimport { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';\nimport Database from 'better-sqlite3';\nimport cors from 'cors';\nimport dotenv from 'dotenv';\nimport express from 'express';\nimport rateLimit from 'express-rate-limit';\nimport helmet from 'helmet';\nimport pino from 'pino';\nimport { z } from 'zod';\n\nimport { registerPlatformApi } from './platform_api.js';\nimport {\n createPluginCodexPolicy,\n pluginConversationNamespace,\n pluginMcpTokenEnv,\n} from './plugin_codex_policy.js';\nimport {\n handleOfficialMcpRequest,\n officialMcpApps,\n} from './official_mcp_apps.js';\n\ndotenv.config();\n\nconst __dirname = path.dirname(fileURLToPath(import.meta.url));\nconst rootDir = path.resolve(__dirname, '..');\nconst dataDir = process.env.DATA_DIR || path.join(rootDir, 'data');\nconst resourcesDir = path.join(dataDir, 'resources');\nconst openClawRuntimeUpdatesDir =\n process.env.OPENCLAW_RUNTIME_UPDATES_DIR || path.join(dataDir, 'openclaw-runtime');\nconst openClawRuntimeEnableMacosUpdates =\n process.env.OPENCLAW_RUNTIME_ENABLE_MACOS_UPDATES === 'true';\nconst codexHomeDir = process.env.CODEX_HOME || path.join(dataDir, 'codex-home');\nconst codexPluginSessionsDir = path.join(dataDir, 'codex-plugin-sessions');\nconst codexTempDir = process.env.CODEX_TMPDIR || path.join(dataDir, 'codex-tmp');\nconst codexRuntimeDir = process.env.XDG_RUNTIME_DIR || path.join(dataDir, 'codex-runtime');\nconst dbPath = process.env.SQLITE_PATH || path.join(dataDir, 'dacheng-ai.sqlite');\n\nfs.mkdirSync(resourcesDir, { recursive: true });\nfs.mkdirSync(openClawRuntimeUpdatesDir, { recursive: true });\nfs.mkdirSync(codexHomeDir, { recursive: true });\nfs.mkdirSync(codexPluginSessionsDir, { recursive: true });\nfs.mkdirSync(codexTempDir, { recursive: true });\nfs.mkdirSync(codexRuntimeDir, { recursive: true });\n\nconst logger = pino({\n level: process.env.LOG_LEVEL || 'info',\n});\n\nconst app = express();\napp.disable('x-powered-by');\napp.set('trust proxy', Number(process.env.TRUST_PROXY_HOPS || 1));\napp.use(helmet());\nconst corsOrigin =\n !process.env.CORS_ORIGIN || process.env.CORS_ORIGIN.trim() === '*'\n ? true\n : process.env.CORS_ORIGIN.split(',').map((item) => item.trim()).filter(Boolean);\napp.use(cors({\n origin: corsOrigin,\n credentials: true,\n exposedHeaders: ['mcp-session-id', 'mcp-protocol-version'],\n}));\napp.use(express.json({ limit: '1mb' }));\napp.use(\n rateLimit({\n windowMs: 60_000,\n limit: Number(process.env.RATE_LIMIT_PER_MINUTE || 90),\n standardHeaders: true,\n legacyHeaders: false,\n }),\n);\n\nconst db = new Database(dbPath);\ndb.pragma('journal_mode = WAL');\ndb.pragma('foreign_keys = ON');\n\ndb.exec(`\n CREATE TABLE IF NOT EXISTS conversations (\n id TEXT PRIMARY KEY,\n user_id TEXT NOT NULL,\n title TEXT NOT NULL,\n provider TEXT NOT NULL,\n model TEXT NOT NULL,\n created_at TEXT NOT NULL,\n updated_at TEXT NOT NULL\n );\n\n CREATE INDEX IF NOT EXISTS idx_conversations_user_updated\n ON conversations (user_id, updated_at DESC);\n\n CREATE TABLE IF NOT EXISTS messages (\n id TEXT PRIMARY KEY,\n conversation_id TEXT NOT NULL,\n role TEXT NOT NULL,\n content TEXT NOT NULL,\n prompt_tokens INTEGER NOT NULL DEFAULT 0,\n completion_tokens INTEGER NOT NULL DEFAULT 0,\n total_tokens INTEGER NOT NULL DEFAULT 0,\n created_at TEXT NOT NULL,\n FOREIGN KEY (conversation_id) REFERENCES conversations(id) ON DELETE CASCADE\n );\n\n CREATE INDEX IF NOT EXISTS idx_messages_conversation_created\n ON messages (conversation_id, created_at ASC);\n\n CREATE TABLE IF NOT EXISTS usage_months (\n user_id TEXT NOT NULL,\n month TEXT NOT NULL,\n total_tokens INTEGER NOT NULL DEFAULT 0,\n updated_at TEXT NOT NULL,\n PRIMARY KEY (user_id, month)\n );\n\n CREATE TABLE IF NOT EXISTS resource_downloads (\n id TEXT PRIMARY KEY,\n user_id TEXT NOT NULL,\n title TEXT NOT NULL,\n source_name TEXT NOT NULL,\n source_url TEXT NOT NULL,\n file_name TEXT NOT NULL,\n file_path TEXT NOT NULL,\n created_at TEXT NOT NULL\n );\n\n CREATE TABLE IF NOT EXISTS agent_runs (\n id TEXT PRIMARY KEY,\n user_id TEXT NOT NULL,\n conversation_id TEXT NOT NULL,\n message_id TEXT NOT NULL,\n openclaw_run_id TEXT,\n openclaw_session_key TEXT NOT NULL,\n status TEXT NOT NULL,\n mode TEXT NOT NULL,\n provider TEXT NOT NULL,\n model TEXT,\n input_tokens INTEGER NOT NULL DEFAULT 0,\n output_tokens INTEGER NOT NULL DEFAULT 0,\n tool_call_count INTEGER NOT NULL DEFAULT 0,\n cost_microusd INTEGER NOT NULL DEFAULT 0,\n started_at TEXT NOT NULL,\n completed_at TEXT,\n failed_at TEXT,\n error_code TEXT,\n error_message TEXT\n );\n\n CREATE INDEX IF NOT EXISTS idx_agent_runs_user_started\n ON agent_runs (user_id, started_at DESC);\n\n CREATE INDEX IF NOT EXISTS idx_agent_runs_status_started\n ON agent_runs (status, started_at DESC);\n\n CREATE TABLE IF NOT EXISTS agent_message_feedback (\n id TEXT PRIMARY KEY,\n message_id TEXT NOT NULL,\n user_id TEXT NOT NULL,\n rating TEXT NOT NULL,\n reason TEXT,\n comment TEXT,\n created_at TEXT NOT NULL\n );\n`);\n\nconst conversationColumns = new Set(\n db.prepare('PRAGMA table_info(conversations)').all().map((column) => column.name),\n);\nif (!conversationColumns.has('namespace')) {\n db.exec('ALTER TABLE conversations ADD COLUMN namespace TEXT');\n}\nif (!conversationColumns.has('thread_id')) {\n db.exec('ALTER TABLE conversations ADD COLUMN thread_id TEXT');\n}\ndb.exec(`\n CREATE INDEX IF NOT EXISTS idx_conversations_user_namespace_updated\n ON conversations (user_id, namespace, updated_at DESC)\n`);\n\nconst statements = {\n insertConversation: db.prepare(`\n INSERT INTO conversations (id, user_id, title, provider, model, created_at, updated_at)\n VALUES (@id, @userId, @title, @provider, @model, @createdAt, @updatedAt)\n `),\n updateConversation: db.prepare(`\n UPDATE conversations\n SET title = @title, updated_at = @updatedAt\n WHERE id = @id AND user_id = @userId\n `),\n getConversation: db.prepare(`\n SELECT * FROM conversations WHERE id = ? AND user_id = ? LIMIT 1\n `),\n listConversations: db.prepare(`\n SELECT id, title, updated_at AS updatedAt\n FROM conversations\n WHERE user_id = ? AND (namespace IS NULL OR namespace = '')\n ORDER BY updated_at DESC\n LIMIT 30\n `),\n insertPluginConversation: db.prepare(`\n INSERT INTO conversations (\n id, user_id, title, provider, model, namespace, thread_id, created_at, updated_at\n )\n VALUES (\n @id, @userId, @title, @provider, @model, @namespace, NULL, @createdAt, @updatedAt\n )\n `),\n getPluginConversation: db.prepare(`\n SELECT * FROM conversations\n WHERE id = ? AND user_id = ? AND namespace = ?\n LIMIT 1\n `),\n updatePluginConversation: db.prepare(`\n UPDATE conversations\n SET title = @title, thread_id = @threadId, updated_at = @updatedAt\n WHERE id = @id AND user_id = @userId AND namespace = @namespace\n `),\n insertMessage: db.prepare(`\n INSERT INTO messages (\n id, conversation_id, role, content,\n prompt_tokens, completion_tokens, total_tokens, created_at\n )\n VALUES (\n @id, @conversationId, @role, @content,\n @promptTokens, @completionTokens, @totalTokens, @createdAt\n )\n `),\n listMessages: db.prepare(`\n SELECT role, content, prompt_tokens AS promptTokens,\n completion_tokens AS completionTokens, total_tokens AS totalTokens,\n created_at AS createdAt\n FROM messages\n WHERE conversation_id = ?\n ORDER BY created_at ASC\n `),\n getUsage: db.prepare(`\n SELECT total_tokens AS totalTokens FROM usage_months WHERE user_id = ? AND month = ?\n `),\n upsertUsage: db.prepare(`\n INSERT INTO usage_months (user_id, month, total_tokens, updated_at)\n VALUES (@userId, @month, @totalTokens, @updatedAt)\n ON CONFLICT(user_id, month)\n DO UPDATE SET total_tokens = usage_months.total_tokens + excluded.total_tokens,\n updated_at = excluded.updated_at\n `),\n insertResourceDownload: db.prepare(`\n INSERT INTO resource_downloads (\n id, user_id, title, source_name, source_url, file_name, file_path, created_at\n )\n VALUES (\n @id, @userId, @title, @sourceName, @sourceUrl, @fileName, @filePath, @createdAt\n )\n `),\n insertAgentRun: db.prepare(`\n INSERT INTO agent_runs (\n id, user_id, conversation_id, message_id, openclaw_run_id, openclaw_session_key,\n status, mode, provider, model, started_at\n )\n VALUES (\n @id, @userId, @conversationId, @messageId, @openClawRunId, @openClawSessionKey,\n @status, @mode, @provider, @model, @startedAt\n )\n `),\n getAgentRun: db.prepare(`\n SELECT * FROM agent_runs WHERE id = ? AND user_id = ? LIMIT 1\n `),\n updateAgentRunStatus: db.prepare(`\n UPDATE agent_runs\n SET status = @status,\n openclaw_run_id = COALESCE(@openClawRunId, openclaw_run_id),\n input_tokens = COALESCE(@inputTokens, input_tokens),\n output_tokens = COALESCE(@outputTokens, output_tokens),\n tool_call_count = COALESCE(@toolCallCount, tool_call_count),\n completed_at = @completedAt,\n failed_at = @failedAt,\n error_code = @errorCode,\n error_message = @errorMessage\n WHERE id = @id AND user_id = @userId\n `),\n insertAgentFeedback: db.prepare(`\n INSERT INTO agent_message_feedback (id, message_id, user_id, rating, reason, comment, created_at)\n VALUES (@id, @messageId, @userId, @rating, @reason, @comment, @createdAt)\n `),\n};\n\nconst deepseekApiKey = process.env.DEEPSEEK_API_KEY || '';\nconst deepseekBaseUrl = (process.env.DEEPSEEK_BASE_URL || 'https://api.deepseek.com').replace(/\\/+$/, '');\nconst deepseekModel = process.env.DEEPSEEK_MODEL || 'deepseek-chat';\nconst deepseekAvailableModels = Array.from(\n new Set(\n (process.env.DEEPSEEK_MODELS || `${deepseekModel},deepseek-chat,deepseek-reasoner`)\n .split(',')\n .map((item) => normalizeDeepSeekModelName(item))\n .filter(Boolean),\n ),\n);\nconst cbetaApiRoot = (process.env.CBETA_API_ROOT || 'https://api.ombhrum.com/api/cbeta').replace(/\\/+$/, '');\nconst fabushiApiBaseUrl = (process.env.FABUSHI_API_BASE_URL || 'https://api.ombhrum.com').replace(/\\/+$/, '');\nconst memberMonthlyLimit = Number(process.env.MEMBER_MONTHLY_TOKEN_LIMIT || 1_000_000);\nconst freeMonthlyLimit = Number(process.env.FREE_MONTHLY_TOKEN_LIMIT || 1_000);\nconst requireMemberForAi = process.env.REQUIRE_MEMBER_FOR_AI === 'true';\nconst deepseekMaxCompletionTokens = Math.max(1, Number(process.env.DEEPSEEK_MAX_COMPLETION_TOKENS || 1400));\nconst botFatherMaxCompletionTokens = Math.max(\n deepseekMaxCompletionTokens,\n Math.min(12_000, Number(process.env.BOT_FATHER_MAX_COMPLETION_TOKENS || 6000)),\n);\nconst maxResourceTextChars = Number(process.env.MAX_RESOURCE_TEXT_CHARS || 80_000);\nconst openClawRuntimeManifestPath =\n process.env.OPENCLAW_RUNTIME_MANIFEST_PATH || path.join(openClawRuntimeUpdatesDir, 'manifest.json');\nconst openClawRuntimePublicBaseUrl = (process.env.OPENCLAW_RUNTIME_PUBLIC_BASE_URL || '').replace(/\\/+$/, '');\nconst enableLibreChatAgentChat = process.env.ENABLE_LIBRECHAT_AGENT_CHAT === 'true';\nconst libreChatAgentsBaseUrl = (\n process.env.LIBRECHAT_AGENTS_BASE_URL || 'http://127.0.0.1:3080/api/agents/v1'\n).replace(/\\/+$/, '');\nconst libreChatAgentApiKey = process.env.LIBRECHAT_AGENT_API_KEY || '';\nconst libreChatAgentId = process.env.LIBRECHAT_AGENT_ID || '';\nconst enableCodexSdkChat = process.env.ENABLE_CODEX_SDK_CHAT === 'true';\nconst enableOpenClawAgentChat = process.env.ENABLE_OPENCLAW_AGENT_CHAT === 'true';\nconst openClawGatewayUrl = (process.env.OPENCLAW_GATEWAY_URL || 'http://127.0.0.1:18789').replace(/\\/+$/, '');\nconst openClawGatewayToken = process.env.OPENCLAW_GATEWAY_TOKEN || '';\nconst openClawAgentId = process.env.OPENCLAW_AGENT_ID || 'fabushi-public-agent';\nconst openClawRunsEndpoint = process.env.OPENCLAW_RUNS_ENDPOINT || '';\nconst codexDeepSeekProviderId = 'dacheng-deepseek-proxy';\nconst codexResponsesBaseUrl = (\n process.env.CODEX_DEEPSEEK_RESPONSES_BASE_URL ||\n `http://127.0.0.1:${process.env.PORT || 8788}/codex-deepseek/v1`\n).replace(/\\/+$/, '');\nconst codexAdapterTokenPrefix = 'dacheng-codex-proxy.';\nconst codexAdapterTokenTtlMs = Math.max(\n 60_000,\n Number(process.env.CODEX_DEEPSEEK_ADAPTER_TOKEN_TTL_MS || 15 * 60_000),\n);\nconst codexAdapterSecret =\n process.env.CODEX_DEEPSEEK_ADAPTER_SECRET ||\n deepseekApiKey ||\n openClawGatewayToken ||\n 'dacheng-codex-local-dev-secret';\n\nconst systemPrompt = [\n '你是“大乘”App 的 AI 助手。',\n '你的核心任务是帮助用户查找、整理、理解并全球法布施合法可分享的佛法资源。',\n '回答要庄重、简洁、可执行;涉及经典、仪轨或资源时,提醒用户尊重版权、来源和当地法规。',\n '如果用户想找资源、下载经文、寻找音频或准备可分享资料,你必须先在后端自动执行搜索、验证和下载步骤;不要要求用户去前端 + 菜单手动查找。',\n '你可以把执行进度总结给用户,但不要输出隐藏推理原文。',\n].join('\\n');\n\nfunction nowIso() {\n return new Date().toISOString();\n}\n\nfunction monthKey(date = new Date()) {\n return `${date.getUTCFullYear()}-${String(date.getUTCMonth() + 1).padStart(2, '0')}`;\n}\n\nfunction jsonResponse(res, status, payload) {\n res.status(status).json(payload);\n}\n\nasync function readJsonFile(filePath) {\n const raw = await fs.promises.readFile(filePath, 'utf8');\n return JSON.parse(raw);\n}\n\nfunction requestOrigin(req) {\n if (openClawRuntimePublicBaseUrl) return openClawRuntimePublicBaseUrl;\n const proto = req.get('X-Forwarded-Proto') || req.protocol || 'https';\n const host = req.get('X-Forwarded-Host') || req.get('Host');\n return host ? `${proto}://${host}` : '';\n}\n\nfunction absolutizeOpenClawRuntimeUrl(req, value) {\n const text = readText(value);\n if (!text) return '';\n if (/^https?:\\/\\//i.test(text)) return text;\n const normalized = text.startsWith('/')\n ? text\n : `/api/openclaw/runtime/files/${text.replace(/^\\/+/, '')}`;\n const origin = requestOrigin(req);\n return origin ? new URL(normalized, `${origin}/`).toString() : normalized;\n}\n\nfunction textToolResult(text, structuredContent = {}) {\n return {\n structuredContent,\n content: [{ type: 'text', text }],\n };\n}\n\nfunction sseHeaders(res) {\n res.status(200);\n res.setHeader('Content-Type', 'text/event-stream; charset=utf-8');\n res.setHeader('Cache-Control', 'no-cache, no-transform');\n res.setHeader('Connection', 'keep-alive');\n res.setHeader('X-Accel-Buffering', 'no');\n if (typeof res.flushHeaders === 'function') res.flushHeaders();\n}\n\nfunction writeSse(res, event, payload) {\n res.write(`event: ${event}\\n`);\n res.write(`data: ${JSON.stringify(payload)}\\n\\n`);\n}\n\nfunction asyncHandler(fn) {\n return (req, res, next) => {\n Promise.resolve(fn(req, res, next)).catch(next);\n };\n}\n\nfunction bearerToken(req) {\n const delegated = readText(req.get('x-dacheng-auth-token'));\n if (\n delegated &&\n delegated !== 'DACHENG_AUTH_TOKEN' &&\n !delegated.startsWith('secretref-env:') &&\n delegated !== 'dacheng-openclaw-proxy'\n ) {\n return delegated;\n }\n const header = req.get('Authorization') || '';\n if (!header.startsWith('Bearer ')) return '';\n const token = header.slice('Bearer '.length).trim();\n return token === 'dacheng-openclaw-proxy' ? '' : token;\n}\n\nfunction sha256(value) {\n return crypto.createHash('sha256').update(value).digest('hex');\n}\n\nfunction safeUserText(value, fallback = '') {\n if (typeof value !== 'string') return fallback;\n return value.replace(/\\u0000/g, '').trim();\n}\n\nfunction estimateTokens(text) {\n const normalized = String(text || '').trim();\n if (!normalized) return 0;\n return Math.max(1, Math.ceil(normalized.length / 3));\n}\n\nfunction readText(value) {\n if (value === undefined || value === null) return '';\n return String(value).replace(/\\u0000/g, '').trim();\n}\n\nfunction clientContextMessage(client) {\n if (!client || typeof client !== 'object') return '';\n const selectedModel = readText(client.selectedModel || client.model);\n const project = client.project && typeof client.project === 'object' ? client.project : {};\n const projectName = readText(project.name);\n const projectPath = readText(project.path);\n const lines = ['桌面端上下文:'];\n if (selectedModel) lines.push(`用户选择的 DeepSeek 模型:${normalizeDeepSeekModelName(selectedModel)}`);\n if (projectPath) {\n lines.push(`当前项目名称:${projectName || projectPath}`);\n lines.push(`当前项目路径:${projectPath}`);\n lines.push('如果本轮任务涉及读取、修改、运行、分析或整理项目,请优先围绕当前项目路径工作;不要擅自操作其他目录。');\n }\n return lines.length > 1 ? lines.join('\\n') : '';\n}\nfunction unwrapPayload(payload) {\n if (!payload || typeof payload !== 'object') return {};\n return payload.data && typeof payload.data === 'object' ? payload.data : payload;\n}\n\nfunction readFirstText(values) {\n for (const value of values) {\n const text = readText(value);\n if (text) return text;\n }\n return '';\n}\n\nfunction activeFlag(value) {\n if (value === true) return true;\n if (value === 1) return true;\n if (typeof value !== 'string') return false;\n return ['true', '1', 'yes', 'active'].includes(value.trim().toLowerCase());\n}\n\nfunction futureDate(value) {\n const text = readText(value);\n if (!text) return false;\n const timestamp = Date.parse(text);\n return Number.isFinite(timestamp) && timestamp > Date.now();\n}\n\nfunction normalizeMembershipPayload(payload) {\n if (!payload || typeof payload !== 'object') return false;\n const data = unwrapPayload(payload);\n const membership = data.membership && typeof data.membership === 'object' ? data.membership : data;\n const type = readText(membership.type || membership.membershipType || data.membershipType).toLowerCase();\n const status = readText(membership.status || data.status).toLowerCase();\n const expiry = membership.expiresAt ||\n membership.expires_at ||\n membership.membershipExpiry ||\n membership.membership_expires_at ||\n data.membershipExpiry ||\n data.membership_expires_at;\n const hasExplicitActiveFlag =\n membership.isActive !== undefined ||\n membership.active !== undefined ||\n membership.hasPremiumAccess !== undefined ||\n membership.hasPremiumMembership !== undefined ||\n membership.isPremium !== undefined;\n\n if (hasExplicitActiveFlag) {\n return Boolean(\n activeFlag(membership.isActive) ||\n activeFlag(membership.active) ||\n activeFlag(membership.hasPremiumAccess) ||\n activeFlag(membership.hasPremiumMembership) ||\n activeFlag(membership.isPremium),\n );\n }\n\n return Boolean(\n status === 'active' ||\n type === 'paid' ||\n type === 'premium' ||\n type === 'trial' ||\n futureDate(expiry),\n );\n}\n\nfunction extractRemoteAccount(payload) {\n const data = unwrapPayload(payload);\n const nestedUser = data.user && typeof data.user === 'object' ? data.user : {};\n const id = readFirstText([\n data.userId,\n data.user_id,\n data.userNo,\n data.user_no,\n data.id,\n nestedUser.userId,\n nestedUser.user_id,\n nestedUser.userNo,\n nestedUser.user_no,\n nestedUser.id,\n ]);\n const username = readFirstText([data.username, data.name, nestedUser.username, nestedUser.name]);\n const email = readFirstText([data.email, nestedUser.email]);\n const membership = data.membership && typeof data.membership === 'object' ? data.membership : null;\n return {\n authenticated: Boolean(id || username || email || membership || data.success === true),\n id,\n username,\n email,\n isMember: normalizeMembershipPayload(payload),\n membership,\n };\n}\n\nasync function resolveRemoteAccount(token) {\n const fallback = {\n authenticated: false,\n id: '',\n username: '',\n email: '',\n isMember: false,\n membership: null,\n };\n if (!token || !fabushiApiBaseUrl) return fallback;\n\n const endpoints = [\n '/api/stripe/membership-status',\n '/api/alipay/check-membership',\n '/api/auth/user-info',\n ];\n\n for (const endpoint of endpoints) {\n try {\n const response = await fetch(`${fabushiApiBaseUrl}${endpoint}`, {\n headers: {\n Accept: 'application/json',\n Authorization: `Bearer ${token}`,\n },\n signal: AbortSignal.timeout(8_000),\n });\n if (response.status === 401 || response.status === 403) return fallback;\n if (!response.ok) continue;\n const payload = await response.json();\n const account = extractRemoteAccount(payload);\n if (account.authenticated || account.isMember) {\n return {\n ...fallback,\n ...account,\n authenticated: true,\n };\n }\n } catch (error) {\n logger.debug({ endpoint, error: String(error) }, 'membership lookup skipped');\n }\n }\n\n return fallback;\n}\n\nasync function resolveUser(req, body = {}) {\n const token = bearerToken(req);\n const usernameHint = safeUserText(body.username || req.query.username);\n\n const internalMcpAccount = resolveCodexAdapterToken(token);\n if (\n internalMcpAccount?.audience === 'mcp-plugin' &&\n req.path.startsWith('/api/mcp/apps/')\n ) {\n return internalMcpAccount.user;\n }\n\n const testToken = readText(process.env.TEST_ACCOUNT_TOKEN);\n if (testToken && token && token === testToken) {\n return {\n userId: 'user:test_account',\n username: 'TestAccount',\n tokenHash: sha256(token).slice(0, 24),\n isAuthenticated: true,\n isMember: true,\n isTestAccount: true,\n membership: { type: 'lifetime', active: true },\n };\n }\n\n const tokenHash = token ? sha256(token).slice(0, 24) : '';\n const anonymousHash = sha256(`${req.ip}|${req.get('User-Agent') || ''}`).slice(0, 24);\n const remoteAccount = await resolveRemoteAccount(token);\n const remoteIdentity = remoteAccount.id ||\n remoteAccount.username ||\n remoteAccount.email ||\n (remoteAccount.authenticated ? tokenHash : '');\n const userId = remoteAccount.authenticated\n ? `user:${remoteIdentity}`\n : tokenHash\n ? `token:${tokenHash}`\n : `anon:${anonymousHash}`;\n return {\n userId,\n username: remoteAccount.username || usernameHint,\n tokenHash,\n isAuthenticated: remoteAccount.authenticated,\n isMember: remoteAccount.isMember,\n membership: remoteAccount.membership,\n };\n}\n\nfunction base64UrlJson(value) {\n return Buffer.from(JSON.stringify(value), 'utf8').toString('base64url');\n}\n\nfunction timingSafeEqualText(left, right) {\n const leftBuffer = Buffer.from(String(left || ''));\n const rightBuffer = Buffer.from(String(right || ''));\n if (leftBuffer.length !== rightBuffer.length) return false;\n return crypto.timingSafeEqual(leftBuffer, rightBuffer);\n}\n\nfunction signCodexAdapterPayload(encodedPayload) {\n return crypto\n .createHmac('sha256', codexAdapterSecret)\n .update(encodedPayload)\n .digest('base64url');\n}\n\nfunction createCodexAdapterToken(user, options = {}) {\n const requestedMaxCompletionTokens = Number(options.maxCompletionTokens || 0);\n const payload = {\n sub: user.userId,\n username: user.username || '',\n tokenHash: user.tokenHash || '',\n isAuthenticated: Boolean(user.isAuthenticated),\n isMember: Boolean(user.isMember),\n isTestAccount: Boolean(user.isTestAccount),\n membership: user.membership || {},\n aud: safeUserText(options.audience) || 'codex-responses',\n recordUsage: options.recordUsage !== false,\n maxCompletionTokens:\n Number.isFinite(requestedMaxCompletionTokens) && requestedMaxCompletionTokens > 0\n ? Math.floor(requestedMaxCompletionTokens)\n : null,\n exp: Date.now() + codexAdapterTokenTtlMs,\n };\n const encodedPayload = base64UrlJson(payload);\n return `${codexAdapterTokenPrefix}${encodedPayload}.${signCodexAdapterPayload(encodedPayload)}`;\n}\n\nfunction resolveCodexAdapterToken(token) {\n const raw = readText(token);\n if (!raw.startsWith(codexAdapterTokenPrefix)) return null;\n\n const signed = raw.slice(codexAdapterTokenPrefix.length);\n const separator = signed.lastIndexOf('.');\n if (separator < 1) return null;\n\n const encodedPayload = signed.slice(0, separator);\n const signature = signed.slice(separator + 1);\n const expectedSignature = signCodexAdapterPayload(encodedPayload);\n if (!timingSafeEqualText(signature, expectedSignature)) return null;\n\n let payload;\n try {\n payload = JSON.parse(Buffer.from(encodedPayload, 'base64url').toString('utf8'));\n } catch {\n return null;\n }\n\n if (!payload || typeof payload !== 'object') return null;\n if (Number(payload.exp || 0) < Date.now()) return null;\n\n const userId = safeUserText(payload.sub);\n if (!userId) return null;\n\n return {\n user: {\n userId,\n username: safeUserText(payload.username),\n tokenHash: safeUserText(payload.tokenHash),\n isAuthenticated: Boolean(payload.isAuthenticated),\n isMember: Boolean(payload.isMember),\n isTestAccount: Boolean(payload.isTestAccount),\n membership:\n payload.membership && typeof payload.membership === 'object'\n ? payload.membership\n : {},\n },\n audience: safeUserText(payload.aud) || 'codex-responses',\n recordUsage: payload.recordUsage !== false,\n maxCompletionTokens:\n Number.isFinite(Number(payload.maxCompletionTokens)) && Number(payload.maxCompletionTokens) > 0\n ? Math.floor(Number(payload.maxCompletionTokens))\n : null,\n };\n}\n\nasync function resolveCodexResponsesBilling(req) {\n const token = bearerToken(req);\n const adapter = resolveCodexAdapterToken(token);\n if (adapter) return adapter;\n\n if (deepseekApiKey && token === deepseekApiKey) {\n const error = new Error('Codex DeepSeek adapter now requires first-party user billing');\n error.statusCode = 401;\n throw error;\n }\n\n return {\n user: await resolveUser(req, req.body || {}),\n recordUsage: true,\n };\n}\n\nfunction usageFor(userId) {\n const month = monthKey();\n const row = statements.getUsage.get(userId, month);\n return {\n month,\n used: Number(row?.totalTokens || 0),\n };\n}\n\nfunction enforceTokenBudget(user, estimatedTokensForRequest) {\n if (user && user.isTestAccount) {\n return { limit: 999999999, usage: { month: monthKey(), used: 0 } };\n }\n if (requireMemberForAi && !user.isMember) {\n const error = new Error('大乘 AI 需要开通会员后使用');\n error.statusCode = 403;\n error.code = 'AI_MEMBERSHIP_REQUIRED';\n error.details = {\n requireMemberForAi,\n monthlyLimit: memberMonthlyLimit,\n remainingTokens: 0,\n };\n throw error;\n }\n\n const limit = user.isMember ? memberMonthlyLimit : freeMonthlyLimit;\n const usage = usageFor(user.userId);\n if (usage.used + estimatedTokensForRequest > limit) {\n const error = new Error('本月 AI token 额度已不足');\n error.statusCode = 429;\n error.code = 'USER_TOKEN_QUOTA_EXCEEDED';\n error.details = {\n monthlyLimit: limit,\n usedTokens: usage.used,\n remainingTokens: Math.max(0, limit - usage.used),\n };\n throw error;\n }\n return { limit, usage };\n}\n\nfunction completionBudgetFor(\n budget,\n estimatedPromptTokens,\n maximumCompletionTokens = deepseekMaxCompletionTokens,\n) {\n const remainingAfterPrompt = budget.limit - budget.usage.used - estimatedPromptTokens;\n return Math.max(1, Math.min(maximumCompletionTokens, Math.floor(remainingAfterPrompt)));\n}\n\nfunction recordUsage(userId, totalTokens) {\n if (!totalTokens || totalTokens < 1) return;\n statements.upsertUsage.run({\n userId,\n month: monthKey(),\n totalTokens,\n updatedAt: nowIso(),\n });\n}\n\nfunction titleFromMessage(message) {\n const compact = message.replace(/\\s+/g, ' ').trim();\n if (!compact) return '新对话';\n return compact.length > 22 ? `${compact.slice(0, 22)}...` : compact;\n}\n\nfunction normalizeMessages(rows) {\n return rows.map((row) => ({\n role: row.role,\n content: row.content,\n }));\n}\n\nfunction normalizeMaxCompletionTokens(\n value,\n maximumCompletionTokens = deepseekMaxCompletionTokens,\n) {\n const numeric = Number(value || maximumCompletionTokens);\n if (!Number.isFinite(numeric) || numeric < 1) return maximumCompletionTokens;\n return Math.max(1, Math.min(maximumCompletionTokens, Math.floor(numeric)));\n}\n\nfunction normalizeDeepSeekModelName(value) {\n const raw = readText(value || deepseekModel);\n if (!raw) return deepseekModel;\n const modelId = raw.includes('/') ? raw.split('/').pop() : raw;\n return modelId || deepseekModel;\n}\n\nasync function callDeepSeek(messages, options = {}) {\n if (!deepseekApiKey) {\n const error = new Error('DeepSeek API key is not configured');\n error.statusCode = 503;\n throw error;\n }\n\n const model = normalizeDeepSeekModelName(options.model);\n const response = await fetch(`${deepseekBaseUrl}/chat/completions`, {\n method: 'POST',\n headers: {\n Accept: 'application/json',\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${deepseekApiKey}`,\n },\n body: JSON.stringify({\n model,\n messages,\n temperature: 0.4,\n max_tokens: normalizeMaxCompletionTokens(\n options.maxCompletionTokens,\n options.maximumCompletionTokens || deepseekMaxCompletionTokens,\n ),\n stream: false,\n }),\n signal: AbortSignal.timeout(60_000),\n });\n\n const bodyText = await response.text();\n let payload;\n try {\n payload = bodyText ? JSON.parse(bodyText) : {};\n } catch {\n payload = { error: bodyText };\n }\n\n if (!response.ok) {\n const upstreamMessage = payload?.error?.message || payload?.message || '';\n const friendlyMessage =\n response.status === 402 || /insufficient\\s+balance/i.test(upstreamMessage)\n ? '大乘 AI 服务额度暂不可用,请联系管理员充值 DeepSeek 账户后重试。'\n : upstreamMessage || `DeepSeek request failed: ${response.status}`;\n const error = new Error(friendlyMessage);\n error.statusCode = response.status;\n if (response.status === 402 || /insufficient\\s+balance/i.test(upstreamMessage)) {\n error.code = 'DEEPSEEK_PROVIDER_QUOTA';\n }\n throw error;\n }\n\n const message = payload?.choices?.[0]?.message?.content?.trim();\n if (!message) {\n const error = new Error('DeepSeek returned an empty response');\n error.statusCode = 502;\n throw error;\n }\n\n return {\n message,\n model,\n usage: {\n promptTokens: Number(payload?.usage?.prompt_tokens || 0),\n completionTokens: Number(payload?.usage?.completion_tokens || 0),\n totalTokens: Number(payload?.usage?.total_tokens || 0),\n },\n };\n}\n\nasync function callDeepSeekStream(messages, callbacks = {}) {\n if (!deepseekApiKey) {\n const error = new Error('DeepSeek API key is not configured');\n error.statusCode = 503;\n throw error;\n }\n\n const model = normalizeDeepSeekModelName(callbacks.model);\n const response = await fetch(`${deepseekBaseUrl}/chat/completions`, {\n method: 'POST',\n headers: {\n Accept: 'text/event-stream',\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${deepseekApiKey}`,\n },\n body: JSON.stringify({\n model,\n messages,\n temperature: 0.4,\n max_tokens: normalizeMaxCompletionTokens(\n callbacks.maxCompletionTokens,\n callbacks.maximumCompletionTokens || deepseekMaxCompletionTokens,\n ),\n stream: true,\n stream_options: { include_usage: true },\n }),\n signal: callbacks.signal || AbortSignal.timeout(90_000),\n });\n\n if (!response.ok) {\n const bodyText = await response.text();\n let payload;\n try {\n payload = bodyText ? JSON.parse(bodyText) : {};\n } catch {\n payload = { error: bodyText };\n }\n const upstreamMessage = payload?.error?.message || payload?.message || '';\n const friendlyMessage =\n response.status === 402 || /insufficient\\s+balance/i.test(upstreamMessage)\n ? '大乘 AI 服务额度暂不可用,请联系管理员充值 DeepSeek 账户后重试。'\n : upstreamMessage || `DeepSeek request failed: ${response.status}`;\n const error = new Error(friendlyMessage);\n error.statusCode = response.status;\n if (response.status === 402 || /insufficient\\s+balance/i.test(upstreamMessage)) {\n error.code = 'DEEPSEEK_PROVIDER_QUOTA';\n }\n throw error;\n }\n\n let buffer = '';\n let message = '';\n let usage = {\n promptTokens: 0,\n completionTokens: 0,\n totalTokens: 0,\n };\n const decoder = new TextDecoder();\n\n for await (const chunk of response.body) {\n buffer += decoder.decode(chunk, { stream: true });\n const lines = buffer.split(/\\r?\\n/);\n buffer = lines.pop() || '';\n\n for (const rawLine of lines) {\n const line = rawLine.trim();\n if (!line.startsWith('data:')) continue;\n const data = line.slice('data:'.length).trim();\n if (!data || data === '[DONE]') continue;\n\n let payload;\n try {\n payload = JSON.parse(data);\n } catch {\n continue;\n }\n\n const delta = payload?.choices?.[0]?.delta?.content || '';\n if (delta) {\n message += delta;\n callbacks.onToken?.(delta);\n }\n\n if (payload?.usage) {\n usage = {\n promptTokens: Number(payload.usage.prompt_tokens || usage.promptTokens || 0),\n completionTokens: Number(payload.usage.completion_tokens || usage.completionTokens || 0),\n totalTokens: Number(payload.usage.total_tokens || usage.totalTokens || 0),\n };\n }\n }\n }\n\n message = message.trim();\n if (!message) {\n const error = new Error('DeepSeek returned an empty response');\n error.statusCode = 502;\n throw error;\n }\n\n return { message, model, usage };\n}\n\nfunction createCodexDeepSeekRuntime(user = null, runtimeOptions = {}) {\n if (!enableCodexSdkChat || !deepseekApiKey) {\n return {\n enabled: false,\n provider: codexDeepSeekProviderId,\n reason: enableCodexSdkChat ? 'DeepSeek API key is not configured' : 'ENABLE_CODEX_SDK_CHAT is not true',\n };\n }\n\n const adapterToken = user\n ? createCodexAdapterToken(user, {\n maxCompletionTokens: runtimeOptions.maxCompletionTokens,\n })\n : '';\n return {\n enabled: true,\n provider: codexDeepSeekProviderId,\n options: {\n apiKey: adapterToken,\n baseUrl: codexResponsesBaseUrl,\n env: {\n ...process.env,\n DEEPSEEK_API_KEY: adapterToken,\n HOME: runtimeOptions.codexHomeDir || codexHomeDir,\n CODEX_HOME: runtimeOptions.codexHomeDir || codexHomeDir,\n TMPDIR: codexTempDir,\n XDG_RUNTIME_DIR: codexRuntimeDir,\n XDG_CACHE_HOME: path.join(codexHomeDir, '.cache'),\n XDG_CONFIG_HOME: path.join(codexHomeDir, '.config'),\n XDG_DATA_HOME: path.join(codexHomeDir, '.local', 'share'),\n PATH: process.env.PATH || '/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin',\n ...(runtimeOptions.env || {}),\n },\n config: {\n model_provider: codexDeepSeekProviderId,\n model_providers: {\n [codexDeepSeekProviderId]: {\n name: 'Dacheng DeepSeek Proxy',\n base_url: codexResponsesBaseUrl,\n env_key: 'DEEPSEEK_API_KEY',\n wire_api: 'responses',\n query_params: {},\n },\n },\n ...(runtimeOptions.config || {}),\n },\n },\n threadOptions: {\n model: deepseekModel,\n approvalPolicy: runtimeOptions.approvalPolicy || 'never',\n sandboxMode: runtimeOptions.sandboxMode || 'read-only',\n ...(runtimeOptions.workingDirectory\n ? { workingDirectory: runtimeOptions.workingDirectory }\n : {}),\n skipGitRepoCheck: true,\n networkAccessEnabled: runtimeOptions.networkAccessEnabled ?? true,\n webSearchMode: runtimeOptions.webSearchMode || 'disabled',\n },\n };\n}\n\nfunction codexPromptFromMessages(messages) {\n return messages\n .map((message) => {\n const role = String(message.role || 'user').toUpperCase();\n return `${role}:\\n${message.content}`;\n })\n .join('\\n\\n');\n}\n\nfunction responseContentText(content) {\n if (typeof content === 'string') return content;\n if (!Array.isArray(content)) return '';\n return content\n .map((part) => {\n if (typeof part === 'string') return part;\n if (!part || typeof part !== 'object') return '';\n return String(part.text || part.input_text || part.output_text || '');\n })\n .filter(Boolean)\n .join('\\n');\n}\n\nfunction codexResponsesPrompt(body) {\n const input = Array.isArray(body?.input) ? body.input : [];\n const userItems = input.filter((item) => item?.role === 'user');\n const source = userItems[userItems.length - 1] || input[input.length - 1];\n const prompt = responseContentText(source?.content).trim();\n if (prompt) return prompt;\n if (typeof body?.input === 'string') return body.input.trim();\n return '';\n}\n\nfunction writeResponsesEvent(res, event, data) {\n res.write(`event: ${event}\\n`);\n res.write(`data: ${JSON.stringify(data)}\\n\\n`);\n}\n\nfunction responseUsage(usage = {}) {\n return {\n input_tokens: Number(usage.promptTokens || usage.input_tokens || 0),\n cached_input_tokens: 0,\n output_tokens: Number(usage.completionTokens || usage.output_tokens || 0),\n reasoning_output_tokens: 0,\n total_tokens: Number(usage.totalTokens || usage.total_tokens || 0),\n };\n}\n\nfunction createLibreChatAgentRuntime() {\n if (!enableLibreChatAgentChat || !libreChatAgentApiKey || !libreChatAgentId) {\n return {\n enabled: false,\n provider: 'librechat-agent',\n reason: !enableLibreChatAgentChat\n ? 'ENABLE_LIBRECHAT_AGENT_CHAT is not true'\n : !libreChatAgentApiKey\n ? 'LIBRECHAT_AGENT_API_KEY is not configured'\n : 'LIBRECHAT_AGENT_ID is not configured',\n };\n }\n\n return {\n enabled: true,\n provider: 'librechat-agent',\n model: libreChatAgentId,\n chatCompletionsUrl: `${libreChatAgentsBaseUrl}/chat/completions`,\n };\n}\n\nfunction libreChatUsage(usage = {}) {\n return {\n promptTokens: Number(usage.prompt_tokens || usage.input_tokens || usage.promptTokens || 0),\n completionTokens: Number(usage.completion_tokens || usage.output_tokens || usage.completionTokens || 0),\n totalTokens: Number(usage.total_tokens || usage.totalTokens || 0),\n };\n}\n\nasync function callLibreChatAgent(messages) {\n const runtime = createLibreChatAgentRuntime();\n if (!runtime.enabled) {\n const error = new Error(runtime.reason || 'LibreChat Agent chat is not enabled');\n error.statusCode = 503;\n throw error;\n }\n\n const response = await fetch(runtime.chatCompletionsUrl, {\n method: 'POST',\n headers: {\n Accept: 'application/json',\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${libreChatAgentApiKey}`,\n },\n body: JSON.stringify({\n model: runtime.model,\n messages,\n temperature: 0.4,\n stream: false,\n }),\n signal: AbortSignal.timeout(120_000),\n });\n\n const bodyText = await response.text();\n let payload;\n try {\n payload = bodyText ? JSON.parse(bodyText) : {};\n } catch {\n payload = { error: bodyText };\n }\n\n if (!response.ok) {\n const upstreamMessage = payload?.error?.message || payload?.message || bodyText || '';\n const error = new Error(upstreamMessage || `LibreChat Agent request failed: ${response.status}`);\n error.statusCode = response.status;\n throw error;\n }\n\n const message = payload?.choices?.[0]?.message?.content?.trim();\n if (!message) {\n const error = new Error('LibreChat Agent returned an empty response');\n error.statusCode = 502;\n throw error;\n }\n\n return {\n message,\n usage: libreChatUsage(payload?.usage),\n };\n}\n\nasync function callLibreChatAgentStream(messages, callbacks = {}) {\n const runtime = createLibreChatAgentRuntime();\n if (!runtime.enabled) {\n const error = new Error(runtime.reason || 'LibreChat Agent chat is not enabled');\n error.statusCode = 503;\n throw error;\n }\n\n const response = await fetch(runtime.chatCompletionsUrl, {\n method: 'POST',\n headers: {\n Accept: 'text/event-stream',\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${libreChatAgentApiKey}`,\n },\n body: JSON.stringify({\n model: runtime.model,\n messages,\n temperature: 0.4,\n stream: true,\n }),\n signal: callbacks.signal || AbortSignal.timeout(180_000),\n });\n\n if (!response.ok) {\n const bodyText = await response.text();\n let payload;\n try {\n payload = bodyText ? JSON.parse(bodyText) : {};\n } catch {\n payload = { error: bodyText };\n }\n const upstreamMessage = payload?.error?.message || payload?.message || bodyText || '';\n const error = new Error(upstreamMessage || `LibreChat Agent request failed: ${response.status}`);\n error.statusCode = response.status;\n throw error;\n }\n\n let buffer = '';\n let message = '';\n let usage = { promptTokens: 0, completionTokens: 0, totalTokens: 0 };\n const decoder = new TextDecoder();\n\n for await (const chunk of response.body) {\n buffer += decoder.decode(chunk, { stream: true });\n const lines = buffer.split(/\\r?\\n/);\n buffer = lines.pop() || '';\n\n for (const rawLine of lines) {\n const line = rawLine.trim();\n if (!line.startsWith('data:')) continue;\n const data = line.slice('data:'.length).trim();\n if (!data || data === '[DONE]') continue;\n\n let payload;\n try {\n payload = JSON.parse(data);\n } catch {\n continue;\n }\n\n const delta = payload?.choices?.[0]?.delta?.content || '';\n if (delta) {\n message += delta;\n callbacks.onToken?.(delta);\n }\n\n if (payload?.usage) {\n usage = libreChatUsage(payload.usage);\n }\n }\n }\n\n message = message.trim();\n if (!message) {\n const error = new Error('LibreChat Agent returned an empty response');\n error.statusCode = 502;\n throw error;\n }\n\n return { message, usage };\n}\n\nfunction isDeepSeekQuotaError(error) {\n if (error?.code === 'DEEPSEEK_PROVIDER_QUOTA') return true;\n if (error?.code === 'USER_TOKEN_QUOTA_EXCEEDED' || error?.code === 'AI_MEMBERSHIP_REQUIRED') return false;\n const message = String(error?.message || error || '');\n return /insufficient\\s+balance|balance/i.test(message);\n}\n\nfunction publicAiErrorMessage(error) {\n if (isDeepSeekQuotaError(error)) {\n return '大乘 AI 服务额度暂不可用,请联系管理员充值 DeepSeek 账户后重试。';\n }\n return error?.message || 'Internal Server Error';\n}\n\nfunction responsesPayload({\n responseId,\n itemId,\n status,\n model,\n text = '',\n usage = null,\n error = null,\n}) {\n return {\n id: responseId,\n object: 'response',\n status,\n model,\n output:\n status === 'completed'\n ? [\n {\n id: itemId,\n type: 'message',\n role: 'assistant',\n status: 'completed',\n content: [{ type: 'output_text', text }],\n },\n ]\n : [],\n ...(usage ? { usage: responseUsage(usage) } : {}),\n ...(error\n ? {\n error: {\n message: error.message || String(error),\n type: 'server_error',\n },\n }\n : {}),\n };\n}\n\nasync function callCodexSdkDeepSeek(messages, callbacks = {}) {\n const codexRuntime = createCodexDeepSeekRuntime(callbacks.user, {\n maxCompletionTokens: callbacks.maxCompletionTokens,\n workingDirectory: callbacks.workingDirectory,\n sandboxMode: callbacks.sandboxMode,\n approvalPolicy: callbacks.approvalPolicy,\n networkAccessEnabled: callbacks.networkAccessEnabled,\n webSearchMode: callbacks.webSearchMode,\n codexHomeDir: callbacks.codexHomeDir,\n env: callbacks.env,\n config: callbacks.config,\n });\n if (!codexRuntime.enabled) {\n const error = new Error(codexRuntime.reason || 'Codex SDK chat is not enabled');\n error.statusCode = 503;\n throw error;\n }\n if (!callbacks.user) {\n const error = new Error('Codex SDK DeepSeek proxy requires a resolved user for billing');\n error.statusCode = 401;\n throw error;\n }\n\n const { Codex } = await import('@openai/codex-sdk');\n const codex = new Codex(codexRuntime.options);\n const thread = callbacks.threadId\n ? codex.resumeThread(callbacks.threadId, codexRuntime.threadOptions)\n : codex.startThread(codexRuntime.threadOptions);\n const prompt = codexPromptFromMessages(messages);\n const turnOptions = {\n ...(callbacks.outputSchema ? { outputSchema: callbacks.outputSchema } : {}),\n ...(callbacks.signal ? { signal: callbacks.signal } : {}),\n };\n\n if (callbacks.onToken || callbacks.onStep) {\n const { events } = await thread.runStreamed(prompt, turnOptions);\n let finalResponse = '';\n let usage = null;\n let threadId = callbacks.threadId || null;\n\n for await (const event of events) {\n if (event.type === 'thread.started') {\n threadId = event.thread_id || threadId;\n } else if (event.type === 'item.completed') {\n const item = event.item;\n if (item.type === 'agent_message') {\n finalResponse = item.text || finalResponse;\n callbacks.onToken?.(item.text || '');\n } else if (item.type === 'todo_list') {\n callbacks.onStep?.({\n title: 'Codex SDK 执行计划',\n message: item.items.map((todo) => `${todo.completed ? '✓' : '•'} ${todo.text}`).join('\\n'),\n });\n } else if (item.type === 'command_execution') {\n callbacks.onStep?.({\n title: 'Codex SDK 执行命令',\n message: item.command,\n });\n } else if (item.type === 'file_change') {\n callbacks.onStep?.({\n title: 'Codex SDK 修改文件',\n message: item.changes.map((change) => `${change.kind}: ${change.path}`).join('\\n'),\n });\n } else if (item.type === 'mcp_tool_call') {\n callbacks.onStep?.({\n title: 'Codex SDK 调用工具',\n message: `${item.server}.${item.tool}`,\n });\n }\n } else if (event.type === 'turn.completed') {\n usage = event.usage;\n } else if (event.type === 'turn.failed') {\n throw new Error(event.error?.message || 'Codex SDK turn failed');\n } else if (event.type === 'error') {\n throw new Error(event.message || 'Codex SDK stream failed');\n }\n }\n\n if (!finalResponse.trim()) {\n const error = new Error('Codex SDK returned an empty response');\n error.statusCode = 502;\n throw error;\n }\n\n return {\n message: finalResponse.trim(),\n usage: {\n promptTokens: Number(usage?.input_tokens || 0),\n completionTokens: Number(usage?.output_tokens || 0),\n totalTokens: Number((usage?.input_tokens || 0) + (usage?.output_tokens || 0)),\n },\n usageAlreadyRecorded: true,\n threadId: thread.id || threadId,\n };\n }\n\n const turn = await thread.run(prompt, turnOptions);\n if (!turn.finalResponse?.trim()) {\n const error = new Error('Codex SDK returned an empty response');\n error.statusCode = 502;\n throw error;\n }\n\n return {\n message: turn.finalResponse.trim(),\n usage: {\n promptTokens: Number(turn.usage?.input_tokens || 0),\n completionTokens: Number(turn.usage?.output_tokens || 0),\n totalTokens: Number((turn.usage?.input_tokens || 0) + (turn.usage?.output_tokens || 0)),\n },\n usageAlreadyRecorded: true,\n threadId: thread.id || callbacks.threadId || null,\n };\n}\n\nconst activeAgentRuns = new Map();\n\nfunction normalizeAgentMode(value) {\n const mode = safeUserText(value || 'dharma_guide');\n return /^[a-z][a-z0-9_:-]{0,48}$/i.test(mode) ? mode : 'dharma_guide';\n}\n\nfunction openClawSessionKey(user, conversationId) {\n const env = process.env.NODE_ENV === 'production' ? 'prod' : 'dev';\n return `fabushi:env:${env}:user:${user.userId}:conversation:${conversationId}`;\n}\n\nfunction createOpenClawRuntime() {\n if (!enableOpenClawAgentChat || !openClawGatewayToken || !openClawGatewayUrl) {\n return {\n enabled: false,\n provider: 'openclaw-gateway',\n reason: !enableOpenClawAgentChat\n ? 'ENABLE_OPENCLAW_AGENT_CHAT is not true'\n : !openClawGatewayToken\n ? 'OPENCLAW_GATEWAY_TOKEN is not configured'\n : 'OPENCLAW_GATEWAY_URL is not configured',\n };\n }\n\n return {\n enabled: true,\n provider: 'openclaw-gateway',\n model: openClawAgentId,\n runsUrl: openClawRunsEndpoint || `${openClawGatewayUrl}/v1/agents/${encodeURIComponent(openClawAgentId)}/runs`,\n };\n}\n\nfunction extractOpenClawText(payload) {\n if (!payload || typeof payload !== 'object') return '';\n const candidates = [\n payload.message,\n payload.content,\n payload.output_text,\n payload.finalResponse,\n payload.final_response,\n payload.result?.message,\n payload.result?.content,\n payload.output?.message,\n payload.output?.content,\n payload.choices?.[0]?.message?.content,\n ];\n for (const candidate of candidates) {\n if (typeof candidate === 'string' && candidate.trim()) return candidate.trim();\n }\n return '';\n}\n\nfunction extractOpenClawUsage(payload) {\n const usage = payload?.usage || payload?.result?.usage || {};\n return {\n promptTokens: Number(usage.input_tokens || usage.prompt_tokens || usage.promptTokens || 0),\n completionTokens: Number(usage.output_tokens || usage.completion_tokens || usage.completionTokens || 0),\n totalTokens: Number(usage.total_tokens || usage.totalTokens || 0),\n };\n}\n\nasync function callOpenClawAgent({ messages, user, conversationId, mode, signal }) {\n const runtime = createOpenClawRuntime();\n if (!runtime.enabled) {\n const error = new Error(runtime.reason || 'OpenClaw Agent chat is not enabled');\n error.statusCode = 503;\n throw error;\n }\n\n const sessionKey = openClawSessionKey(user, conversationId);\n const response = await fetch(runtime.runsUrl, {\n method: 'POST',\n headers: {\n Accept: 'application/json',\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${openClawGatewayToken}`,\n },\n body: JSON.stringify({\n agentId: openClawAgentId,\n sessionKey,\n mode,\n stream: false,\n input: { messages },\n messages,\n metadata: {\n app: 'fabushi',\n userId: user.userId,\n conversationId,\n toolPolicy: 'public-user-read-mostly',\n },\n }),\n signal: signal || AbortSignal.timeout(180_000),\n });\n\n const bodyText = await response.text();\n let payload;\n try {\n payload = bodyText ? JSON.parse(bodyText) : {};\n } catch {\n payload = { message: bodyText };\n }\n\n if (!response.ok) {\n const error = new Error(payload?.error?.message || payload?.message || `OpenClaw request failed: ${response.status}`);\n error.statusCode = response.status;\n throw error;\n }\n\n const message = extractOpenClawText(payload);\n if (!message) {\n const error = new Error('OpenClaw returned an empty response');\n error.statusCode = 502;\n throw error;\n }\n\n return {\n message,\n runId: payload.runId || payload.id || payload.result?.runId || null,\n provider: runtime.provider,\n model: runtime.model,\n usage: extractOpenClawUsage(payload),\n };\n}\n\nasync function runAgentModel({\n messages,\n user,\n conversationId,\n mode,\n callbacks = {},\n signal,\n maxCompletionTokens,\n preferCodex = false,\n requireCodex = false,\n workingDirectory,\n sandboxMode,\n outputSchema,\n threadId,\n}) {\n const codexRuntime = createCodexDeepSeekRuntime(user, {\n workingDirectory,\n sandboxMode,\n maxCompletionTokens,\n });\n if (requireCodex && !codexRuntime.enabled) {\n const error = new Error(codexRuntime.reason || 'Codex SDK chat is not enabled');\n error.statusCode = 503;\n throw error;\n }\n\n let preferredCodexFailed = false;\n if ((preferCodex || requireCodex) && codexRuntime.enabled) {\n try {\n const result = await callCodexSdkDeepSeek(messages, {\n ...callbacks,\n user,\n signal,\n maxCompletionTokens,\n workingDirectory,\n sandboxMode,\n outputSchema,\n threadId,\n });\n return {\n ...result,\n provider: codexRuntime.provider,\n model: deepseekModel,\n };\n } catch (error) {\n if (requireCodex || isDeepSeekQuotaError(error)) throw error;\n logger.warn(\n { error: String(error), mode },\n 'Preferred Codex SDK model failed, continuing through agent fallbacks',\n );\n preferredCodexFailed = true;\n }\n }\n\n const openClawRuntime = createOpenClawRuntime();\n if (openClawRuntime.enabled && !callbacks.onToken) {\n return await callOpenClawAgent({ messages, user, conversationId, mode, signal });\n }\n\n const libreChatRuntime = createLibreChatAgentRuntime();\n let provider = libreChatRuntime.enabled\n ? libreChatRuntime.provider\n : codexRuntime.enabled && !preferredCodexFailed\n ? codexRuntime.provider\n : 'deepseek';\n let model = libreChatRuntime.enabled ? libreChatRuntime.model : deepseekModel;\n\n try {\n if (libreChatRuntime.enabled) {\n const result = callbacks.onToken\n ? await callLibreChatAgentStream(messages, { ...callbacks, signal })\n : await callLibreChatAgent(messages);\n return { ...result, provider, model };\n }\n if (codexRuntime.enabled && !preferredCodexFailed) {\n const result = await callCodexSdkDeepSeek(messages, {\n ...callbacks,\n user,\n signal,\n maxCompletionTokens,\n workingDirectory,\n sandboxMode,\n outputSchema,\n threadId,\n });\n return { ...result, provider, model };\n }\n const result = callbacks.onToken\n ? await callDeepSeekStream(messages, { ...callbacks, signal, maxCompletionTokens })\n : await callDeepSeek(messages, { maxCompletionTokens });\n return { ...result, provider, model };\n } catch (error) {\n if (!codexRuntime.enabled || preferredCodexFailed || isDeepSeekQuotaError(error)) throw error;\n logger.warn({ error: String(error) }, 'Agent model failed, falling back to DeepSeek direct');\n const result = callbacks.onToken\n ? await callDeepSeekStream(messages, { ...callbacks, signal, maxCompletionTokens })\n : await callDeepSeek(messages, { maxCompletionTokens });\n return { ...result, provider: 'deepseek', model: deepseekModel };\n }\n}\n\nfunction updateAgentRunStatus(run, status, fields = {}) {\n statements.updateAgentRunStatus.run({\n id: run.id,\n userId: run.user_id || run.userId,\n status,\n openClawRunId: fields.openClawRunId ?? null,\n inputTokens: fields.inputTokens ?? null,\n outputTokens: fields.outputTokens ?? null,\n toolCallCount: fields.toolCallCount ?? null,\n completedAt: status === 'completed' || status === 'cancelled' ? nowIso() : null,\n failedAt: status === 'failed' ? nowIso() : null,\n errorCode: fields.errorCode ?? null,\n errorMessage: fields.errorMessage ?? null,\n });\n}\n\nfunction buildAgentMessages(conversationId) {\n const historyRows = statements.listMessages.all(conversationId).slice(-14);\n return [{ role: 'system', content: systemPrompt }, ...normalizeMessages(historyRows)];\n}\n\nfunction agentUsagePayload(usage, user, budget) {\n const promptTokens = usage.promptTokens || usage.inputTokens || 0;\n const completionTokens = usage.completionTokens || usage.outputTokens || 0;\n const totalTokens = usage.totalTokens || promptTokens + completionTokens;\n const latestUsage = usageFor(user.userId);\n return {\n inputTokens: promptTokens,\n outputTokens: completionTokens,\n totalTokens,\n monthlyLimit: budget.limit,\n remainingTokens: Math.max(0, budget.limit - latestUsage.used),\n };\n}\n\nfunction openAiContentToText(content) {\n if (typeof content === 'string') return content;\n if (Array.isArray(content)) {\n return content\n .map((part) => {\n if (typeof part === 'string') return part;\n if (!part || typeof part !== 'object') return '';\n return readText(part.text || part.content || part.value);\n })\n .filter(Boolean)\n .join('\\n');\n }\n return readText(content);\n}\n\nfunction normalizeOpenAiMessages(value) {\n if (!Array.isArray(value)) return [];\n const allowedRoles = new Set(['system', 'user', 'assistant', 'tool']);\n return value\n .map((item) => {\n if (!item || typeof item !== 'object') return null;\n const role = allowedRoles.has(item.role) ? item.role : 'user';\n const content = openAiContentToText(item.content);\n if (!content) return null;\n return { role, content };\n })\n .filter(Boolean);\n}\n\nfunction truncateForDeepSeekMessage(text, maxChars = 12_000) {\n const value = readText(text);\n if (value.length <= maxChars) return value;\n return `${value.slice(0, maxChars)}\\n\\n[内容已截断]`;\n}\n\nfunction truncateTailForDeepSeekMessage(text, maxChars = 2400) {\n const value = readText(text);\n if (value.length <= maxChars) return value;\n return `[前文已省略]\\n\\n${value.slice(-maxChars)}`;\n}\n\nfunction stripOpenClawPromptNoise(text) {\n const value = readText(text);\n if (!value) return '';\n\n const finalPromptMatch = value.match(/finalPromptText[\"'\\s::]+([\\s\\S]{1,4000})$/i);\n if (finalPromptMatch?.[1]) {\n return readText(finalPromptMatch[1].replace(/^[\"'`]+|[\"'`,}\\]]+$/g, ''));\n }\n\n const markers = [\n 'final prompt:',\n 'final user prompt:',\n 'user prompt:',\n '用户问题:',\n '用户消息:',\n '用户输入:',\n ];\n const lower = value.toLowerCase();\n for (const marker of markers) {\n const index = lower.lastIndexOf(marker.toLowerCase());\n if (index >= 0) {\n const tail = value.slice(index + marker.length).trim();\n if (tail) return tail;\n }\n }\n\n return value;\n}\n\nfunction compactOpenClawMessagesForDeepSeek(messages) {\n const conversationalMessages = messages\n .filter((message) => message.role === 'user' || message.role === 'assistant')\n .map((message) => ({\n role: message.role,\n content: truncateTailForDeepSeekMessage(stripOpenClawPromptNoise(message.content)),\n }))\n .filter((message) => message.content);\n\n const recent = conversationalMessages.slice(-6);\n while (recent.length > 0 && recent[0].role !== 'user') {\n recent.shift();\n }\n\n if (recent.length === 0) {\n const fallback = messages\n .slice()\n .reverse()\n .map((message) => truncateTailForDeepSeekMessage(stripOpenClawPromptNoise(message.content), 2400))\n .find(Boolean);\n if (!fallback) return messages;\n recent.push({ role: 'user', content: fallback });\n }\n\n return [\n {\n role: 'system',\n content: '你是大乘 App 的桌面 AI 助手。请直接回答用户问题,默认使用简洁、准确的中文。',\n },\n ...recent,\n ];\n}\n\nfunction isDeepSeekBlockedError(error) {\n return (\n Number(error?.statusCode) === 403 &&\n /blocked|被拦截|forbidden/i.test(String(error?.message || error || ''))\n );\n}\n\nfunction shouldRetryWithCompactedOpenClawMessages(error, messages, compactedMessages) {\n if (!isDeepSeekBlockedError(error)) return false;\n return JSON.stringify(messages) !== JSON.stringify(compactedMessages);\n}\n\nfunction openClawBlockedFallbackText(messages) {\n const lastUser = messages\n .slice()\n .reverse()\n .find((message) => message.role === 'user')?.content || '';\n const localActionIntent =\n /(file|folder|directory|project|workspace|path|文件|文件夹|目录|项目|工程|路径|本地)/i.test(lastUser) &&\n /(create|make|mkdir|write|edit|modify|rename|delete|move|copy|save|download|read|list|生成|创建|新建|写入|编辑|修改|重命名|删除|移动|复制|保存|下载|读取|列出)/i.test(lastUser);\n\n if (localActionIntent) {\n return [\n '本机工具规划被模型服务拦截了,所以这次没有实际读取、创建、修改或下载本机文件。',\n '如果只是新建文件夹,请先在底部选择项目后重新发送“新建文件夹”;桌面端会直接在已选项目里创建。',\n '如果是更复杂的本地文件操作,请在“连应用”里重启本机 AI 后重试,或把要操作的文件路径和目标说得更具体。',\n ].join('\\n');\n }\n\n return '模型服务拦截了这次回复,未能生成结果。请换一种更具体、更短的表述后重试。';\n}\nfunction usageWithFallback(usage, messages, text) {\n const promptTokens = Number(usage?.promptTokens || usage?.inputTokens || 0);\n const completionTokens = Number(usage?.completionTokens || usage?.outputTokens || 0);\n const totalTokens = Number(usage?.totalTokens || 0);\n if (totalTokens > 0) {\n return {\n promptTokens,\n completionTokens,\n totalTokens,\n };\n }\n const estimatedPrompt = promptTokens || estimateTokens(JSON.stringify(messages));\n const estimatedCompletion = completionTokens || estimateTokens(text);\n return {\n promptTokens: estimatedPrompt,\n completionTokens: estimatedCompletion,\n totalTokens: estimatedPrompt + estimatedCompletion,\n };\n}\n\nfunction openAiUsagePayload(usage, user, budget) {\n const promptTokens = Number(usage.promptTokens || 0);\n const completionTokens = Number(usage.completionTokens || 0);\n const totalTokens = Number(usage.totalTokens || promptTokens + completionTokens);\n const latestUsage = usageFor(user.userId);\n const remainingTokens = Math.max(0, budget.limit - latestUsage.used);\n return {\n prompt_tokens: promptTokens,\n completion_tokens: completionTokens,\n total_tokens: totalTokens,\n promptTokens,\n completionTokens,\n totalTokens,\n monthlyLimit: budget.limit,\n remainingTokens,\n };\n}\n\nfunction writeOpenAiSse(res, payload) {\n res.write(`data: ${JSON.stringify(payload)}\\n\\n`);\n}\n\napp.post(\n '/api/openclaw/deepseek/v1/chat/completions',\n asyncHandler(async (req, res) => {\n const user = await resolveUser(req, req.body || {});\n const incomingMessages = normalizeOpenAiMessages(req.body?.messages);\n if (incomingMessages.length === 0) {\n res.status(400).json({ error: { message: 'messages is required' } });\n return;\n }\n\n const messages = compactOpenClawMessagesForDeepSeek(incomingMessages);\n const promptEstimate = estimateTokens(JSON.stringify(messages));\n const estimated = promptEstimate + 600;\n const budget = enforceTokenBudget(user, estimated);\n const requestCompletionLimit = deepseekMaxCompletionTokens;\n const maxCompletionTokens = Math.min(\n completionBudgetFor(budget, promptEstimate, requestCompletionLimit),\n normalizeMaxCompletionTokens(req.body?.max_tokens, requestCompletionLimit),\n );\n const model = normalizeDeepSeekModelName(req.body?.model);\n const wantsStream =\n req.body?.stream === true ||\n String(req.get('accept') || '').includes('text/event-stream');\n const id = `chatcmpl_${crypto.randomUUID().replaceAll('-', '')}`;\n const created = Math.floor(Date.now() / 1000);\n const compactedMessages = messages;\n\n if (!wantsStream) {\n let result;\n let usageMessages = messages;\n try {\n result = await callDeepSeek(messages, {\n model,\n maxCompletionTokens,\n maximumCompletionTokens: requestCompletionLimit,\n });\n } catch (error) {\n if (!shouldRetryWithCompactedOpenClawMessages(error, messages, compactedMessages)) {\n throw error;\n }\n logger.warn(\n {\n statusCode: error?.statusCode,\n messageCount: messages.length,\n compactedMessageCount: compactedMessages.length,\n },\n 'OpenClaw DeepSeek proxy retrying with compacted messages',\n );\n usageMessages = compactedMessages;\n result = await callDeepSeek(compactedMessages, {\n model,\n maxCompletionTokens,\n maximumCompletionTokens: requestCompletionLimit,\n });\n }\n const usage = usageWithFallback(result.usage, usageMessages, result.message);\n recordUsage(user.userId, usage.totalTokens);\n res.json({\n id,\n object: 'chat.completion',\n created,\n model: result.model || model,\n choices: [\n {\n index: 0,\n message: { role: 'assistant', content: result.message },\n finish_reason: 'stop',\n },\n ],\n usage: openAiUsagePayload(usage, user, budget),\n });\n return;\n }\n\n const abortController = new AbortController();\n req.on('close', () => abortController.abort());\n\n res.status(200);\n res.set({\n 'Content-Type': 'text/event-stream; charset=utf-8',\n 'Cache-Control': 'no-cache, no-transform',\n Connection: 'keep-alive',\n 'X-Accel-Buffering': 'no',\n });\n res.flushHeaders?.();\n\n const baseChunk = {\n id,\n object: 'chat.completion.chunk',\n created,\n model,\n system_fingerprint: 'fabushi-openclaw-deepseek-proxy',\n };\n writeOpenAiSse(res, {\n ...baseChunk,\n choices: [{ index: 0, delta: { role: 'assistant' }, finish_reason: null }],\n });\n\n let text = '';\n let usageMessages = messages;\n try {\n let result;\n try {\n result = await callDeepSeekStream(messages, {\n model,\n maxCompletionTokens,\n maximumCompletionTokens: requestCompletionLimit,\n signal: abortController.signal,\n onToken: (delta) => {\n text += delta;\n writeOpenAiSse(res, {\n ...baseChunk,\n choices: [{ index: 0, delta: { content: delta }, finish_reason: null }],\n });\n },\n });\n } catch (error) {\n if (!shouldRetryWithCompactedOpenClawMessages(error, messages, compactedMessages)) {\n throw error;\n }\n logger.warn(\n {\n statusCode: error?.statusCode,\n messageCount: messages.length,\n compactedMessageCount: compactedMessages.length,\n },\n 'OpenClaw DeepSeek proxy stream retrying with compacted messages',\n );\n text = '';\n usageMessages = compactedMessages;\n result = await callDeepSeekStream(compactedMessages, {\n model,\n maxCompletionTokens,\n maximumCompletionTokens: requestCompletionLimit,\n signal: abortController.signal,\n onToken: (delta) => {\n text += delta;\n writeOpenAiSse(res, {\n ...baseChunk,\n choices: [{ index: 0, delta: { content: delta }, finish_reason: null }],\n });\n },\n });\n }\n text = result.message || text;\n const usage = usageWithFallback(result.usage, usageMessages, text);\n recordUsage(user.userId, usage.totalTokens);\n writeOpenAiSse(res, {\n ...baseChunk,\n model: result.model || model,\n choices: [{ index: 0, delta: {}, finish_reason: 'stop' }],\n });\n writeOpenAiSse(res, {\n ...baseChunk,\n model: result.model || model,\n choices: [],\n usage: openAiUsagePayload(usage, user, budget),\n });\n res.write('data: [DONE]\\n\\n');\n } catch (error) {\n if (isDeepSeekBlockedError(error)) {\n const fallbackMessage = openClawBlockedFallbackText(usageMessages);\n const usage = usageWithFallback(null, usageMessages, fallbackMessage);\n recordUsage(user.userId, usage.totalTokens);\n logger.warn(\n { statusCode: error?.statusCode, code: error?.code },\n 'OpenClaw DeepSeek proxy stream blocked; returning local fallback message',\n );\n writeOpenAiSse(res, {\n ...baseChunk,\n choices: [{ index: 0, delta: { content: fallbackMessage }, finish_reason: null }],\n });\n writeOpenAiSse(res, {\n ...baseChunk,\n choices: [{ index: 0, delta: {}, finish_reason: 'stop' }],\n });\n writeOpenAiSse(res, {\n ...baseChunk,\n choices: [],\n usage: openAiUsagePayload(usage, user, budget),\n });\n res.write('data: [DONE]\\n\\n');\n return;\n }\n logger.error(\n { error: String(error), code: error?.code, statusCode: error?.statusCode },\n 'OpenClaw DeepSeek proxy stream failed',\n );\n writeOpenAiSse(res, {\n ...baseChunk,\n choices: [],\n error: {\n message: publicAiErrorMessage(error),\n code: error?.code || 'OPENCLAW_DEEPSEEK_PROXY_ERROR',\n },\n });\n res.write('data: [DONE]\\n\\n');\n } finally {\n res.end();\n }\n }),\n);\n\napp.post(\n '/codex-deepseek/v1/responses',\n asyncHandler(async (req, res) => {\n if (!deepseekApiKey) {\n res.status(401).json({\n error: { message: 'Unauthorized Codex DeepSeek adapter request' },\n });\n return;\n }\n\n const prompt = codexResponsesPrompt(req.body);\n if (!prompt) {\n res.status(400).json({ error: { message: 'Responses input is required' } });\n return;\n }\n\n const {\n user,\n recordUsage: shouldRecordUsage,\n maxCompletionTokens: adapterMaxCompletionTokens,\n } = await resolveCodexResponsesBilling(req);\n const messages = [{ role: 'user', content: prompt }];\n const promptEstimate = estimateTokens(JSON.stringify(messages));\n const budget = enforceTokenBudget(user, promptEstimate + 600);\n const responseCompletionLimit = adapterMaxCompletionTokens || deepseekMaxCompletionTokens;\n const maxCompletionTokens = Math.min(\n completionBudgetFor(budget, promptEstimate, responseCompletionLimit),\n normalizeMaxCompletionTokens(\n req.body?.max_output_tokens || req.body?.max_tokens,\n responseCompletionLimit,\n ),\n );\n const model = normalizeDeepSeekModelName(req.body?.model);\n const responseId = `resp_${crypto.randomUUID().replaceAll('-', '')}`;\n const itemId = `msg_${crypto.randomUUID().replaceAll('-', '')}`;\n const wantsStream =\n req.body?.stream !== false ||\n String(req.get('accept') || '').includes('text/event-stream');\n\n if (!wantsStream) {\n const result = await callDeepSeek(messages, {\n model,\n maxCompletionTokens,\n maximumCompletionTokens: responseCompletionLimit,\n });\n const usage = usageWithFallback(result.usage, messages, result.message);\n if (shouldRecordUsage) recordUsage(user.userId, usage.totalTokens);\n res.json(\n responsesPayload({\n responseId,\n itemId,\n status: 'completed',\n model: result.model || model,\n text: result.message,\n usage,\n }),\n );\n return;\n }\n\n res.status(200);\n res.set({\n 'Content-Type': 'text/event-stream; charset=utf-8',\n 'Cache-Control': 'no-cache, no-transform',\n Connection: 'keep-alive',\n 'X-Accel-Buffering': 'no',\n });\n res.flushHeaders?.();\n\n writeResponsesEvent(res, 'response.created', {\n type: 'response.created',\n response: responsesPayload({\n responseId,\n itemId,\n status: 'in_progress',\n model,\n }),\n });\n writeResponsesEvent(res, 'response.output_item.added', {\n type: 'response.output_item.added',\n output_index: 0,\n item: {\n id: itemId,\n type: 'message',\n role: 'assistant',\n status: 'in_progress',\n content: [],\n },\n });\n writeResponsesEvent(res, 'response.content_part.added', {\n type: 'response.content_part.added',\n item_id: itemId,\n output_index: 0,\n content_index: 0,\n part: { type: 'output_text', text: '' },\n });\n\n let text = '';\n try {\n const result = await callDeepSeekStream(messages, {\n model,\n maxCompletionTokens,\n maximumCompletionTokens: responseCompletionLimit,\n onToken: (delta) => {\n text += delta;\n writeResponsesEvent(res, 'response.output_text.delta', {\n type: 'response.output_text.delta',\n item_id: itemId,\n output_index: 0,\n content_index: 0,\n delta,\n });\n },\n });\n text = result.message || text;\n const usage = usageWithFallback(result.usage, messages, text);\n if (shouldRecordUsage) recordUsage(user.userId, usage.totalTokens);\n writeResponsesEvent(res, 'response.output_text.done', {\n type: 'response.output_text.done',\n item_id: itemId,\n output_index: 0,\n content_index: 0,\n text,\n });\n writeResponsesEvent(res, 'response.content_part.done', {\n type: 'response.content_part.done',\n item_id: itemId,\n output_index: 0,\n content_index: 0,\n part: { type: 'output_text', text },\n });\n writeResponsesEvent(res, 'response.output_item.done', {\n type: 'response.output_item.done',\n output_index: 0,\n item: {\n id: itemId,\n type: 'message',\n role: 'assistant',\n status: 'completed',\n content: [{ type: 'output_text', text }],\n },\n });\n writeResponsesEvent(res, 'response.completed', {\n type: 'response.completed',\n response: responsesPayload({\n responseId,\n itemId,\n status: 'completed',\n model: result.model || model,\n text,\n usage,\n }),\n });\n res.end();\n } catch (error) {\n writeResponsesEvent(res, 'response.failed', {\n type: 'response.failed',\n response: responsesPayload({\n responseId,\n itemId,\n status: 'failed',\n model,\n text,\n error,\n }),\n });\n res.end();\n }\n }),\n);\n\napp.use(\n '/api/openclaw/runtime/files',\n express.static(openClawRuntimeUpdatesDir, {\n fallthrough: false,\n immutable: true,\n maxAge: '7d',\n setHeaders(res) {\n res.setHeader('X-Content-Type-Options', 'nosniff');\n },\n }),\n);\n\napp.get(\n '/api/openclaw/runtime/manifest',\n asyncHandler(async (req, res) => {\n const platform = readText(req.query.platform);\n const currentVersion = readText(req.query.currentVersion);\n const appVersion = readText(req.query.appVersion);\n\n let manifest;\n try {\n manifest = await readJsonFile(openClawRuntimeManifestPath);\n } catch (error) {\n logger.info(\n { manifestPath: openClawRuntimeManifestPath, error: String(error) },\n 'OpenClaw runtime manifest unavailable',\n );\n return jsonResponse(res, 200, {\n success: true,\n schema: 1,\n updateAvailable: false,\n reason: 'manifest-unavailable',\n platform,\n currentVersion,\n appVersion,\n });\n }\n\n const latest = manifest.latest && typeof manifest.latest === 'object'\n ? manifest.latest\n : manifest;\n const platforms = latest.platforms && typeof latest.platforms === 'object'\n ? latest.platforms\n : {};\n const platformRelease = platform ? platforms[platform] : null;\n const version = readText(latest.version || manifest.version);\n\n if (!platform || !platformRelease || typeof platformRelease !== 'object') {\n return jsonResponse(res, 200, {\n success: true,\n schema: Number(manifest.schema || 1),\n channel: readText(manifest.channel || latest.channel || 'stable'),\n updateAvailable: false,\n reason: platform ? 'platform-unavailable' : 'platform-required',\n platform,\n currentVersion,\n appVersion,\n latestVersion: version,\n });\n }\n\n if (platform.startsWith('macos-') && !openClawRuntimeEnableMacosUpdates) {\n return jsonResponse(res, 200, {\n success: true,\n schema: Number(manifest.schema || 1),\n channel: readText(manifest.channel || latest.channel || 'stable'),\n updateAvailable: false,\n reason: 'macos-runtime-updates-disabled',\n platform,\n currentVersion,\n appVersion,\n latestVersion: version,\n });\n }\n\n const downloadUrl = absolutizeOpenClawRuntimeUrl(\n req,\n platformRelease.archiveUrl || platformRelease.url || platformRelease.fileName,\n );\n const release = {\n version,\n channel: readText(manifest.channel || latest.channel || 'stable'),\n minAppVersion: readText(latest.minAppVersion || platformRelease.minAppVersion),\n notes: readText(latest.notes || platformRelease.notes),\n nodeExecutable: readText(\n platformRelease.nodeExecutable || latest.nodeExecutable || 'node/bin/node',\n ),\n cliEntrypoint: readText(\n platformRelease.cliEntrypoint || latest.cliEntrypoint || 'openclaw/openclaw.mjs',\n ),\n defaultPort: Number(latest.defaultPort || manifest.defaultPort || 18789),\n defaultModel: readText(latest.defaultModel || manifest.defaultModel || 'openclaw/default'),\n defaultModelOverride: readText(latest.defaultModelOverride || manifest.defaultModelOverride),\n gatewayArgs: Array.isArray(latest.gatewayArgs)\n ? latest.gatewayArgs.map((item) => String(item))\n : ['gateway', '--port', '{port}', '--force'],\n archiveUrl: downloadUrl,\n sha256: readText(platformRelease.sha256),\n size: Number(platformRelease.size || 0),\n };\n\n jsonResponse(res, 200, {\n success: true,\n schema: Number(manifest.schema || 1),\n updateAvailable: Boolean(version && downloadUrl && version !== currentVersion),\n platform,\n currentVersion,\n appVersion,\n latest: release,\n generatedAt: readText(manifest.generatedAt || latest.generatedAt),\n });\n }),\n);\n\napp.get('/health', (_req, res) => {\n const codexRuntime = createCodexDeepSeekRuntime();\n const libreChatRuntime = createLibreChatAgentRuntime();\n const openClawRuntime = createOpenClawRuntime();\n jsonResponse(res, 200, {\n status: 'ok',\n service: 'dacheng-ai-backend',\n provider: libreChatRuntime.enabled ? libreChatRuntime.provider : 'deepseek',\n model: libreChatRuntime.enabled ? libreChatRuntime.model : deepseekModel,\n deepseekModel,\n deepseekAvailableModels,\n libreChatAgentChatEnabled: libreChatRuntime.enabled,\n libreChatAgentProvider: libreChatRuntime.provider,\n libreChatAgentReason: libreChatRuntime.enabled ? undefined : libreChatRuntime.reason,\n codexSdkAvailable: true,\n codexSdkChatEnabled: codexRuntime.enabled,\n codexSdkProvider: codexRuntime.provider,\n openClawAgentChatEnabled: openClawRuntime.enabled,\n openClawAgentProvider: openClawRuntime.provider,\n openClawAgentReason: openClawRuntime.enabled ? undefined : openClawRuntime.reason,\n requireMemberForAi,\n memberMonthlyLimit,\n freeMonthlyLimit,\n deepseekMaxCompletionTokens,\n botFatherMaxCompletionTokens,\n openClawRuntimeUpdatesDir,\n openClawRuntimeManifestConfigured: fs.existsSync(openClawRuntimeManifestPath),\n cbetaApiRoot,\n timestamp: nowIso(),\n });\n});\n\napp.get('/api/plugins/registry', (_req, res) => {\n jsonResponse(res, 200, {\n success: true,\n schemaVersion: 1,\n protocol: 'mcp',\n plugins: officialMcpApps,\n updatedAt: nowIso(),\n });\n});\n\napp.all('/api/mcp/apps/:pluginId', async (req, res) => {\n try {\n const user = await resolveUser(req, req.body || {});\n await handleOfficialMcpRequest(req.params.pluginId, req, res, user.userId);\n } catch (error) {\n logger.error({ error: error.stack || String(error) }, 'official MCP app request failed');\n if (!res.headersSent) {\n res.status(500).json({ error: 'MCP request failed', message: publicAiErrorMessage(error) });\n }\n }\n});\n\napp.post(\n '/api/codex/apps/:pluginId/turns',\n asyncHandler(async (req, res) => {\n const pluginId = safeUserText(req.params.pluginId).replace(/^official\\./, '');\n const plugin = officialMcpApps.find((candidate) => candidate.id === pluginId);\n if (!plugin) {\n return jsonResponse(res, 404, { success: false, message: 'plugin not found' });\n }\n\n const message = safeUserText(req.body?.message);\n if (!message) {\n return jsonResponse(res, 400, { success: false, message: 'message is required' });\n }\n\n const user = await resolveUser(req, req.body || {});\n const promptEstimate = estimateTokens(message);\n const budget = enforceTokenBudget(user, promptEstimate + 600);\n const maxCompletionTokens = completionBudgetFor(budget, promptEstimate);\n const namespace = pluginConversationNamespace(pluginId);\n const requestedConversationId = safeUserText(req.body?.conversationId);\n if (requestedConversationId && !requestedConversationId.startsWith(`${namespace}:`)) {\n return jsonResponse(res, 400, {\n success: false,\n message: 'conversation does not belong to the selected plugin namespace',\n });\n }\n\n const conversationId = requestedConversationId || `${namespace}:${crypto.randomUUID()}`;\n const createdAt = nowIso();\n let conversation = statements.getPluginConversation.get(\n conversationId,\n user.userId,\n namespace,\n );\n if (requestedConversationId && !conversation) {\n return jsonResponse(res, 404, {\n success: false,\n message: 'plugin conversation not found for this account',\n });\n }\n const isNewConversation = !conversation;\n if (isNewConversation) {\n conversation = {\n id: conversationId,\n user_id: user.userId,\n title: titleFromMessage(message),\n namespace,\n thread_id: null,\n };\n }\n\n const accountKey = sha256(user.userId).slice(0, 24);\n const pluginCodexHome = path.join(codexPluginSessionsDir, accountKey, pluginId);\n fs.mkdirSync(pluginCodexHome, { recursive: true });\n const mcpAccountToken = createCodexAdapterToken(user, {\n audience: 'mcp-plugin',\n recordUsage: false,\n });\n const mcpOrigin = (\n process.env.MCP_INTERNAL_BASE_URL || `http://127.0.0.1:${process.env.PORT || 8788}`\n ).replace(/\\/+$/, '');\n const isolationPolicy = createPluginCodexPolicy(pluginId, mcpOrigin);\n const firstTurnMessages = [\n {\n role: 'system',\n content:\n `你正在 ${plugin.title} 插件的隔离 Codex 会话中。` +\n '只能使用 current_plugin MCP Server 暴露的 Tools;禁止 shell、文件系统、Web 搜索和其他插件。' +\n '只读 Tool 可以按需调用。需要写入、外部操作或破坏性操作时,不要自行执行,明确告诉用户改用对应的 /Tool 命令,让宿主显示审批。',\n },\n { role: 'user', content: message },\n ];\n const aiResult = await callCodexSdkDeepSeek(\n conversation?.thread_id ? [{ role: 'user', content: message }] : firstTurnMessages,\n {\n user,\n threadId: conversation?.thread_id || undefined,\n maxCompletionTokens,\n sandboxMode: isolationPolicy.sandboxMode,\n approvalPolicy: isolationPolicy.approvalPolicy,\n networkAccessEnabled: isolationPolicy.networkAccessEnabled,\n webSearchMode: isolationPolicy.webSearchMode,\n codexHomeDir: pluginCodexHome,\n env: { [pluginMcpTokenEnv]: mcpAccountToken },\n config: isolationPolicy.config,\n },\n );\n const usage = {\n promptTokens: aiResult.usage.promptTokens || promptEstimate,\n completionTokens: aiResult.usage.completionTokens || estimateTokens(aiResult.message),\n totalTokens:\n aiResult.usage.totalTokens || promptEstimate + estimateTokens(aiResult.message),\n };\n if (!aiResult.usageAlreadyRecorded) recordUsage(user.userId, usage.totalTokens);\n\n const answeredAt = nowIso();\n if (isNewConversation) {\n statements.insertPluginConversation.run({\n id: conversationId,\n userId: user.userId,\n title: conversation.title,\n provider: codexDeepSeekProviderId,\n model: deepseekModel,\n namespace,\n createdAt,\n updatedAt: answeredAt,\n });\n }\n statements.insertMessage.run({\n id: crypto.randomUUID(),\n conversationId,\n role: 'user',\n content: message,\n promptTokens: 0,\n completionTokens: 0,\n totalTokens: 0,\n createdAt,\n });\n statements.insertMessage.run({\n id: crypto.randomUUID(),\n conversationId,\n role: 'assistant',\n content: aiResult.message,\n promptTokens: usage.promptTokens,\n completionTokens: usage.completionTokens,\n totalTokens: usage.totalTokens,\n createdAt: answeredAt,\n });\n statements.updatePluginConversation.run({\n id: conversationId,\n userId: user.userId,\n namespace,\n title: conversation.title,\n threadId: aiResult.threadId || conversation?.thread_id || null,\n updatedAt: answeredAt,\n });\n\n jsonResponse(res, 200, {\n success: true,\n pluginId,\n conversationId,\n message: aiResult.message,\n provider: codexDeepSeekProviderId,\n model: deepseekModel,\n usage: {\n ...usage,\n monthlyLimit: budget.limit,\n remainingTokens: Math.max(0, budget.limit - usageFor(user.userId).used),\n },\n });\n }),\n);\n\napp.get('/api/ai/models', (_req, res) => {\n jsonResponse(res, 200, {\n success: true,\n provider: 'deepseek',\n defaultModel: deepseekModel,\n models: deepseekAvailableModels.map((model) => ({\n id: model,\n label: model === 'deepseek-reasoner' ? 'DeepSeek Reasoner' : 'DeepSeek Chat',\n })),\n });\n});\napp.get(\n '/api/ai/quota',\n asyncHandler(async (req, res) => {\n const user = await resolveUser(req, { username: req.query.username });\n const budget = enforceTokenBudget(user, 0);\n const usage = usageFor(user.userId);\n jsonResponse(res, 200, {\n success: true,\n isMember: user.isMember,\n monthlyLimit: budget.limit,\n usedTokens: usage.used,\n remainingTokens: Math.max(0, budget.limit - usage.used),\n month: usage.month,\n });\n }),\n);\n\napp.post(\n '/api/agent/chat',\n asyncHandler(async (req, res) => {\n const message = safeUserText(req.body?.message);\n if (!message) {\n return jsonResponse(res, 400, { success: false, message: 'message is required' });\n }\n\n const user = await resolveUser(req, req.body || {});\n const promptEstimate = estimateTokens(message);\n const estimated = promptEstimate + 600;\n const budget = enforceTokenBudget(user, estimated);\n const maxCompletionTokens = completionBudgetFor(budget, promptEstimate);\n const createdAt = nowIso();\n const conversationId = safeUserText(req.body?.conversationId) || crypto.randomUUID();\n const mode = normalizeAgentMode(req.body?.mode);\n const stream = req.body?.stream === true;\n let conversation = statements.getConversation.get(conversationId, user.userId);\n const isNewConversation = !conversation;\n\n if (isNewConversation) {\n conversation = { id: conversationId, user_id: user.userId, title: titleFromMessage(message) };\n statements.insertConversation.run({\n id: conversationId,\n userId: user.userId,\n title: conversation.title,\n provider: 'agent',\n model: openClawAgentId,\n createdAt,\n updatedAt: createdAt,\n });\n }\n\n const messageId = safeUserText(req.body?.messageId) || crypto.randomUUID();\n statements.insertMessage.run({\n id: messageId,\n conversationId,\n role: 'user',\n content: message,\n promptTokens: 0,\n completionTokens: 0,\n totalTokens: 0,\n createdAt,\n });\n\n const runId = `run_${crypto.randomUUID().replaceAll('-', '')}`;\n const sessionKey = openClawSessionKey(user, conversationId);\n const openClawRuntime = createOpenClawRuntime();\n statements.insertAgentRun.run({\n id: runId,\n userId: user.userId,\n conversationId,\n messageId,\n openClawRunId: null,\n openClawSessionKey: sessionKey,\n status: stream ? 'queued' : 'running',\n mode,\n provider: openClawRuntime.enabled ? openClawRuntime.provider : 'dacheng-ai-fallback',\n model: openClawRuntime.enabled ? openClawAgentId : deepseekModel,\n startedAt: createdAt,\n });\n\n if (stream) {\n return jsonResponse(res, 200, {\n success: true,\n runId,\n conversationId,\n streamUrl: `/api/agent/runs/${runId}/events`,\n });\n }\n\n const controller = new AbortController();\n activeAgentRuns.set(runId, controller);\n try {\n const skillResult = await runResourceFinderSkill({ message, user });\n const skillContext = resourceContextMessage(skillResult);\n const historyRows = statements.listMessages.all(conversationId).slice(-14);\n const modelMessages = [\n { role: 'system', content: systemPrompt },\n ...(skillContext ? [{ role: 'system', content: skillContext }] : []),\n ...normalizeMessages(historyRows),\n ];\n\n const aiResult = await runAgentModel({\n messages: modelMessages,\n user,\n conversationId,\n mode,\n signal: controller.signal,\n maxCompletionTokens,\n });\n const usage = {\n promptTokens: aiResult.usage?.promptTokens || estimateTokens(JSON.stringify(modelMessages)),\n completionTokens: aiResult.usage?.completionTokens || estimateTokens(aiResult.message),\n totalTokens:\n aiResult.usage?.totalTokens ||\n estimateTokens(JSON.stringify(modelMessages)) + estimateTokens(aiResult.message),\n };\n if (!aiResult.usageAlreadyRecorded) recordUsage(user.userId, usage.totalTokens);\n const answeredAt = nowIso();\n const assistantMessageId = crypto.randomUUID();\n statements.insertMessage.run({\n id: assistantMessageId,\n conversationId,\n role: 'assistant',\n content: aiResult.message,\n promptTokens: usage.promptTokens,\n completionTokens: usage.completionTokens,\n totalTokens: usage.totalTokens,\n createdAt: answeredAt,\n });\n statements.updateConversation.run({\n id: conversationId,\n userId: user.userId,\n title: isNewConversation ? titleFromMessage(message) : conversation.title,\n updatedAt: answeredAt,\n });\n updateAgentRunStatus({ id: runId, user_id: user.userId }, 'completed', {\n openClawRunId: aiResult.runId,\n inputTokens: usage.promptTokens,\n outputTokens: usage.completionTokens,\n });\n const action = determineClientAction(skillResult, message);\n return jsonResponse(res, 200, {\n success: true,\n runId,\n conversationId,\n messageId: assistantMessageId,\n provider: aiResult.provider,\n model: aiResult.model,\n message: aiResult.message,\n usage: agentUsagePayload(usage, user, budget),\n ...(action ? { clientAction: action } : {}),\n });\n } catch (error) {\n const status = error.name === 'AbortError' ? 'cancelled' : 'failed';\n updateAgentRunStatus({ id: runId, user_id: user.userId }, status, {\n errorCode: error.name || 'AGENT_RUN_FAILED',\n errorMessage: publicAiErrorMessage(error),\n });\n if (status === 'cancelled') {\n return jsonResponse(res, 499, { success: false, runId, status: 'cancelled' });\n }\n throw error;\n } finally {\n activeAgentRuns.delete(runId);\n }\n }),\n);\n\napp.get(\n '/api/agent/runs/:runId/events',\n asyncHandler(async (req, res) => {\n const user = await resolveUser(req, {});\n const run = statements.getAgentRun.get(req.params.runId, user.userId);\n if (!run) {\n return jsonResponse(res, 404, { success: false, message: 'run not found' });\n }\n if (run.status === 'completed' || run.status === 'failed' || run.status === 'cancelled') {\n return jsonResponse(res, 409, { success: false, message: `run is already ${run.status}` });\n }\n\n sseHeaders(res);\n const controller = new AbortController();\n activeAgentRuns.set(run.id, controller);\n updateAgentRunStatus(run, 'running');\n writeSse(res, 'run.started', { runId: run.id, conversationId: run.conversation_id });\n\n try {\n let streamedText = '';\n const modelMessages = buildAgentMessages(run.conversation_id);\n const lastUserMsg = [...modelMessages].reverse().find(m => m.role === 'user');\n const userMsgText = lastUserMsg ? lastUserMsg.content : '';\n const skillResult = userMsgText ? await runResourceFinderSkill({\n message: userMsgText,\n user,\n onStep: (step) => writeSse(res, 'tool.call.completed', {\n tool: 'agent.step',\n displayName: step.title || 'Agent step',\n summary: step.message || '',\n }),\n }) : null;\n const skillContext = resourceContextMessage(skillResult);\n const finalMessages = skillContext ? [\n modelMessages[0],\n { role: 'system', content: skillContext },\n ...modelMessages.slice(1)\n ] : modelMessages;\n\n const promptEstimate = estimateTokens(JSON.stringify(finalMessages));\n const estimated = promptEstimate + 600;\n const budget = enforceTokenBudget(user, estimated);\n const maxCompletionTokens = completionBudgetFor(budget, promptEstimate);\n const aiResult = await runAgentModel({\n messages: finalMessages,\n user,\n conversationId: run.conversation_id,\n mode: run.mode,\n signal: controller.signal,\n maxCompletionTokens,\n callbacks: {\n onStep: (step) => writeSse(res, 'tool.call.completed', {\n tool: 'agent.step',\n displayName: step.title || 'Agent step',\n summary: step.message || '',\n }),\n onToken: (delta) => {\n streamedText += delta;\n writeSse(res, 'assistant.delta', { text: delta });\n },\n },\n });\n const message = aiResult.message || streamedText.trim();\n const usage = {\n promptTokens: aiResult.usage?.promptTokens || estimateTokens(JSON.stringify(finalMessages)),\n completionTokens: aiResult.usage?.completionTokens || estimateTokens(message),\n totalTokens:\n aiResult.usage?.totalTokens || estimateTokens(JSON.stringify(finalMessages)) + estimateTokens(message),\n };\n if (!aiResult.usageAlreadyRecorded) recordUsage(user.userId, usage.totalTokens);\n const assistantMessageId = crypto.randomUUID();\n const answeredAt = nowIso();\n statements.insertMessage.run({\n id: assistantMessageId,\n conversationId: run.conversation_id,\n role: 'assistant',\n content: message,\n promptTokens: usage.promptTokens,\n completionTokens: usage.completionTokens,\n totalTokens: usage.totalTokens,\n createdAt: answeredAt,\n });\n statements.updateConversation.run({\n id: run.conversation_id,\n userId: user.userId,\n title: titleFromMessage(message),\n updatedAt: answeredAt,\n });\n updateAgentRunStatus(run, 'completed', {\n openClawRunId: aiResult.runId,\n inputTokens: usage.promptTokens,\n outputTokens: usage.completionTokens,\n });\n const action = determineClientAction(skillResult, userMsgText);\n writeSse(res, 'assistant.message', { messageId: assistantMessageId, content: message });\n writeSse(res, 'run.completed', {\n usage: agentUsagePayload(usage, user, budget),\n ...(action ? { clientAction: action } : {}),\n });\n res.end();\n } catch (error) {\n const status = error.name === 'AbortError' ? 'cancelled' : 'failed';\n updateAgentRunStatus(run, status, {\n errorCode: error.name || 'AGENT_RUN_FAILED',\n errorMessage: publicAiErrorMessage(error),\n });\n writeSse(res, status === 'cancelled' ? 'run.cancelled' : 'run.failed', {\n runId: run.id,\n message: publicAiErrorMessage(error),\n });\n res.end();\n } finally {\n activeAgentRuns.delete(run.id);\n }\n }),\n);\n\napp.post(\n '/api/agent/runs/:runId/cancel',\n asyncHandler(async (req, res) => {\n const user = await resolveUser(req, req.body || {});\n const run = statements.getAgentRun.get(req.params.runId, user.userId);\n if (!run) {\n return jsonResponse(res, 404, { success: false, message: 'run not found' });\n }\n const controller = activeAgentRuns.get(run.id);\n if (controller) controller.abort();\n updateAgentRunStatus(run, 'cancelled');\n return jsonResponse(res, 200, { success: true, runId: run.id, status: 'cancelled' });\n }),\n);\n\napp.post(\n '/api/agent/messages/:messageId/feedback',\n asyncHandler(async (req, res) => {\n const user = await resolveUser(req, req.body || {});\n const rating = safeUserText(req.body?.rating);\n if (!['up', 'down'].includes(rating)) {\n return jsonResponse(res, 400, { success: false, message: 'rating must be up or down' });\n }\n statements.insertAgentFeedback.run({\n id: crypto.randomUUID(),\n messageId: req.params.messageId,\n userId: user.userId,\n rating,\n reason: safeUserText(req.body?.reason),\n comment: safeUserText(req.body?.comment),\n createdAt: nowIso(),\n });\n return jsonResponse(res, 200, { success: true });\n }),\n);\n\napp.post(\n '/api/ai/chat',\n asyncHandler(async (req, res) => {\n const message = safeUserText(req.body?.message);\n if (!message) {\n return jsonResponse(res, 400, { success: false, message: 'message is required' });\n }\n\n const user = await resolveUser(req, req.body || {});\n const promptEstimate = estimateTokens(message);\n const estimated = promptEstimate + 600;\n const budget = enforceTokenBudget(user, estimated);\n const maxCompletionTokens = completionBudgetFor(budget, promptEstimate);\n const createdAt = nowIso();\n const conversationId = safeUserText(req.body?.conversationId) || crypto.randomUUID();\n const requestedModel = normalizeDeepSeekModelName(req.body?.model);\n const clientContext = clientContextMessage(req.body?.client);\n let conversation = statements.getConversation.get(conversationId, user.userId);\n const isNewConversation = !conversation;\n\n if (isNewConversation) {\n conversation = {\n id: conversationId,\n user_id: user.userId,\n title: titleFromMessage(message),\n };\n statements.insertConversation.run({\n id: conversationId,\n userId: user.userId,\n title: conversation.title,\n provider: 'deepseek',\n model: requestedModel,\n createdAt,\n updatedAt: createdAt,\n });\n }\n\n statements.insertMessage.run({\n id: crypto.randomUUID(),\n conversationId,\n role: 'user',\n content: message,\n promptTokens: 0,\n completionTokens: 0,\n totalTokens: 0,\n createdAt,\n });\n\n const libreChatRuntime = createLibreChatAgentRuntime();\n const skillResult = await runResourceFinderSkill({ message, user });\n const skillContext = resourceContextMessage(skillResult);\n const historyRows = statements.listMessages.all(conversationId).slice(-14);\n const modelMessages = [\n { role: 'system', content: systemPrompt },\n ...(clientContext ? [{ role: 'system', content: clientContext }] : []),\n ...(skillContext ? [{ role: 'system', content: skillContext }] : []),\n ...normalizeMessages(historyRows),\n ];\n\n const openClawRuntime = createOpenClawRuntime();\n const codexRuntime = createCodexDeepSeekRuntime(user);\n let provider = openClawRuntime.enabled\n ? openClawRuntime.provider\n : libreChatRuntime.enabled\n ? libreChatRuntime.provider\n : 'deepseek';\n let responseModel = openClawRuntime.enabled\n ? openClawRuntime.model\n : libreChatRuntime.enabled\n ? libreChatRuntime.model\n : requestedModel;\n let aiResult;\n try {\n if (openClawRuntime.enabled) {\n aiResult = await callOpenClawAgent({\n messages: modelMessages,\n user,\n conversationId,\n mode: normalizeAgentMode(req.body?.mode),\n });\n provider = aiResult.provider || provider;\n responseModel = aiResult.model || responseModel;\n } else if (libreChatRuntime.enabled) {\n aiResult = await callLibreChatAgent(modelMessages);\n } else if (codexRuntime.enabled) {\n provider = codexRuntime.provider;\n aiResult = await callCodexSdkDeepSeek(modelMessages, { user, maxCompletionTokens });\n } else {\n aiResult = await callDeepSeek(modelMessages, { model: requestedModel, maxCompletionTokens });\n }\n } catch (error) {\n if (libreChatRuntime.enabled) {\n logger.warn({ error: String(error) }, 'LibreChat Agent chat failed, falling back to Codex SDK or DeepSeek direct');\n provider = codexRuntime.enabled ? codexRuntime.provider : 'deepseek';\n responseModel = requestedModel;\n if (codexRuntime.enabled) {\n try {\n aiResult = await callCodexSdkDeepSeek(modelMessages, { user, maxCompletionTokens });\n } catch (codexError) {\n if (isDeepSeekQuotaError(codexError)) throw codexError;\n logger.warn({ error: String(codexError) }, 'Codex SDK chat failed, falling back to DeepSeek direct');\n provider = 'deepseek';\n aiResult = await callDeepSeek(modelMessages, { model: requestedModel, maxCompletionTokens });\n }\n } else {\n aiResult = await callDeepSeek(modelMessages, { model: requestedModel, maxCompletionTokens });\n }\n } else {\n if (!codexRuntime.enabled || isDeepSeekQuotaError(error)) throw error;\n logger.warn({ error: String(error) }, 'Codex SDK chat failed, falling back to DeepSeek direct');\n provider = 'deepseek';\n responseModel = requestedModel;\n aiResult = await callDeepSeek(modelMessages, { model: requestedModel, maxCompletionTokens });\n }\n }\n const usage = {\n promptTokens: aiResult.usage.promptTokens || estimateTokens(JSON.stringify(modelMessages)),\n completionTokens: aiResult.usage.completionTokens || estimateTokens(aiResult.message),\n totalTokens:\n aiResult.usage.totalTokens ||\n estimateTokens(JSON.stringify(modelMessages)) + estimateTokens(aiResult.message),\n };\n if (!aiResult.usageAlreadyRecorded) recordUsage(user.userId, usage.totalTokens);\n\n const answeredAt = nowIso();\n statements.insertMessage.run({\n id: crypto.randomUUID(),\n conversationId,\n role: 'assistant',\n content: aiResult.message,\n promptTokens: usage.promptTokens,\n completionTokens: usage.completionTokens,\n totalTokens: usage.totalTokens,\n createdAt: answeredAt,\n });\n statements.updateConversation.run({\n id: conversationId,\n userId: user.userId,\n title: isNewConversation ? titleFromMessage(message) : conversation.title,\n updatedAt: answeredAt,\n });\n\n const latestUsage = usageFor(user.userId);\n const monthlyLimit = budget.limit;\n const action = determineClientAction(skillResult, message);\n jsonResponse(res, 200, {\n success: true,\n conversationId,\n provider,\n model: responseModel,\n message: aiResult.message,\n usage: {\n ...usage,\n monthlyLimit,\n remainingTokens: Math.max(0, monthlyLimit - latestUsage.used),\n },\n ...(action ? { clientAction: action } : {}),\n });\n }),\n);\n\napp.post('/api/ai/chat/stream', async (req, res) => {\n sseHeaders(res);\n\n try {\n const message = safeUserText(req.body?.message);\n if (!message) {\n writeSse(res, 'error', { message: 'message is required' });\n res.end();\n return;\n }\n\n const user = await resolveUser(req, req.body || {});\n const promptEstimate = estimateTokens(message);\n const estimated = promptEstimate + 600;\n const budget = enforceTokenBudget(user, estimated);\n const maxCompletionTokens = completionBudgetFor(budget, promptEstimate);\n const createdAt = nowIso();\n const conversationId = safeUserText(req.body?.conversationId) || crypto.randomUUID();\n const requestedModel = normalizeDeepSeekModelName(req.body?.model);\n const clientContext = clientContextMessage(req.body?.client);\n let conversation = statements.getConversation.get(conversationId, user.userId);\n const isNewConversation = !conversation;\n\n if (isNewConversation) {\n conversation = {\n id: conversationId,\n user_id: user.userId,\n title: titleFromMessage(message),\n };\n statements.insertConversation.run({\n id: conversationId,\n userId: user.userId,\n title: conversation.title,\n provider: 'deepseek',\n model: requestedModel,\n createdAt,\n updatedAt: createdAt,\n });\n }\n\n statements.insertMessage.run({\n id: crypto.randomUUID(),\n conversationId,\n role: 'user',\n content: message,\n promptTokens: 0,\n completionTokens: 0,\n totalTokens: 0,\n createdAt,\n });\n\n const codexRuntime = createCodexDeepSeekRuntime(user);\n const libreChatRuntime = createLibreChatAgentRuntime();\n const initialOpenClawRuntime = createOpenClawRuntime();\n let provider = initialOpenClawRuntime.enabled\n ? initialOpenClawRuntime.provider\n : libreChatRuntime.enabled\n ? libreChatRuntime.provider\n : codexRuntime.enabled\n ? codexRuntime.provider\n : 'deepseek';\n let responseModel = initialOpenClawRuntime.enabled\n ? initialOpenClawRuntime.model\n : libreChatRuntime.enabled\n ? libreChatRuntime.model\n : requestedModel;\n writeSse(res, 'meta', {\n conversationId,\n provider,\n model: responseModel,\n });\n const skillResult = await runResourceFinderSkill({\n message,\n user,\n onStep: (step) => writeSse(res, 'step', step),\n });\n const skillContext = resourceContextMessage(skillResult);\n const historyRows = statements.listMessages.all(conversationId).slice(-14);\n const modelMessages = [\n { role: 'system', content: systemPrompt },\n ...(clientContext ? [{ role: 'system', content: clientContext }] : []),\n ...(skillContext ? [{ role: 'system', content: skillContext }] : []),\n ...normalizeMessages(historyRows),\n ];\n\n let aiResult;\n const openClawRuntime = initialOpenClawRuntime;\n try {\n if (openClawRuntime.enabled) {\n aiResult = await callOpenClawAgent({\n messages: modelMessages,\n user,\n conversationId,\n mode: normalizeAgentMode(req.body?.mode),\n });\n provider = aiResult.provider || provider;\n responseModel = aiResult.model || responseModel;\n writeSse(res, 'delta', { text: aiResult.message });\n } else if (libreChatRuntime.enabled) {\n aiResult = await callLibreChatAgentStream(modelMessages, {\n onToken: (token) => writeSse(res, 'delta', { text: token }),\n });\n } else if (codexRuntime.enabled) {\n aiResult = await callCodexSdkDeepSeek(modelMessages, {\n user,\n maxCompletionTokens,\n onToken: (token) => writeSse(res, 'delta', { text: token }),\n onStep: (step) => writeSse(res, 'step', step),\n });\n } else {\n aiResult = await callDeepSeekStream(modelMessages, {\n model: requestedModel,\n maxCompletionTokens,\n onToken: (token) => writeSse(res, 'delta', { text: token }),\n });\n }\n } catch (error) {\n if (libreChatRuntime.enabled) {\n logger.warn({ error: String(error) }, 'LibreChat Agent streaming chat failed, falling back to Codex SDK or DeepSeek direct');\n provider = codexRuntime.enabled ? codexRuntime.provider : 'deepseek';\n responseModel = requestedModel;\n writeSse(res, 'meta', {\n conversationId,\n provider,\n model: responseModel,\n });\n if (codexRuntime.enabled) {\n try {\n aiResult = await callCodexSdkDeepSeek(modelMessages, {\n user,\n maxCompletionTokens,\n onToken: (token) => writeSse(res, 'delta', { text: token }),\n onStep: (step) => writeSse(res, 'step', step),\n });\n } catch (codexError) {\n if (isDeepSeekQuotaError(codexError)) throw codexError;\n logger.warn({ error: String(codexError) }, 'Codex SDK streaming chat failed, falling back to DeepSeek direct');\n provider = 'deepseek';\n responseModel = requestedModel;\n writeSse(res, 'meta', {\n conversationId,\n provider,\n model: responseModel,\n });\n aiResult = await callDeepSeekStream(modelMessages, {\n model: requestedModel,\n maxCompletionTokens,\n onToken: (token) => writeSse(res, 'delta', { text: token }),\n });\n }\n } else {\n aiResult = await callDeepSeekStream(modelMessages, {\n model: requestedModel,\n maxCompletionTokens,\n onToken: (token) => writeSse(res, 'delta', { text: token }),\n });\n }\n } else {\n if (!codexRuntime.enabled || isDeepSeekQuotaError(error)) throw error;\n logger.warn({ error: String(error) }, 'Codex SDK streaming chat failed, falling back to DeepSeek direct');\n provider = 'deepseek';\n responseModel = requestedModel;\n writeSse(res, 'meta', {\n conversationId,\n provider,\n model: responseModel,\n });\n aiResult = await callDeepSeekStream(modelMessages, {\n model: requestedModel,\n maxCompletionTokens,\n onToken: (token) => writeSse(res, 'delta', { text: token }),\n });\n }\n }\n\n const usage = {\n promptTokens: aiResult.usage.promptTokens || estimateTokens(JSON.stringify(modelMessages)),\n completionTokens: aiResult.usage.completionTokens || estimateTokens(aiResult.message),\n totalTokens:\n aiResult.usage.totalTokens ||\n estimateTokens(JSON.stringify(modelMessages)) + estimateTokens(aiResult.message),\n };\n if (!aiResult.usageAlreadyRecorded) recordUsage(user.userId, usage.totalTokens);\n\n const answeredAt = nowIso();\n statements.insertMessage.run({\n id: crypto.randomUUID(),\n conversationId,\n role: 'assistant',\n content: aiResult.message,\n promptTokens: usage.promptTokens,\n completionTokens: usage.completionTokens,\n totalTokens: usage.totalTokens,\n createdAt: answeredAt,\n });\n statements.updateConversation.run({\n id: conversationId,\n userId: user.userId,\n title: isNewConversation ? titleFromMessage(message) : conversation.title,\n updatedAt: answeredAt,\n });\n\n const latestUsage = usageFor(user.userId);\n const monthlyLimit = budget.limit;\n const action = determineClientAction(skillResult, message);\n writeSse(res, 'done', {\n conversationId,\n provider,\n model: responseModel,\n message: aiResult.message,\n usage: {\n ...usage,\n monthlyLimit,\n remainingTokens: Math.max(0, monthlyLimit - latestUsage.used),\n },\n ...(action ? { clientAction: action } : {}),\n });\n res.end();\n } catch (error) {\n logger.error({ error: error.stack || String(error) }, 'streaming chat failed');\n writeSse(res, 'error', {\n message: publicAiErrorMessage(error),\n details: error.details,\n });\n res.end();\n }\n});\n\napp.get(\n '/api/ai/conversations',\n asyncHandler(async (req, res) => {\n const user = await resolveUser(req, { username: req.query.username });\n const items = statements.listConversations.all(user.userId).map((item) => ({\n id: item.id,\n title: item.title,\n updatedAt: item.updatedAt,\n }));\n jsonResponse(res, 200, { success: true, items });\n }),\n);\n\napp.get(\n '/api/ai/conversations/:id',\n asyncHandler(async (req, res) => {\n const user = await resolveUser(req);\n const conversation = statements.getConversation.get(req.params.id, user.userId);\n if (!conversation) {\n return jsonResponse(res, 404, { success: false, message: 'conversation not found' });\n }\n const messages = statements.listMessages.all(req.params.id).map((item) => ({\n role: item.role,\n content: item.content,\n createdAt: item.createdAt,\n }));\n jsonResponse(res, 200, {\n success: true,\n conversation: {\n id: conversation.id,\n title: conversation.title,\n updatedAt: conversation.updated_at,\n },\n messages,\n });\n }),\n);\n\nfunction buildUrl(base, endpoint, params = {}) {\n const url = new URL(endpoint, `${base}/`);\n for (const [key, value] of Object.entries(params)) {\n if (Array.isArray(value)) {\n for (const item of value) {\n if (item !== undefined && item !== null && item !== '') {\n url.searchParams.append(key, String(item));\n }\n }\n } else if (value !== undefined && value !== null && value !== '') {\n url.searchParams.set(key, String(value));\n }\n }\n return url;\n}\n\nasync function fetchJson(url) {\n const response = await fetch(url, {\n headers: { Accept: 'application/json' },\n signal: AbortSignal.timeout(15_000),\n });\n if (!response.ok) {\n throw new Error(`Request failed: ${response.status}`);\n }\n return response.json();\n}\n\nfunction stripHtml(value) {\n return String(value || '')\n .replace(/<script[\\s\\S]*?<\\/script>/gi, ' ')\n .replace(/<style[\\s\\S]*?<\\/style>/gi, ' ')\n .replace(/<[^>]+>/g, ' ')\n .replace(/ /g, ' ')\n .replace(/</g, '<')\n .replace(/>/g, '>')\n .replace(/&/g, '&')\n .replace(/&#(\\d+);/g, (_match, code) => String.fromCharCode(Number(code)))\n .replace(/\\s+/g, ' ')\n .trim();\n}\n\nfunction cleanExtractedText(value) {\n let text = String(value || '')\n .replace(/\\b[A-Z]{1,3}\\d{2}n\\d{4,5}[a-z]?_p?\\d{4}[a-z]\\d{2}\\b/g, ' ')\n .replace(/\\uFFFD/g, '')\n .replace(/\\s+([,。;:!?、」』)》】])/g, '$1')\n .replace(/([「『《(【])\\s+/g, '$1');\n for (let i = 0; i < 3; i += 1) {\n text = text.replace(/([\\u3400-\\u9fff])\\s+([\\u3400-\\u9fff])/g, '$1$2');\n }\n return text.replace(/[ \\t]{2,}/g, ' ').trim();\n}\n\nfunction compactText(value, limit = 140) {\n const text = stripHtml(value);\n return text.length > limit ? `${text.slice(0, limit)}...` : text;\n}\n\nfunction collectSearchResults(data) {\n if (!data || typeof data !== 'object') return [];\n if (Array.isArray(data.results)) return data.results;\n if (Array.isArray(data?.results?.docs)) return data.results.docs;\n if (Array.isArray(data?.response?.docs)) return data.response.docs;\n if (Array.isArray(data.docs)) return data.docs;\n return [];\n}\n\nfunction firstText(item, keys) {\n for (const key of keys) {\n const value = item?.[key];\n if (Array.isArray(value)) {\n const first = value.map((entry) => safeUserText(String(entry))).find(Boolean);\n if (first) return first;\n }\n if (typeof value === 'string' && value.trim()) return value.trim();\n if (typeof value === 'number') return String(value);\n }\n return '';\n}\n\nfunction firstNumber(item, keys, fallback = undefined) {\n for (const key of keys) {\n const value = item?.[key];\n if (typeof value === 'number') return value;\n if (typeof value === 'string' && value.trim() && Number.isFinite(Number(value))) {\n return Number(value);\n }\n }\n return fallback;\n}\n\nasync function searchCbetaTitle(query, limit) {\n const url = buildUrl(cbetaApiRoot, 'search/title', { q: query, start: 0, rows: limit });\n const data = await fetchJson(url);\n return collectSearchResults(data).flatMap((item) => {\n const work = firstText(item, ['work', 'work_id', 'workId']);\n const title = firstText(item, ['content', 'title', 'work_title', 'workTitle']);\n if (!work || !title) return [];\n const juan = firstNumber(item, ['juan', 'juan_num', 'juanNum'], 1);\n return [\n {\n id: `cbeta-title-${work}-${juan}`,\n title,\n sourceName: 'CBETA 佛典',\n url: `cbeta:${work}:${juan}`,\n snippet: firstText(item, ['byline', 'creators', 'time_dynasty']) || '可下载经文正文并用于全球法布施。',\n resourceType: 'scripture',\n work,\n juan,\n },\n ];\n });\n}\n\nasync function searchCbetaContent(query, limit) {\n const endpoints = ['search/fulltext', 'search/content', 'search/all_in_one'];\n const settled = await Promise.allSettled(\n endpoints.map((endpoint) =>\n fetchJson(buildUrl(cbetaApiRoot, endpoint, { q: query, start: 0, rows: limit, field: 'content' })),\n ),\n );\n const results = [];\n for (const response of settled) {\n if (response.status !== 'fulfilled') continue;\n for (const item of collectSearchResults(response.value)) {\n const work = firstText(item, ['work', 'work_id', 'workId', 'sutra', 'id']);\n const snippet = compactText(firstText(item, ['kwic', 'highlight', 'snippet', 'content', 'text', 'body', 'p']));\n if (!work || !snippet) continue;\n const juan = firstNumber(item, ['juan', 'juan_num', 'juanNum', '卷'], 1);\n const title = firstText(item, ['title', 'work_title', 'workTitle', 'sutra_name', 'book', 'name']) || work;\n results.push({\n id: `cbeta-content-${work}-${juan}-${sha256(snippet).slice(0, 8)}`,\n title,\n sourceName: 'CBETA 佛典',\n url: `cbeta:${work}:${juan}`,\n snippet,\n resourceType: 'scripture',\n work,\n juan,\n });\n if (results.length >= limit) return results;\n }\n }\n return results;\n}\n\nfunction directUrlResource(query) {\n const url = safeUserText(query);\n let parsed;\n try {\n parsed = new URL(url);\n } catch {\n return null;\n }\n if (!['http:', 'https:'].includes(parsed.protocol)) return null;\n return {\n id: `url-${sha256(url).slice(0, 12)}`,\n title: parsed.hostname,\n sourceName: '网页资源',\n url,\n snippet: '直接下载并提取这个链接的可读正文。',\n resourceType: 'web',\n };\n}\n\nfunction looksLikeAudioQuery(query) {\n return /音乐|音樂|音频|音訊|mp3|audio|念诵|念誦|诵经|誦經|唱诵|唱誦|读诵|讀誦/.test(\n safeUserText(query).toLowerCase(),\n );\n}\n\nfunction archiveSearchTerm(query) {\n return (\n safeUserText(query)\n .replace(/下载|查找|寻找|找一?个|资源|音频|音訊|音乐|音樂|mp3|audio|念诵|念誦|诵经|誦經|唱诵|唱誦|读诵|讀誦/gi, ' ')\n .replace(/\\s+/g, ' ')\n .trim() || safeUserText(query)\n );\n}\n\nfunction solrPhrase(value) {\n return String(value).replace(/\\\\/g, '\\\\\\\\').replace(/\"/g, '\\\\\"');\n}\n\nasync function searchInternetArchiveAudio(query, limit) {\n if (!looksLikeAudioQuery(query)) return [];\n const term = archiveSearchTerm(query);\n if (!term) return [];\n const variants = Array.from(\n new Set([\n term,\n term.replaceAll('金刚经', '金剛經'),\n term.replaceAll('金剛經', '金刚经'),\n ].filter(Boolean)),\n );\n const phraseQuery = variants\n .flatMap((item) => [`title:\"${solrPhrase(item)}\"`, `description:\"${solrPhrase(item)}\"`])\n .join(' OR ');\n const url = buildUrl('https://archive.org', 'advancedsearch.php', {\n q: `(${phraseQuery}) AND mediatype:audio`,\n rows: Math.min(Math.max(limit, 1), 10),\n output: 'json',\n 'fl[]': ['identifier', 'title', 'description'],\n });\n const data = await fetchJson(url);\n const docs = Array.isArray(data?.response?.docs) ? data.response.docs : [];\n return docs\n .map((item) => {\n const identifier = firstText(item, ['identifier']);\n const title = firstText(item, ['title']) || identifier;\n if (!identifier || !title) return null;\n return {\n id: `ia-audio-${identifier}`,\n title,\n sourceName: 'Internet Archive 音频',\n url: `ia:${identifier}`,\n snippet: compactText(firstText(item, ['description'])) || '公开可访问的音频资源,将下载第一个可用 MP3 文件。',\n resourceType: 'audio',\n identifier,\n };\n })\n .filter(Boolean);\n}\n\nconst curatedCbetaWorks = [\n {\n work: 'T0235',\n juan: 1,\n title: '金剛般若波羅蜜經',\n keywords: ['金剛般若波羅蜜經', '金刚般若波罗蜜经', '金剛經', '金刚经'],\n snippet: '姚秦 鳩摩羅什譯。常用于读诵、共修与法布施的般若经典。',\n },\n {\n work: 'T0251',\n juan: 1,\n title: '般若波羅蜜多心經',\n keywords: ['般若波羅蜜多心經', '般若波罗蜜多心经', '心經', '心经'],\n snippet: '唐 玄奘譯。篇幅短,适合加入禅室功课本与日常持诵。',\n },\n {\n work: 'T0262',\n juan: 1,\n title: '妙法蓮華經',\n keywords: ['妙法蓮華經', '妙法莲华经', '法華經', '法华经'],\n snippet: '姚秦 鳩摩羅什譯。可按卷下载正文,用于长期功课与分享。',\n },\n {\n work: 'T0279',\n juan: 1,\n title: '大方廣佛華嚴經',\n keywords: ['大方廣佛華嚴經', '大方广佛华严经', '華嚴經', '华严经'],\n snippet: '唐 實叉難陀譯。可按卷下载正文,用于系统研读与法布施。',\n },\n {\n work: 'T0360',\n juan: 1,\n title: '佛說無量壽經',\n keywords: ['佛說無量壽經', '佛说无量寿经', '無量壽經', '无量寿经'],\n snippet: '净土经典。可下载正文,用于净土功课本与全球法布施。',\n },\n {\n work: 'T0365',\n juan: 1,\n title: '佛說觀無量壽佛經',\n keywords: ['佛說觀無量壽佛經', '佛说观无量寿佛经', '觀無量壽經', '观无量寿经'],\n snippet: '净土三经之一。适合查找下载后加入净土功课。',\n },\n {\n work: 'T0366',\n juan: 1,\n title: '佛說阿彌陀經',\n keywords: ['佛說阿彌陀經', '佛说阿弥陀经', '阿彌陀經', '阿弥陀经'],\n snippet: '姚秦 鳩摩羅什譯。篇幅适中,适合共修、持诵与分享。',\n },\n {\n work: 'T0475',\n juan: 1,\n title: '維摩詰所說經',\n keywords: ['維摩詰所說經', '维摩诘所说经', '維摩詰經', '维摩诘经'],\n snippet: '姚秦 鳩摩羅什譯。大乘经典,可按卷下载用于研读。',\n },\n {\n work: 'T0676',\n juan: 1,\n title: '解深密經',\n keywords: ['解深密經', '解深密经'],\n snippet: '唯识相关大乘经典。适合研读、整理笔记与法布施。',\n },\n {\n work: 'T1666',\n juan: 1,\n title: '大乘起信論',\n keywords: ['大乘起信論', '大乘起信论', '起信論', '起信论'],\n snippet: '大乘论典。适合下载后作为研习功课资料。',\n },\n];\n\nfunction localCbetaResults(query, limit) {\n const normalizedQuery = safeUserText(query).toLowerCase();\n if (!normalizedQuery) return [];\n return curatedCbetaWorks\n .filter((item) =>\n item.keywords.some((keyword) => {\n const normalizedKeyword = keyword.toLowerCase();\n return normalizedKeyword.includes(normalizedQuery) || normalizedQuery.includes(normalizedKeyword);\n }),\n )\n .slice(0, limit)\n .map((item) => ({\n id: `cbeta-curated-${item.work}-${item.juan}`,\n title: item.title,\n sourceName: 'CBETA 佛典',\n url: `cbeta:${item.work}:${item.juan}`,\n snippet: item.snippet,\n resourceType: 'scripture',\n work: item.work,\n juan: item.juan,\n }));\n}\n\nfunction uniqueResources(items, limit) {\n const seen = new Set();\n const results = [];\n for (const item of items) {\n const key = `${item.url}:${item.title}`;\n if (seen.has(key)) continue;\n seen.add(key);\n results.push(item);\n if (results.length >= limit) break;\n }\n return results;\n}\n\nasync function findResourceCandidates(query, limit) {\n const direct = directUrlResource(query);\n const results = [];\n if (direct) results.push(direct);\n try {\n results.push(...(await searchInternetArchiveAudio(query, limit)));\n } catch (error) {\n logger.warn({ error: String(error), query }, 'Internet Archive audio search skipped');\n }\n results.push(...localCbetaResults(query, limit));\n\n const [titleResults, contentResults] = await Promise.allSettled([\n searchCbetaTitle(query, limit),\n searchCbetaContent(query, limit),\n ]);\n if (titleResults.status === 'fulfilled') results.push(...titleResults.value);\n if (contentResults.status === 'fulfilled') results.push(...contentResults.value);\n\n return uniqueResources(results, limit);\n}\n\nfunction looksLikeResourceTask(message) {\n const text = safeUserText(message).toLowerCase();\n if (!text) return false;\n return /下载|查找|寻找|找一?个|资源|經文|经文|佛经|佛經|仪轨|儀軌|音乐|音樂|mp3|audio|念诵|念誦|诵经|誦經/.test(text);\n}\n\nfunction extractResourceQuery(message) {\n const text = safeUserText(message)\n .replace(/请|請|帮我|幫我|麻烦|麻煩|可以|能不能|需要|我要|想要/g, ' ')\n .replace(/下载|下載|查找|寻找|尋找|找一个|找一個|找|资源|資源|并|並|然后|然後|用于|用於|全球法布施|加入功课本|加入功課本/g, ' ')\n .replace(/\\s+/g, ' ')\n .trim();\n return text || safeUserText(message);\n}\n\nfunction resourceContextMessage(skillResult) {\n if (!skillResult) return '';\n const lines = [\n '本轮资源检索/下载已经由大乘 App 后端完成。',\n '不要再次调用 search_dharma_resources、download_dharma_resource 或其他资源 MCP 工具;请直接基于以下结果回答用户。',\n `后端已调用 skill: ${skillResult.skillName}`,\n `执行目标: ${skillResult.query}`,\n ];\n if (skillResult.selected) {\n lines.push(`候选资源: ${skillResult.selected.title}`);\n lines.push(`来源: ${skillResult.selected.sourceName}`);\n lines.push(`资源地址: ${skillResult.selected.url}`);\n }\n if (skillResult.downloaded) {\n lines.push(`下载标题: ${skillResult.downloaded.title}`);\n lines.push(`下载文件: ${skillResult.downloaded.fileName}`);\n const text = safeUserText(skillResult.downloaded.contentText);\n if (text) {\n lines.push('下载正文摘录:');\n lines.push(text.slice(0, 8000));\n }\n }\n return lines.join('\\n');\n}\n\nfunction determineClientAction(skillResult, message) {\n if (skillResult && skillResult.downloaded) {\n const text = safeUserText(message);\n if (/法布施|全球法布施|分享/.test(text)) {\n return {\n type: 'prepare_dharma_share_text',\n title: skillResult.downloaded.title,\n text: skillResult.downloaded.contentText || '',\n };\n }\n }\n return null;\n}\n\nasync function runResourceFinderSkill({ message, user, onStep }) {\n if (!looksLikeResourceTask(message)) return null;\n\n const skillName = 'resource-finder-downloader';\n const query = extractResourceQuery(message);\n const audioTask = looksLikeAudioQuery(message) || looksLikeAudioQuery(query);\n onStep?.({\n type: 'skill.step',\n skillName,\n stage: 'start',\n title: '调用资源查找下载 skill',\n message: `识别到资源任务: ${query}`,\n });\n\n onStep?.({\n type: 'skill.step',\n skillName,\n stage: 'search',\n title: '搜索可分享资源',\n message: audioTask\n ? '优先检索公开可访问音频资源,失败时再回落到佛典正文资源。'\n : '优先检索 CBETA 佛典正文接口,搜索索引不可用时使用内置经典映射。',\n });\n const candidates = await findResourceCandidates(query, 8);\n if (candidates.length === 0) {\n onStep?.({\n type: 'skill.step',\n skillName,\n stage: 'empty',\n title: '没有找到可下载资源',\n message: '后端没有找到明确可下载且可分享的资源。',\n });\n return {\n skillName,\n query,\n candidates: [],\n selected: null,\n downloaded: null,\n };\n }\n\n const selected = candidates[0];\n onStep?.({\n type: 'skill.step',\n skillName,\n stage: 'select',\n title: '选择可信候选资源',\n message: `${selected.title} / ${selected.sourceName}`,\n });\n\n onStep?.({\n type: 'skill.step',\n skillName,\n stage: 'download',\n title: '下载并提取资源',\n message: selected.url,\n });\n const cbeta = parseCbetaResource(selected);\n const internetArchive = parseInternetArchiveResource(selected);\n const downloaded = cbeta\n ? await downloadCbetaResource({ ...selected, ...cbeta })\n : internetArchive\n ? await downloadInternetArchiveAudio({ ...selected, ...internetArchive })\n : await downloadWebResource(selected);\n const persisted = persistDownloadedResource(user, downloaded);\n\n onStep?.({\n type: 'skill.step',\n skillName,\n stage: 'done',\n title: '资源已准备完成',\n message: downloaded.fileName,\n });\n\n return {\n skillName,\n query,\n candidates,\n selected,\n downloaded: {\n ...downloaded,\n downloadId: persisted.id,\n },\n };\n}\n\napp.post(\n '/api/resources/search',\n asyncHandler(async (req, res) => {\n const query = safeUserText(req.body?.query);\n const limit = Math.min(Math.max(Number(req.body?.limit || 12), 1), 20);\n if (query.length < 2) {\n return jsonResponse(res, 400, { success: false, message: 'query is required' });\n }\n\n const items = await findResourceCandidates(query, limit);\n\n jsonResponse(res, 200, {\n success: true,\n source: 'resource-index',\n items,\n });\n }),\n);\n\nfunction parseCbetaResource(input) {\n const work = safeUserText(input.work);\n const juan = Number(input.juan || 1);\n if (work) return { work, juan: Number.isFinite(juan) ? juan : 1 };\n\n const url = safeUserText(input.url);\n const match = /^cbeta:([^:]+):(\\d+)$/i.exec(url);\n if (!match) return null;\n return {\n work: match[1].toUpperCase(),\n juan: Number(match[2] || 1),\n };\n}\n\nfunction parseInternetArchiveResource(input) {\n const url = safeUserText(input.url);\n const match = /^ia:([^:]+)$/i.exec(url);\n const identifier = match?.[1] || safeUserText(input.identifier);\n if (!identifier) return null;\n return { identifier };\n}\n\nfunction safeFileName(value) {\n const normalized = String(value || 'dharma-resource')\n .replace(/[\\\\/:*?\"<>|]+/g, '-')\n .replace(/\\s+/g, '-')\n .replace(/-+/g, '-')\n .replace(/^-|-$/g, '')\n .slice(0, 80);\n return normalized || 'dharma-resource';\n}\n\nfunction extractCbetaHtml(data) {\n const first = Array.isArray(data?.results) ? data.results[0] : null;\n if (typeof first === 'string') return first;\n if (first && typeof first === 'object' && typeof first.html === 'string') return first.html;\n if (typeof data?.html === 'string') return data.html;\n return '';\n}\n\nasync function downloadCbetaResource(resource) {\n const url = buildUrl(cbetaApiRoot, 'juans', {\n work: resource.work,\n juan: resource.juan,\n work_info: 1,\n toc: 1,\n });\n const data = await fetchJson(url);\n const workInfo = data.work_info || {};\n const title = safeUserText(workInfo.title) || safeUserText(resource.title) || resource.work;\n const html = extractCbetaHtml(data);\n const text = cleanExtractedText(stripHtml(html));\n if (!text) throw new Error('CBETA resource returned empty content');\n return {\n title,\n sourceName: 'CBETA 佛典',\n url: `cbeta:${resource.work}:${resource.juan}`,\n contentText: text.slice(0, maxResourceTextChars),\n fileName: `${resource.work}_${resource.juan}_${safeFileName(title)}.txt`,\n };\n}\n\nasync function downloadInternetArchiveAudio(resource) {\n const identifier = safeUserText(resource.identifier);\n if (!identifier) throw new Error('Internet Archive identifier is required');\n const metadata = await fetchJson(`https://archive.org/metadata/${encodeURIComponent(identifier)}`);\n const title = safeUserText(metadata?.metadata?.title) || safeUserText(resource.title) || identifier;\n const files = Array.isArray(metadata?.files) ? metadata.files : [];\n const audioFile = files.find((file) => {\n const name = safeUserText(file?.name);\n const format = safeUserText(file?.format).toLowerCase();\n return /\\.(mp3|m4a|ogg)$/i.test(name) || format.includes('mp3');\n });\n if (!audioFile?.name) {\n throw new Error('Internet Archive audio item has no downloadable audio file');\n }\n const sourceFileName = audioFile.name;\n const fileUrl = `https://archive.org/download/${encodeURIComponent(identifier)}/${sourceFileName\n .split('/')\n .map((part) => encodeURIComponent(part))\n .join('/')}`;\n const response = await fetch(fileUrl, {\n headers: {\n Accept: 'audio/mpeg, audio/*;q=0.9, */*;q=0.5',\n 'User-Agent': 'DachengResourceDownloader/1.0 (+https://ombhrum.com)',\n },\n signal: AbortSignal.timeout(60_000),\n });\n if (!response.ok) {\n throw new Error(`Internet Archive audio download failed: ${response.status}`);\n }\n const binaryContent = Buffer.from(await response.arrayBuffer());\n if (!binaryContent.length) throw new Error('Internet Archive audio returned empty content');\n const sizeMb = (binaryContent.length / 1024 / 1024).toFixed(1);\n return {\n title,\n sourceName: 'Internet Archive 音频',\n url: fileUrl,\n contentText: `已下载音频资源: ${title}\\n文件名: ${sourceFileName}\\n大小: ${sizeMb} MB\\n来源: ${fileUrl}`,\n binaryContent,\n fileName: safeFileName(sourceFileName),\n };\n}\n\nasync function downloadWebResource(input) {\n const url = safeUserText(input.url);\n const parsed = new URL(url);\n if (!['http:', 'https:'].includes(parsed.protocol)) {\n throw new Error('Only http/https resources can be downloaded');\n }\n\n const response = await fetch(parsed, {\n headers: {\n Accept: 'text/html, text/plain, application/xhtml+xml, application/xml;q=0.8, */*;q=0.5',\n 'User-Agent': 'DachengResourceDownloader/1.0 (+https://ombhrum.com)',\n },\n signal: AbortSignal.timeout(25_000),\n });\n if (!response.ok) throw new Error(`Resource download failed: ${response.status}`);\n const contentType = response.headers.get('content-type') || '';\n const body = await response.text();\n const title = safeUserText(input.title) || parsed.hostname;\n const contentText = contentType.includes('html') ? cleanExtractedText(stripHtml(body)) : body.trim();\n if (!contentText) throw new Error('Resource returned empty content');\n return {\n title,\n sourceName: safeUserText(input.sourceName) || parsed.hostname,\n url,\n contentText: contentText.slice(0, maxResourceTextChars),\n fileName: `${safeFileName(title)}.txt`,\n };\n}\n\nfunction persistDownloadedResource(user, content) {\n const id = crypto.randomUUID();\n const fileName = `${id}-${safeFileName(content.fileName)}`;\n const filePath = path.join(resourcesDir, fileName);\n if (content.binaryContent) {\n fs.writeFileSync(filePath, content.binaryContent);\n } else {\n fs.writeFileSync(filePath, content.contentText, 'utf8');\n }\n statements.insertResourceDownload.run({\n id,\n userId: user.userId,\n title: content.title,\n sourceName: content.sourceName,\n sourceUrl: content.url,\n fileName: content.fileName,\n filePath,\n createdAt: nowIso(),\n });\n return { id, fileName, filePath };\n}\n\nfunction createLibreChatMcpServer() {\n const server = new McpServer({\n name: 'dacheng-ai-tools',\n version: '0.1.0',\n });\n\n server.registerTool(\n 'search_dharma_resources',\n {\n title: 'Search dharma resources',\n description:\n 'Search for legally shareable Buddhist scripture, text, web, or audio resources that can be prepared for Fabushi sharing.',\n inputSchema: {\n query: z.string().min(2).max(200),\n limit: z.number().int().min(1).max(12).optional(),\n },\n outputSchema: {\n query: z.string(),\n items: z.array(\n z.object({\n id: z.string(),\n title: z.string(),\n sourceName: z.string(),\n url: z.string(),\n snippet: z.string(),\n resourceType: z.string(),\n work: z.string().optional(),\n juan: z.number().optional(),\n }),\n ),\n },\n annotations: {\n readOnlyHint: true,\n destructiveHint: false,\n openWorldHint: true,\n },\n },\n async ({ query, limit }) => {\n const items = await findResourceCandidates(query, limit ?? 8);\n return textToolResult(`Found ${items.length} candidate resources for \"${query}\".`, {\n query,\n items,\n });\n },\n );\n\n server.registerTool(\n 'download_dharma_resource',\n {\n title: 'Download dharma resource',\n description:\n 'Download and extract a selected resource. Use the url/work/juan returned by search_dharma_resources whenever possible.',\n inputSchema: {\n url: z.string().min(1).max(1000),\n title: z.string().max(300).optional(),\n sourceName: z.string().max(120).optional(),\n work: z.string().max(40).optional(),\n juan: z.number().int().min(1).optional(),\n identifier: z.string().max(300).optional(),\n },\n outputSchema: {\n title: z.string(),\n sourceName: z.string(),\n url: z.string(),\n fileName: z.string(),\n downloadId: z.string(),\n contentText: z.string(),\n },\n annotations: {\n readOnlyHint: false,\n destructiveHint: false,\n openWorldHint: true,\n },\n },\n async (input) => {\n const user = { userId: 'librechat:mcp' };\n const cbeta = parseCbetaResource(input);\n const internetArchive = parseInternetArchiveResource(input);\n const content = cbeta\n ? await downloadCbetaResource({ ...input, ...cbeta })\n : internetArchive\n ? await downloadInternetArchiveAudio({ ...input, ...internetArchive })\n : await downloadWebResource(input);\n const persisted = persistDownloadedResource(user, content);\n const { binaryContent: _binaryContent, ...publicContent } = content;\n return textToolResult(`Downloaded ${publicContent.title} as ${publicContent.fileName}.`, {\n ...publicContent,\n downloadId: persisted.id,\n });\n },\n );\n\n server.registerTool(\n 'prepare_dharma_share_text',\n {\n title: 'Prepare dharma sharing text',\n description:\n 'Prepare a text payload for the Fabushi app to confirm and add to global dharma sharing. This returns a clientAction; it does not start sending.',\n inputSchema: {\n title: z.string().min(1).max(120),\n text: z.string().min(1).max(20000),\n },\n outputSchema: {\n clientAction: z.object({\n type: z.literal('prepare_dharma_share_text'),\n title: z.string(),\n text: z.string(),\n }),\n },\n annotations: {\n readOnlyHint: false,\n destructiveHint: false,\n openWorldHint: false,\n },\n },\n async ({ title, text }) =>\n textToolResult(`Prepared \"${title}\" for user confirmation in the Fabushi app.`, {\n clientAction: {\n type: 'prepare_dharma_share_text',\n title: safeUserText(title).slice(0, 120),\n text: String(text || '').trim().slice(0, 20000),\n },\n }),\n );\n\n server.registerTool(\n 'prepare_practice_book_item',\n {\n title: 'Prepare practice book item',\n description:\n 'Prepare a scripture, chant, or practice item for the Fabushi app practice book. This returns a clientAction; it does not mutate the app by itself.',\n inputSchema: {\n title: z.string().min(1).max(120),\n sourceName: z.string().max(120).optional(),\n text: z.string().min(1).max(20000),\n },\n outputSchema: {\n clientAction: z.object({\n type: z.literal('prepare_practice_book_item'),\n title: z.string(),\n sourceName: z.string(),\n text: z.string(),\n }),\n },\n annotations: {\n readOnlyHint: false,\n destructiveHint: false,\n openWorldHint: false,\n },\n },\n async ({ title, sourceName, text }) =>\n textToolResult(`Prepared \"${title}\" for the Fabushi practice book confirmation flow.`, {\n clientAction: {\n type: 'prepare_practice_book_item',\n title: safeUserText(title).slice(0, 120),\n sourceName: safeUserText(sourceName || '大乘 AI').slice(0, 120),\n text: String(text || '').trim().slice(0, 20000),\n },\n }),\n );\n\n return server;\n}\n\nasync function handleMcpRequest(req, res) {\n const server = createLibreChatMcpServer();\n const transport = new StreamableHTTPServerTransport({\n sessionIdGenerator: undefined,\n enableJsonResponse: true,\n });\n\n res.on('close', () => {\n transport.close();\n server.close();\n });\n\n await server.connect(transport);\n await transport.handleRequest(req, res, req.body);\n}\n\napp.all('/mcp', async (req, res) => {\n try {\n await handleMcpRequest(req, res);\n } catch (error) {\n logger.error({ error: error.stack || String(error) }, 'MCP request failed');\n if (!res.headersSent) {\n res.status(500).json({\n error: 'MCP request failed',\n message: publicAiErrorMessage(error),\n });\n }\n }\n});\n\napp.post(\n '/api/resources/download',\n asyncHandler(async (req, res) => {\n const user = await resolveUser(req, req.body || {});\n const cbeta = parseCbetaResource(req.body || {});\n const internetArchive = parseInternetArchiveResource(req.body || {});\n const content = cbeta\n ? await downloadCbetaResource({ ...req.body, ...cbeta })\n : internetArchive\n ? await downloadInternetArchiveAudio({ ...req.body, ...internetArchive })\n : await downloadWebResource(req.body || {});\n const persisted = persistDownloadedResource(user, content);\n const { binaryContent: _binaryContent, ...publicContent } = content;\n jsonResponse(res, 200, {\n success: true,\n downloadId: persisted.id,\n ...publicContent,\n });\n }),\n);\n\napp.post(\n '/api/codex/resource-task',\n asyncHandler(async (req, res) => {\n if (process.env.ENABLE_CODEX_SDK !== 'true') {\n return jsonResponse(res, 403, {\n success: false,\n message: 'Codex SDK task runner is disabled. Set ENABLE_CODEX_SDK=true on the server to enable it.',\n });\n }\n const prompt = safeUserText(req.body?.prompt);\n if (!prompt) {\n return jsonResponse(res, 400, { success: false, message: 'prompt is required' });\n }\n const user = await resolveUser(req, req.body || {});\n const { Codex } = await import('@openai/codex-sdk');\n const codexRuntime = createCodexDeepSeekRuntime(user);\n const codex = new Codex(codexRuntime.enabled ? codexRuntime.options : {});\n const thread = codex.startThread(\n codexRuntime.enabled\n ? codexRuntime.threadOptions\n : { skipGitRepoCheck: true, networkAccessEnabled: true },\n );\n const result = await thread.run(prompt);\n const usage = {\n promptTokens: Number(result.usage?.input_tokens || 0),\n completionTokens: Number(result.usage?.output_tokens || 0),\n totalTokens: Number((result.usage?.input_tokens || 0) + (result.usage?.output_tokens || 0)),\n };\n if (!codexRuntime.enabled) {\n recordUsage(user.userId, usage.totalTokens || estimateTokens(JSON.stringify(result)));\n }\n jsonResponse(res, 200, {\n success: true,\n threadId: thread.id,\n provider: codexRuntime.provider,\n model: deepseekModel,\n usage,\n result,\n });\n }),\n);\n\nregisterPlatformApi({ app, db, resolveUser, asyncHandler });\n\napp.use((error, _req, res, _next) => {\n const status = Number(error.statusCode || error.status || 500);\n const safeStatus = status >= 400 && status < 600 ? status : 500;\n logger.error({ error: error.stack || String(error) }, 'request failed');\n jsonResponse(res, safeStatus, {\n success: false,\n message: error.message || 'Internal Server Error',\n details: error.details,\n });\n});\n\nconst port = Number(process.env.PORT || 8788);\napp.listen(port, '0.0.0.0', () => {\n logger.info(\n {\n port,\n dbPath,\n dataDir,\n deepseekModel,\n cbetaApiRoot,\n },\n 'Dacheng AI backend started',\n );\n});\n"}, "38d524992bab8670d6b186aa3caa86bef65e06c1": {"t": 1785757950.2163007, "n": 159349, "h": "d9a738aad753a53f", "c": "import crypto from 'node:crypto';\nimport fs from 'node:fs';\nimport path from 'node:path';\nimport process from 'node:process';\nimport { fileURLToPath } from 'node:url';\n\nimport { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';\nimport { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';\nimport Database from 'better-sqlite3';\nimport cors from 'cors';\nimport dotenv from 'dotenv';\nimport express from 'express';\nimport rateLimit from 'express-rate-limit';\nimport helmet from 'helmet';\nimport pino from 'pino';\nimport { z } from 'zod';\n\nimport { registerPlatformApi } from './platform_api.js';\nimport {\n extractMiniAppHtml,\n isBotFatherGenerationMessages,\n} from './miniapp_generation.js';\n\ndotenv.config();\n\nconst __dirname = path.dirname(fileURLToPath(import.meta.url));\nconst rootDir = path.resolve(__dirname, '..');\nconst dataDir = process.env.DATA_DIR || path.join(rootDir, 'data');\nconst resourcesDir = path.join(dataDir, 'resources');\nconst miniAppsDir = path.join(dataDir, 'miniapps');\nconst miniAppsStorePath = path.join(miniAppsDir, 'sandbox-miniapps.json');\n\nfunction miniAppWorkspaceDir(user, prompt) {\n const userId = String(user?.userId || 'anonymous')\n .replace(/[^a-zA-Z0-9_-]/g, '_')\n .slice(0, 64);\n const seed = crypto\n .createHash('sha256')\n .update(`${userId}:${prompt}`)\n .digest('hex')\n .slice(0, 16);\n const dir = path.join(miniAppsDir, userId, seed);\n fs.mkdirSync(dir, { recursive: true });\n return dir;\n}\nconst openClawRuntimeUpdatesDir =\n process.env.OPENCLAW_RUNTIME_UPDATES_DIR || path.join(dataDir, 'openclaw-runtime');\nconst openClawRuntimeEnableMacosUpdates =\n process.env.OPENCLAW_RUNTIME_ENABLE_MACOS_UPDATES === 'true';\nconst codexHomeDir = process.env.CODEX_HOME || path.join(dataDir, 'codex-home');\nconst codexTempDir = process.env.CODEX_TMPDIR || path.join(dataDir, 'codex-tmp');\nconst codexRuntimeDir = process.env.XDG_RUNTIME_DIR || path.join(dataDir, 'codex-runtime');\nconst dbPath = process.env.SQLITE_PATH || path.join(dataDir, 'dacheng-ai.sqlite');\n\nfs.mkdirSync(resourcesDir, { recursive: true });\nfs.mkdirSync(miniAppsDir, { recursive: true });\nfs.mkdirSync(openClawRuntimeUpdatesDir, { recursive: true });\nfs.mkdirSync(codexHomeDir, { recursive: true });\nfs.mkdirSync(codexTempDir, { recursive: true });\nfs.mkdirSync(codexRuntimeDir, { recursive: true });\n\nconst logger = pino({\n level: process.env.LOG_LEVEL || 'info',\n});\n\nconst app = express();\napp.disable('x-powered-by');\napp.set('trust proxy', Number(process.env.TRUST_PROXY_HOPS || 1));\napp.use(helmet());\nconst corsOrigin =\n !process.env.CORS_ORIGIN || process.env.CORS_ORIGIN.trim() === '*'\n ? true\n : process.env.CORS_ORIGIN.split(',').map((item) => item.trim()).filter(Boolean);\napp.use(cors({ origin: corsOrigin }));\napp.use(express.json({ limit: '1mb' }));\napp.use(\n rateLimit({\n windowMs: 60_000,\n limit: Number(process.env.RATE_LIMIT_PER_MINUTE || 90),\n standardHeaders: true,\n legacyHeaders: false,\n }),\n);\n\nconst db = new Database(dbPath);\ndb.pragma('journal_mode = WAL');\ndb.pragma('foreign_keys = ON');\n\ndb.exec(`\n CREATE TABLE IF NOT EXISTS conversations (\n id TEXT PRIMARY KEY,\n user_id TEXT NOT NULL,\n title TEXT NOT NULL,\n provider TEXT NOT NULL,\n model TEXT NOT NULL,\n created_at TEXT NOT NULL,\n updated_at TEXT NOT NULL\n );\n\n CREATE INDEX IF NOT EXISTS idx_conversations_user_updated\n ON conversations (user_id, updated_at DESC);\n\n CREATE TABLE IF NOT EXISTS messages (\n id TEXT PRIMARY KEY,\n conversation_id TEXT NOT NULL,\n role TEXT NOT NULL,\n content TEXT NOT NULL,\n prompt_tokens INTEGER NOT NULL DEFAULT 0,\n completion_tokens INTEGER NOT NULL DEFAULT 0,\n total_tokens INTEGER NOT NULL DEFAULT 0,\n created_at TEXT NOT NULL,\n FOREIGN KEY (conversation_id) REFERENCES conversations(id) ON DELETE CASCADE\n );\n\n CREATE INDEX IF NOT EXISTS idx_messages_conversation_created\n ON messages (conversation_id, created_at ASC);\n\n CREATE TABLE IF NOT EXISTS usage_months (\n user_id TEXT NOT NULL,\n month TEXT NOT NULL,\n total_tokens INTEGER NOT NULL DEFAULT 0,\n updated_at TEXT NOT NULL,\n PRIMARY KEY (user_id, month)\n );\n\n CREATE TABLE IF NOT EXISTS resource_downloads (\n id TEXT PRIMARY KEY,\n user_id TEXT NOT NULL,\n title TEXT NOT NULL,\n source_name TEXT NOT NULL,\n source_url TEXT NOT NULL,\n file_name TEXT NOT NULL,\n file_path TEXT NOT NULL,\n created_at TEXT NOT NULL\n );\n\n CREATE TABLE IF NOT EXISTS agent_runs (\n id TEXT PRIMARY KEY,\n user_id TEXT NOT NULL,\n conversation_id TEXT NOT NULL,\n message_id TEXT NOT NULL,\n openclaw_run_id TEXT,\n openclaw_session_key TEXT NOT NULL,\n status TEXT NOT NULL,\n mode TEXT NOT NULL,\n provider TEXT NOT NULL,\n model TEXT,\n input_tokens INTEGER NOT NULL DEFAULT 0,\n output_tokens INTEGER NOT NULL DEFAULT 0,\n tool_call_count INTEGER NOT NULL DEFAULT 0,\n cost_microusd INTEGER NOT NULL DEFAULT 0,\n started_at TEXT NOT NULL,\n completed_at TEXT,\n failed_at TEXT,\n error_code TEXT,\n error_message TEXT\n );\n\n CREATE INDEX IF NOT EXISTS idx_agent_runs_user_started\n ON agent_runs (user_id, started_at DESC);\n\n CREATE INDEX IF NOT EXISTS idx_agent_runs_status_started\n ON agent_runs (status, started_at DESC);\n\n CREATE TABLE IF NOT EXISTS agent_message_feedback (\n id TEXT PRIMARY KEY,\n message_id TEXT NOT NULL,\n user_id TEXT NOT NULL,\n rating TEXT NOT NULL,\n reason TEXT,\n comment TEXT,\n created_at TEXT NOT NULL\n );\n`);\n\nconst statements = {\n insertConversation: db.prepare(`\n INSERT INTO conversations (id, user_id, title, provider, model, created_at, updated_at)\n VALUES (@id, @userId, @title, @provider, @model, @createdAt, @updatedAt)\n `),\n updateConversation: db.prepare(`\n UPDATE conversations\n SET title = @title, updated_at = @updatedAt\n WHERE id = @id AND user_id = @userId\n `),\n getConversation: db.prepare(`\n SELECT * FROM conversations WHERE id = ? AND user_id = ? LIMIT 1\n `),\n listConversations: db.prepare(`\n SELECT id, title, updated_at AS updatedAt\n FROM conversations\n WHERE user_id = ?\n ORDER BY updated_at DESC\n LIMIT 30\n `),\n insertMessage: db.prepare(`\n INSERT INTO messages (\n id, conversation_id, role, content,\n prompt_tokens, completion_tokens, total_tokens, created_at\n )\n VALUES (\n @id, @conversationId, @role, @content,\n @promptTokens, @completionTokens, @totalTokens, @createdAt\n )\n `),\n listMessages: db.prepare(`\n SELECT role, content, prompt_tokens AS promptTokens,\n completion_tokens AS completionTokens, total_tokens AS totalTokens,\n created_at AS createdAt\n FROM messages\n WHERE conversation_id = ?\n ORDER BY created_at ASC\n `),\n getUsage: db.prepare(`\n SELECT total_tokens AS totalTokens FROM usage_months WHERE user_id = ? AND month = ?\n `),\n upsertUsage: db.prepare(`\n INSERT INTO usage_months (user_id, month, total_tokens, updated_at)\n VALUES (@userId, @month, @totalTokens, @updatedAt)\n ON CONFLICT(user_id, month)\n DO UPDATE SET total_tokens = usage_months.total_tokens + excluded.total_tokens,\n updated_at = excluded.updated_at\n `),\n insertResourceDownload: db.prepare(`\n INSERT INTO resource_downloads (\n id, user_id, title, source_name, source_url, file_name, file_path, created_at\n )\n VALUES (\n @id, @userId, @title, @sourceName, @sourceUrl, @fileName, @filePath, @createdAt\n )\n `),\n insertAgentRun: db.prepare(`\n INSERT INTO agent_runs (\n id, user_id, conversation_id, message_id, openclaw_run_id, openclaw_session_key,\n status, mode, provider, model, started_at\n )\n VALUES (\n @id, @userId, @conversationId, @messageId, @openClawRunId, @openClawSessionKey,\n @status, @mode, @provider, @model, @startedAt\n )\n `),\n getAgentRun: db.prepare(`\n SELECT * FROM agent_runs WHERE id = ? AND user_id = ? LIMIT 1\n `),\n updateAgentRunStatus: db.prepare(`\n UPDATE agent_runs\n SET status = @status,\n openclaw_run_id = COALESCE(@openClawRunId, openclaw_run_id),\n input_tokens = COALESCE(@inputTokens, input_tokens),\n output_tokens = COALESCE(@outputTokens, output_tokens),\n tool_call_count = COALESCE(@toolCallCount, tool_call_count),\n completed_at = @completedAt,\n failed_at = @failedAt,\n error_code = @errorCode,\n error_message = @errorMessage\n WHERE id = @id AND user_id = @userId\n `),\n insertAgentFeedback: db.prepare(`\n INSERT INTO agent_message_feedback (id, message_id, user_id, rating, reason, comment, created_at)\n VALUES (@id, @messageId, @userId, @rating, @reason, @comment, @createdAt)\n `),\n};\n\nconst deepseekApiKey = process.env.DEEPSEEK_API_KEY || '';\nconst deepseekBaseUrl = (process.env.DEEPSEEK_BASE_URL || 'https://api.deepseek.com').replace(/\\/+$/, '');\nconst deepseekModel = process.env.DEEPSEEK_MODEL || 'deepseek-chat';\nconst deepseekAvailableModels = Array.from(\n new Set(\n (process.env.DEEPSEEK_MODELS || `${deepseekModel},deepseek-chat,deepseek-reasoner`)\n .split(',')\n .map((item) => normalizeDeepSeekModelName(item))\n .filter(Boolean),\n ),\n);\nconst cbetaApiRoot = (process.env.CBETA_API_ROOT || 'https://api.ombhrum.com/api/cbeta').replace(/\\/+$/, '');\nconst fabushiApiBaseUrl = (process.env.FABUSHI_API_BASE_URL || 'https://api.ombhrum.com').replace(/\\/+$/, '');\nconst memberMonthlyLimit = Number(process.env.MEMBER_MONTHLY_TOKEN_LIMIT || 1_000_000);\nconst freeMonthlyLimit = Number(process.env.FREE_MONTHLY_TOKEN_LIMIT || 1_000);\nconst requireMemberForAi = process.env.REQUIRE_MEMBER_FOR_AI === 'true';\nconst deepseekMaxCompletionTokens = Math.max(1, Number(process.env.DEEPSEEK_MAX_COMPLETION_TOKENS || 1400));\nconst botFatherMaxCompletionTokens = Math.max(\n deepseekMaxCompletionTokens,\n Math.min(12_000, Number(process.env.BOT_FATHER_MAX_COMPLETION_TOKENS || 6000)),\n);\nconst maxResourceTextChars = Number(process.env.MAX_RESOURCE_TEXT_CHARS || 80_000);\nconst openClawRuntimeManifestPath =\n process.env.OPENCLAW_RUNTIME_MANIFEST_PATH || path.join(openClawRuntimeUpdatesDir, 'manifest.json');\nconst openClawRuntimePublicBaseUrl = (process.env.OPENCLAW_RUNTIME_PUBLIC_BASE_URL || '').replace(/\\/+$/, '');\nconst enableLibreChatAgentChat = process.env.ENABLE_LIBRECHAT_AGENT_CHAT === 'true';\nconst libreChatAgentsBaseUrl = (\n process.env.LIBRECHAT_AGENTS_BASE_URL || 'http://127.0.0.1:3080/api/agents/v1'\n).replace(/\\/+$/, '');\nconst libreChatAgentApiKey = process.env.LIBRECHAT_AGENT_API_KEY || '';\nconst libreChatAgentId = process.env.LIBRECHAT_AGENT_ID || '';\nconst enableCodexSdkChat = process.env.ENABLE_CODEX_SDK_CHAT === 'true';\nconst enableOpenClawAgentChat = process.env.ENABLE_OPENCLAW_AGENT_CHAT === 'true';\nconst openClawGatewayUrl = (process.env.OPENCLAW_GATEWAY_URL || 'http://127.0.0.1:18789').replace(/\\/+$/, '');\nconst openClawGatewayToken = process.env.OPENCLAW_GATEWAY_TOKEN || '';\nconst openClawAgentId = process.env.OPENCLAW_AGENT_ID || 'fabushi-public-agent';\nconst openClawRunsEndpoint = process.env.OPENCLAW_RUNS_ENDPOINT || '';\nconst codexDeepSeekProviderId = 'dacheng-deepseek-proxy';\nconst codexResponsesBaseUrl = (\n process.env.CODEX_DEEPSEEK_RESPONSES_BASE_URL ||\n `http://127.0.0.1:${process.env.PORT || 8788}/codex-deepseek/v1`\n).replace(/\\/+$/, '');\nconst codexAdapterTokenPrefix = 'dacheng-codex-proxy.';\nconst codexAdapterTokenTtlMs = Math.max(\n 60_000,\n Number(process.env.CODEX_DEEPSEEK_ADAPTER_TOKEN_TTL_MS || 15 * 60_000),\n);\nconst codexAdapterSecret =\n process.env.CODEX_DEEPSEEK_ADAPTER_SECRET ||\n deepseekApiKey ||\n openClawGatewayToken ||\n 'dacheng-codex-local-dev-secret';\n\nconst systemPrompt = [\n '你是“大乘”App 的 AI 助手。',\n '你的核心任务是帮助用户查找、整理、理解并全球法布施合法可分享的佛法资源。',\n '回答要庄重、简洁、可执行;涉及经典、仪轨或资源时,提醒用户尊重版权、来源和当地法规。',\n '如果用户想找资源、下载经文、寻找音频或准备可分享资料,你必须先在后端自动执行搜索、验证和下载步骤;不要要求用户去前端 + 菜单手动查找。',\n '你可以把执行进度总结给用户,但不要输出隐藏推理原文。',\n].join('\\n');\n\nfunction nowIso() {\n return new Date().toISOString();\n}\n\nfunction monthKey(date = new Date()) {\n return `${date.getUTCFullYear()}-${String(date.getUTCMonth() + 1).padStart(2, '0')}`;\n}\n\nfunction jsonResponse(res, status, payload) {\n res.status(status).json(payload);\n}\n\nasync function readJsonFile(filePath) {\n const raw = await fs.promises.readFile(filePath, 'utf8');\n return JSON.parse(raw);\n}\n\nfunction requestOrigin(req) {\n if (openClawRuntimePublicBaseUrl) return openClawRuntimePublicBaseUrl;\n const proto = req.get('X-Forwarded-Proto') || req.protocol || 'https';\n const host = req.get('X-Forwarded-Host') || req.get('Host');\n return host ? `${proto}://${host}` : '';\n}\n\nfunction absolutizeOpenClawRuntimeUrl(req, value) {\n const text = readText(value);\n if (!text) return '';\n if (/^https?:\\/\\//i.test(text)) return text;\n const normalized = text.startsWith('/')\n ? text\n : `/api/openclaw/runtime/files/${text.replace(/^\\/+/, '')}`;\n const origin = requestOrigin(req);\n return origin ? new URL(normalized, `${origin}/`).toString() : normalized;\n}\n\nfunction textToolResult(text, structuredContent = {}) {\n return {\n structuredContent,\n content: [{ type: 'text', text }],\n };\n}\n\nfunction sseHeaders(res) {\n res.status(200);\n res.setHeader('Content-Type', 'text/event-stream; charset=utf-8');\n res.setHeader('Cache-Control', 'no-cache, no-transform');\n res.setHeader('Connection', 'keep-alive');\n res.setHeader('X-Accel-Buffering', 'no');\n if (typeof res.flushHeaders === 'function') res.flushHeaders();\n}\n\nfunction writeSse(res, event, payload) {\n res.write(`event: ${event}\\n`);\n res.write(`data: ${JSON.stringify(payload)}\\n\\n`);\n}\n\nfunction asyncHandler(fn) {\n return (req, res, next) => {\n Promise.resolve(fn(req, res, next)).catch(next);\n };\n}\n\nfunction bearerToken(req) {\n const delegated = readText(req.get('x-dacheng-auth-token'));\n if (\n delegated &&\n delegated !== 'DACHENG_AUTH_TOKEN' &&\n !delegated.startsWith('secretref-env:') &&\n delegated !== 'dacheng-openclaw-proxy'\n ) {\n return delegated;\n }\n const header = req.get('Authorization') || '';\n if (!header.startsWith('Bearer ')) return '';\n const token = header.slice('Bearer '.length).trim();\n return token === 'dacheng-openclaw-proxy' ? '' : token;\n}\n\nfunction sha256(value) {\n return crypto.createHash('sha256').update(value).digest('hex');\n}\n\nfunction safeUserText(value, fallback = '') {\n if (typeof value !== 'string') return fallback;\n return value.replace(/\\u0000/g, '').trim();\n}\n\nfunction estimateTokens(text) {\n const normalized = String(text || '').trim();\n if (!normalized) return 0;\n return Math.max(1, Math.ceil(normalized.length / 3));\n}\n\nfunction readText(value) {\n if (value === undefined || value === null) return '';\n return String(value).replace(/\\u0000/g, '').trim();\n}\n\nconst miniAppHostApiVersion = '2.0';\nconst miniAppHighRiskPermissions = new Set([\n 'auth.token',\n 'payments.alipay',\n 'payments.fudeGold',\n 'desktop.control',\n 'network.http',\n 'network.udp',\n 'network.interfaces',\n 'system.keepAwake',\n 'hotspot.settings',\n 'local.loopback',\n 'fs.readWrite',\n 'runtime.process',\n 'shell.execute',\n 'browser.external',\n 'files.write',\n 'projects.write',\n 'openclaw.restart',\n]);\n\nfunction officialMiniAppRegistry(req) {\n const webBase = process.env.MINIAPP_WEB_BASE_URL || 'https://fabushi.ombhrum.com';\n const official = [\n {\n botId: 'bot.global-dharma',\n miniAppId: 'official.global-dharma',\n title: '全球法布施',\n subtitle: '地区、循环、场能都在对话框上方设置',\n initials: '法',\n icon: 'public',\n avatarColor: '#4CAF7A',\n kind: 'global_dharma',\n entryUrl: process.env.GLOBAL_DHARMA_MINIAPP_URL,\n greeting: '把要分享的文字、链接或素材发给我,我会按上方选择的地区与模式启动全球法布施。',\n inputHint: '输入要法布施的文字/链接,或点 + 添加素材',\n permissions: [\n 'app.context',\n 'bot.chat',\n 'auth.session',\n 'wallet.balance',\n 'payments.entitlement',\n 'payments.fudeGold',\n 'payments.alipay',\n 'files.pick',\n 'network.http',\n 'network.udp',\n 'network.interfaces',\n 'system.keepAwake',\n 'hotspot.settings',\n 'local.loopback',\n 'openclaw.status',\n 'openclaw.chat',\n 'desktop.control',\n 'fs.readWrite',\n 'runtime.process',\n ],\n },\n {\n botId: 'bot.flashcards',\n miniAppId: 'official.flashcards',\n title: '背诵闪卡制作',\n subtitle: '随机挖空 / AI 制卡从顶部模式按钮选择',\n initials: '卡',\n icon: 'flashcards',\n avatarColor: '#7E57C2',\n kind: 'flashcards',\n entryUrl: process.env.FLASHCARDS_MINIAPP_URL,\n greeting: '粘贴经文、文章正文或链接即可制作背诵闪卡。制卡模式请在上方按钮切换。',\n inputHint: '粘贴链接或正文,发送后制作闪卡',\n permissions: ['app.context', 'bot.chat', 'flashcards.create', 'openclaw.status', 'openclaw.chat'],\n },\n {\n botId: 'bot.platform-publish',\n miniAppId: 'official.platform-publish',\n title: '法布施到平台',\n subtitle: '选择平台后生成发布草稿并打开入口',\n initials: '发',\n icon: 'publish',\n avatarColor: '#FF9F43',\n kind: 'platform_publish',\n entryUrl: process.env.PLATFORM_PUBLISH_MINIAPP_URL,\n greeting: '把要发布的正文或链接发给我,上方选择平台后,我会生成发布草稿并打开对应平台入口。',\n inputHint: '输入要发布到平台的正文/链接',\n permissions: [\n 'app.context',\n 'bot.chat',\n 'platform.publish',\n 'files.pick',\n 'fs.readWrite',\n 'runtime.process',\n 'shell.execute',\n 'browser.external',\n ],\n },\n {\n botId: 'bot.father',\n miniAppId: 'official.bot-father',\n title: '机器人之父',\n subtitle: '用对话生成个人沙箱小程序',\n initials: '父',\n icon: 'bot_father',\n avatarColor: '#3D8BFF',\n kind: 'bot_father',\n entryUrl: process.env.BOT_FATHER_MINIAPP_URL,\n greeting: '告诉我你想要的小程序,我会生成 manifest、界面和权限声明,并放进你的个人沙箱。',\n inputHint: '描述你想创建的小程序',\n permissions: ['app.context', 'bot.chat', 'miniapps.dev'],\n },\n ];\n return {\n bots: official.map((bot, index) => ({\n ...bot,\n destinationIndex: index,\n source: 'official',\n })),\n miniApps: official.map((bot) => ({\n miniAppId: bot.miniAppId,\n botId: bot.botId,\n title: bot.title,\n subtitle: bot.subtitle,\n entryUrl:\n bot.entryUrl ||\n `${webBase.replace(/\\/+$/, '')}/miniapps/${encodeURIComponent(bot.miniAppId)}`,\n version: '2.0.0',\n permissions: bot.permissions,\n surfaces: ['homePinned', 'chatPanel'],\n theme: 'telegramDark',\n signature: 'official',\n reviewStatus: 'trusted',\n source: 'official',\n })),\n };\n}\n\nasync function readMiniAppStore() {\n try {\n const raw = await fs.promises.readFile(miniAppsStorePath, 'utf8');\n const decoded = JSON.parse(raw);\n return {\n apps: Array.isArray(decoded.apps) ? decoded.apps : [],\n };\n } catch (error) {\n if (error?.code === 'ENOENT') return { apps: [] };\n throw error;\n }\n}\n\nasync function writeMiniAppStore(store) {\n await fs.promises.mkdir(miniAppsDir, { recursive: true });\n await fs.promises.writeFile(\n miniAppsStorePath,\n JSON.stringify({ apps: store.apps || [] }, null, 2),\n );\n}\n\nfunction miniAppEntryUrl(req, miniAppId) {\n const origin = requestOrigin(req) || '';\n const pathName = `/api/miniapps/dev/${encodeURIComponent(miniAppId)}/index.html`;\n return origin ? new URL(pathName, `${origin}/`).toString() : pathName;\n}\n\nfunction sanitizeMiniAppPermissions(value, { allowHighRisk = false } = {}) {\n const raw = Array.isArray(value) ? value : [];\n const normalized = raw\n .map((item) => readText(item).toLowerCase())\n .filter((item) => /^[a-z][a-z0-9_.:-]{0,64}$/.test(item));\n const deduped = [...new Set(['app.context', 'bot.chat', ...normalized])];\n return deduped.filter((permission) => allowHighRisk || !miniAppHighRiskPermissions.has(permission));\n}\n\nfunction miniAppScan(permissions, sourceHtml = '') {\n const highRisk = permissions.filter((permission) => miniAppHighRiskPermissions.has(permission));\n const blockedPatterns = [\n /authorization\\s*:\\s*['\"`]bearer/i,\n /desktop_control_bridge_token/i,\n /openclaw_gateway_token/i,\n /eval\\s*\\(/i,\n ];\n const blocked = blockedPatterns\n .filter((pattern) => pattern.test(sourceHtml))\n .map((pattern) => pattern.toString());\n return {\n passed: blocked.length === 0,\n highRiskPermissions: highRisk,\n blockedPatterns: blocked,\n };\n}\n\nfunction inferMiniAppPermissionsFromText(...values) {\n const text = values.map((value) => readText(value)).join('\\n').toLowerCase();\n const rules = [\n ['auth.session', [/auth\\.(getsession|requirelogin)/, /登录|用户信息|宿主账号/]],\n ['auth.token', [/auth\\.getaccesstoken/, /access token|访问 token/]],\n ['payments.alipay', [/payments\\.alipay/, /支付宝|支付|购买|订单/]],\n ['payments.entitlement', [/payments\\.checkentitlement|权益|解锁状态|购买状态/]],\n ['payments.fudeGold', [/payments\\.requestpayment|福德金|代币|钱包|余额/]],\n ['wallet.balance', [/wallet\\.getbalance|福德金|代币|钱包|余额/]],\n ['network.http', [/network\\.http|http\\.fetch|fetch\\s*\\(/, /链接正文|抓取网页|http 请求|网页内容/]],\n ['network.udp', [/network\\.udp|udp|datagram/, /全球法布施|法布施发送|数据报/]],\n ['network.interfaces', [/network\\.interfaces|网卡|网络接口|广播地址/]],\n ['system.keepAwake', [/system\\.keepawake|保持唤醒|后台任务|keep awake/]],\n ['hotspot.settings', [/hotspot\\.opensettings|热点|本地场能/]],\n ['local.loopback', [/localloopback\\./, /localhost|127\\.0\\.0\\.1|本地回环|本地转经轮/]],\n ['flashcards.create', [/flashcards\\./, /闪卡|背诵卡|挖空/]],\n ['platform.publish', [/platformpublish\\./, /发布草稿|发布到平台|公众号|小红书|知乎/]],\n ['files.pick', [/files\\.pick/, /选择文件|上传文件|本地素材/]],\n ['fs.readWrite', [/fs\\.(writefile|readfile)/, /本地文件|写文件|读文件|文件处理/]],\n ['runtime.process', [/runtime\\.process|process\\.execute/, /本地进程|执行程序|命令行|cli|playwright/]],\n ['shell.execute', [/shell\\.execute/, /shell/]],\n ['browser.external', [/browser\\.open/, /浏览器|打开网页|打开平台/]],\n ['desktop.control', [/desktopcontrol\\./, /桌面控制|点击|键盘|鼠标/]],\n ['openclaw.chat', [/openclaw\\.chat/, /openclaw/]],\n ];\n return rules\n .filter(([, patterns]) => patterns.some((pattern) => pattern.test(text)))\n .map(([permission]) => permission);\n}\n\nfunction miniAppRecordToBot(record) {\n return {\n botId: record.botId,\n miniAppId: record.miniAppId,\n title: record.title,\n subtitle: record.subtitle,\n initials: record.initials || '小',\n icon: record.icon || 'code',\n avatarColor: record.avatarColor || '#3D8BFF',\n kind: 'third_party',\n greeting: record.greeting || '你好,我是这个小程序的机器人。',\n inputHint: record.inputHint || '输入消息,或打开小程序面板',\n permissions: record.permissions || ['app.context', 'bot.chat'],\n source: record.source || 'sandbox',\n };\n}\n\nfunction miniAppRecordToManifest(req, record) {\n return {\n miniAppId: record.miniAppId,\n botId: record.botId,\n title: record.title,\n subtitle: record.subtitle,\n entryUrl: miniAppEntryUrl(req, record.miniAppId),\n version: record.version || '0.0.1',\n permissions: record.permissions || ['app.context', 'bot.chat'],\n surfaces: record.surfaces || ['chatPanel'],\n theme: record.theme || 'telegramDark',\n signature: record.signature || `sandbox:${record.ownerId}`,\n reviewStatus: record.reviewStatus || 'sandbox',\n source: record.source || 'sandbox',\n };\n}\n\nfunction safeMiniAppId(value) {\n return readText(value).replace(/[^a-zA-Z0-9_.-]/g, '-').slice(0, 80);\n}\n\nfunction titleFromPromptForMiniApp(prompt) {\n const text = readText(prompt).replace(/\\s+/g, ' ');\n if (!text) return '个人小程序';\n return text.length <= 18 ? text : text.slice(0, 18);\n}\n\nfunction escapeHtml(value) {\n return String(value || '')\n .replace(/&/g, '&')\n .replace(/</g, '<')\n .replace(/>/g, '>')\n .replace(/\"/g, '"')\n .replace(/'/g, ''');\n}\n\nfunction generatedMiniAppHtml({ title, prompt }) {\n const safeTitle = escapeHtml(title);\n const safePrompt = escapeHtml(prompt);\n const promptJson = JSON.stringify(readText(prompt));\n return `<!doctype html>\n<html lang=\"zh-Hans\">\n<head>\n <meta charset=\"utf-8\" />\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n <title>${safeTitle}</title>\n <style>\n body { margin:0; font-family:-apple-system,BlinkMacSystemFont,\"Segoe UI\",sans-serif; background:#0f1722; color:#fff; }\n main { padding:20px; display:grid; gap:14px; }\n .card { border:1px solid #263445; background:#17212b; border-radius:16px; padding:16px; }\n button { border:0; border-radius:999px; padding:10px 14px; background:#3390ec; color:white; font-weight:800; }\n pre { white-space:pre-wrap; color:#9ec7ff; }\n </style>\n</head>\n<body>\n <main>\n <section class=\"card\">\n <h1>${safeTitle}</h1>\n <p>这是机器人之父根据你的描述生成的个人沙箱小程序。</p>\n <pre>${safePrompt}</pre>\n </section>\n <section class=\"card\">\n <button id=\"context\">读取宿主上下文</button>\n <button id=\"spec\">读取宿主能力</button>\n <button id=\"chat\">让机器人解释这个小程序</button>\n <pre id=\"output\">等待操作...</pre>\n </section>\n </main>\n <script>\n const out = document.getElementById('output');\n async function invoke(method, params) {\n if (!window.FabushiMiniApp) throw new Error('FabushiMiniApp host SDK not ready');\n const result = await window.FabushiMiniApp.invoke(method, params || {});\n out.textContent = JSON.stringify(result, null, 2);\n }\n document.getElementById('context').onclick = () => invoke('app.getContext');\n document.getElementById('spec').onclick = () => invoke('app.getHostApiSpec');\n const prompt = ${promptJson};\n document.getElementById('chat').onclick = () => invoke('bot.sendMessage', {\n message: '请简要说明这个小程序能做什么:' + prompt\n });\n const seenCommands = new Set();\n function deliverMiniAppCommand(detail) {\n if (!detail || typeof detail !== 'object') return;\n const commandKey = detail.commandId || detail.id || [detail.createdAt, detail.command, detail.rawText || detail.text].filter(Boolean).join(':');\n if (commandKey) {\n if (seenCommands.has(commandKey)) return;\n seenCommands.add(commandKey);\n }\n handleMiniAppCommand(detail);\n }\n async function handleMiniAppCommand(detail) {\n out.textContent = '收到后台命令:' + JSON.stringify(detail, null, 2);\n try {\n await invoke('bot.postMessage', {\n commandId: detail.commandId || detail.id,\n message: '「${safeTitle}」已收到后台命令:' + (detail.args || detail.text || '')\n });\n await invoke('bot.reportCommandResult', {\n commandId: detail.commandId || detail.id,\n status: 'completed',\n message: '「${safeTitle}」后台命令已处理。'\n });\n } catch (error) {\n out.textContent = String(error && error.message || error);\n }\n }\n window.addEventListener('fabushi-miniapp-command', (event) => {\n deliverMiniAppCommand(event.detail || {});\n });\n if (window.__fabushiLastMiniAppCommand) {\n const cmd = window.__fabushiLastMiniAppCommand;\n window.__fabushiLastMiniAppCommand = null;\n (window.queueMicrotask || ((fn) => setTimeout(fn, 0)))(() => {\n deliverMiniAppCommand(cmd);\n });\n }\n </script>\n</body>\n</html>`;\n}\n\nfunction writeBotFatherWorkspaceInstructions(workspaceDir) {\n const instructions = [\n '# Fabushi Mini App Workspace',\n '',\n 'You are developing a Fabushi mini app in this directory.',\n 'Use your normal coding workflow: inspect files, create or edit code, run checks, and fix problems.',\n 'The required runtime entry point is index.html.',\n 'index.html must be a complete self-contained HTML document with inline CSS and JavaScript.',\n 'Do not load external scripts, stylesheets, fonts, or remote application code.',\n 'Host capabilities are available only through window.FabushiMiniApp.invoke(method, params).',\n 'Do not include Authorization tokens, desktop bridge tokens, OpenClaw tokens, eval, or dynamically fetched executable code.',\n 'Before finishing, verify that index.html contains html and body elements and can run without a build step.',\n 'Do not paste the full source into the final response; write the files and briefly summarize the result.',\n '',\n ].join('\\n');\n fs.writeFileSync(path.join(workspaceDir, 'AGENTS.md'), instructions, 'utf8');\n}\n\nfunction readBotFatherWorkspaceHtml(workspaceDir) {\n const entryPath = path.join(workspaceDir, 'index.html');\n if (!fs.existsSync(entryPath)) return '';\n return extractMiniAppHtml(fs.readFileSync(entryPath, 'utf8'));\n}\n\nasync function generateBotFatherMiniAppHtml({ title, prompt, user, miniAppId }) {\n const workspaceDir = miniAppWorkspaceDir(\n user,\n miniAppId || crypto.randomUUID(),\n );\n writeBotFatherWorkspaceInstructions(workspaceDir);\n try {\n const result = await runAgentModel({\n user,\n mode: 'miniapp_generation',\n preferCodex: true,\n requireCodex: true,\n sandboxMode: 'workspace-write',\n maxCompletionTokens: botFatherMaxCompletionTokens,\n workingDirectory: workspaceDir,\n messages: [\n {\n role: 'system',\n content: [\n '你是 Fabushi App 的小程序开发 Agent。',\n '你运行在一个独立的小程序工作区中。',\n '不要把自己当成 HTML 生成器;请像真实软件工程师一样完成任务。',\n '先分析需求,再创建或修改项目文件,必要时运行测试和修复错误。',\n '最终回复只需要说明完成情况,不需要把完整源码粘贴到聊天中。',\n '小程序只能通过 window.FabushiMiniApp.invoke(method, params) 调用宿主能力。',\n '先调用 app.getHostApiSpec 或 app.getCapabilities 判断宿主版本和已授权权限,再显示对应 UI。',\n '聊天后台调用协议:优先使用 window.FabushiMiniApp.bot.onCommand(\"/start\", handler) 或 bot.onAnyCommand(handler);如果手写监听 window 的 fabushi-miniapp-command 事件,注册后也要读取 window.__fabushiLastMiniAppCommand 并按 commandId 去重补处理。detail.command 默认为 /start,detail.args 是用户文本;处理完成后调用 bot.postMessage 或 bot.reportCommandResult 把进度/结果写回聊天框。',\n '标准能力:auth.getSession/auth.requireLogin 复用宿主登录;payments.requestPayment 使用福德金支付并由宿主登记权益;payments.checkEntitlement 查询商品是否已解锁;payments.alipay.pay/createOrder 仅作为宿主官方现金支付兜底;network.http.fetch/network.udp/network.interfaces 提供 HTTP、UDP 和网卡原语;system.keepAwake/hotspot.openSettings 提供系统级辅助;flashcards.createDeck 接入背诵闪卡;platformPublish.createDraft 接入发布草稿;fs.writeFile/fs.readFile、runtime.process.execute、browser.open 用于本地文件、进程和浏览器。',\n '高危权限包括 auth.token、payments.alipay、payments.fudeGold、network.http、network.udp、network.interfaces、system.keepAwake、hotspot.settings、local.loopback、fs.readWrite、runtime.process、shell.execute、browser.external、desktop.control;只有用户需求明确需要时才使用。',\n '禁止外链脚本、eval、Authorization token、桌面桥 token、OpenClaw token。',\n '不要调用 auth.getAccessToken,除非用户明确要求对接需要 token 的受信服务。',\n '优先实现一个可点击、可立即使用的小面板,包含 app.getContext 或 bot.sendMessage 示例。',\n ].join('\\n'),\n },\n {\n role: 'user',\n content: [\n `小程序标题:${title}`,\n `用户需求:${prompt}`,\n '请直接在当前工作区中完成小程序,实现并验证 index.html。',\n ].join('\\n'),\n },\n ],\n });\n let sourceHtml = readBotFatherWorkspaceHtml(workspaceDir);\n let finalResult = result;\n let repaired = false;\n if (!sourceHtml) {\n logger.warn(\n {\n provider: result.provider || 'ai',\n model: result.model || '',\n responseLength: readText(result.message).length,\n workspaceDir,\n indexExists: fs.existsSync(path.join(workspaceDir, 'index.html')),\n },\n 'Bot Father Codex did not create a valid index.html; asking Codex to repair the workspace',\n );\n finalResult = await runAgentModel({\n user,\n mode: 'miniapp_generation_repair',\n preferCodex: true,\n requireCodex: true,\n sandboxMode: 'workspace-write',\n maxCompletionTokens: botFatherMaxCompletionTokens,\n workingDirectory: workspaceDir,\n messages: [\n {\n role: 'system',\n content: [\n '你是 Fabushi App 的小程序开发 Agent。',\n '检查当前工作区并修复它,不要只在回复中输出源码。',\n '必须在工作区中创建或修复完整的 index.html。',\n 'index.html 必须包含完整 html、body、内联 CSS 和 JavaScript,并且无需构建即可运行。',\n '禁止外链脚本、eval、Authorization token、桌面桥 token、OpenClaw token。',\n '需要宿主能力时只能调用 window.FabushiMiniApp.invoke(method, params)。',\n ].join('\\n'),\n },\n {\n role: 'user',\n content: `标题:${title}\\n需求:${prompt}\\n请检查现有文件,完成修复并验证 index.html,然后简要报告。`,\n },\n ],\n });\n sourceHtml = readBotFatherWorkspaceHtml(workspaceDir);\n repaired = Boolean(sourceHtml);\n }\n if (!sourceHtml) {\n const error = new Error('Codex CLI did not create a valid index.html in the mini app workspace');\n error.statusCode = 502;\n throw error;\n }\n return {\n sourceHtml,\n generation: {\n provider: finalResult.provider || 'ai',\n model: finalResult.model || '',\n repaired,\n workspaceId: path.basename(workspaceDir),\n threadId: finalResult.threadId || null,\n },\n };\n } catch (error) {\n logger.error(\n {\n error: readText(error?.message || error),\n workspaceDir,\n },\n 'Bot Father Codex workspace generation failed',\n );\n throw error;\n }\n}\n\nfunction clientContextMessage(client) {\n if (!client || typeof client !== 'object') return '';\n const selectedModel = readText(client.selectedModel || client.model);\n const project = client.project && typeof client.project === 'object' ? client.project : {};\n const projectName = readText(project.name);\n const projectPath = readText(project.path);\n const lines = ['桌面端上下文:'];\n if (selectedModel) lines.push(`用户选择的 DeepSeek 模型:${normalizeDeepSeekModelName(selectedModel)}`);\n if (projectPath) {\n lines.push(`当前项目名称:${projectName || projectPath}`);\n lines.push(`当前项目路径:${projectPath}`);\n lines.push('如果本轮任务涉及读取、修改、运行、分析或整理项目,请优先围绕当前项目路径工作;不要擅自操作其他目录。');\n }\n return lines.length > 1 ? lines.join('\\n') : '';\n}\nfunction unwrapPayload(payload) {\n if (!payload || typeof payload !== 'object') return {};\n return payload.data && typeof payload.data === 'object' ? payload.data : payload;\n}\n\nfunction readFirstText(values) {\n for (const value of values) {\n const text = readText(value);\n if (text) return text;\n }\n return '';\n}\n\nfunction activeFlag(value) {\n if (value === true) return true;\n if (value === 1) return true;\n if (typeof value !== 'string') return false;\n return ['true', '1', 'yes', 'active'].includes(value.trim().toLowerCase());\n}\n\nfunction futureDate(value) {\n const text = readText(value);\n if (!text) return false;\n const timestamp = Date.parse(text);\n return Number.isFinite(timestamp) && timestamp > Date.now();\n}\n\nfunction normalizeMembershipPayload(payload) {\n if (!payload || typeof payload !== 'object') return false;\n const data = unwrapPayload(payload);\n const membership = data.membership && typeof data.membership === 'object' ? data.membership : data;\n const type = readText(membership.type || membership.membershipType || data.membershipType).toLowerCase();\n const status = readText(membership.status || data.status).toLowerCase();\n const expiry = membership.expiresAt ||\n membership.expires_at ||\n membership.membershipExpiry ||\n membership.membership_expires_at ||\n data.membershipExpiry ||\n data.membership_expires_at;\n const hasExplicitActiveFlag =\n membership.isActive !== undefined ||\n membership.active !== undefined ||\n membership.hasPremiumAccess !== undefined ||\n membership.hasPremiumMembership !== undefined ||\n membership.isPremium !== undefined;\n\n if (hasExplicitActiveFlag) {\n return Boolean(\n activeFlag(membership.isActive) ||\n activeFlag(membership.active) ||\n activeFlag(membership.hasPremiumAccess) ||\n activeFlag(membership.hasPremiumMembership) ||\n activeFlag(membership.isPremium),\n );\n }\n\n return Boolean(\n status === 'active' ||\n type === 'paid' ||\n type === 'premium' ||\n type === 'trial' ||\n futureDate(expiry),\n );\n}\n\nfunction extractRemoteAccount(payload) {\n const data = unwrapPayload(payload);\n const nestedUser = data.user && typeof data.user === 'object' ? data.user : {};\n const id = readFirstText([\n data.userId,\n data.user_id,\n data.userNo,\n data.user_no,\n data.id,\n nestedUser.userId,\n nestedUser.user_id,\n nestedUser.userNo,\n nestedUser.user_no,\n nestedUser.id,\n ]);\n const username = readFirstText([data.username, data.name, nestedUser.username, nestedUser.name]);\n const email = readFirstText([data.email, nestedUser.email]);\n const membership = data.membership && typeof data.membership === 'object' ? data.membership : null;\n return {\n authenticated: Boolean(id || username || email || membership || data.success === true),\n id,\n username,\n email,\n isMember: normalizeMembershipPayload(payload),\n membership,\n };\n}\n\nasync function resolveRemoteAccount(token) {\n const fallback = {\n authenticated: false,\n id: '',\n username: '',\n email: '',\n isMember: false,\n membership: null,\n };\n if (!token || !fabushiApiBaseUrl) return fallback;\n\n const endpoints = [\n '/api/stripe/membership-status',\n '/api/alipay/check-membership',\n '/api/auth/user-info',\n ];\n\n for (const endpoint of endpoints) {\n try {\n const response = await fetch(`${fabushiApiBaseUrl}${endpoint}`, {\n headers: {\n Accept: 'application/json',\n Authorization: `Bearer ${token}`,\n },\n signal: AbortSignal.timeout(8_000),\n });\n if (response.status === 401 || response.status === 403) return fallback;\n if (!response.ok) continue;\n const payload = await response.json();\n const account = extractRemoteAccount(payload);\n if (account.authenticated || account.isMember) {\n return {\n ...fallback,\n ...account,\n authenticated: true,\n };\n }\n } catch (error) {\n logger.debug({ endpoint, error: String(error) }, 'membership lookup skipped');\n }\n }\n\n return fallback;\n}\n\nasync function resolveUser(req, body = {}) {\n const token = bearerToken(req);\n const usernameHint = safeUserText(body.username || req.query.username);\n\n const testToken = readText(process.env.TEST_ACCOUNT_TOKEN);\n if (testToken && token && token === testToken) {\n return {\n userId: 'user:test_account',\n username: 'TestAccount',\n tokenHash: sha256(token).slice(0, 24),\n isAuthenticated: true,\n isMember: true,\n isTestAccount: true,\n membership: { type: 'lifetime', active: true },\n };\n }\n\n const tokenHash = token ? sha256(token).slice(0, 24) : '';\n const anonymousHash = sha256(`${req.ip}|${req.get('User-Agent') || ''}`).slice(0, 24);\n const remoteAccount = await resolveRemoteAccount(token);\n const remoteIdentity = remoteAccount.id ||\n remoteAccount.username ||\n remoteAccount.email ||\n (remoteAccount.authenticated ? tokenHash : '');\n const userId = remoteAccount.authenticated\n ? `user:${remoteIdentity}`\n : tokenHash\n ? `token:${tokenHash}`\n : `anon:${anonymousHash}`;\n return {\n userId,\n username: remoteAccount.username || usernameHint,\n tokenHash,\n isAuthenticated: remoteAccount.authenticated,\n isMember: remoteAccount.isMember,\n membership: remoteAccount.membership,\n };\n}\n\nfunction base64UrlJson(value) {\n return Buffer.from(JSON.stringify(value), 'utf8').toString('base64url');\n}\n\nfunction timingSafeEqualText(left, right) {\n const leftBuffer = Buffer.from(String(left || ''));\n const rightBuffer = Buffer.from(String(right || ''));\n if (leftBuffer.length !== rightBuffer.length) return false;\n return crypto.timingSafeEqual(leftBuffer, rightBuffer);\n}\n\nfunction signCodexAdapterPayload(encodedPayload) {\n return crypto\n .createHmac('sha256', codexAdapterSecret)\n .update(encodedPayload)\n .digest('base64url');\n}\n\nfunction createCodexAdapterToken(user, options = {}) {\n const requestedMaxCompletionTokens = Number(options.maxCompletionTokens || 0);\n const payload = {\n sub: user.userId,\n username: user.username || '',\n tokenHash: user.tokenHash || '',\n isAuthenticated: Boolean(user.isAuthenticated),\n isMember: Boolean(user.isMember),\n isTestAccount: Boolean(user.isTestAccount),\n membership: user.membership || {},\n recordUsage: options.recordUsage !== false,\n maxCompletionTokens:\n Number.isFinite(requestedMaxCompletionTokens) && requestedMaxCompletionTokens > 0\n ? Math.floor(requestedMaxCompletionTokens)\n : null,\n exp: Date.now() + codexAdapterTokenTtlMs,\n };\n const encodedPayload = base64UrlJson(payload);\n return `${codexAdapterTokenPrefix}${encodedPayload}.${signCodexAdapterPayload(encodedPayload)}`;\n}\n\nfunction resolveCodexAdapterToken(token) {\n const raw = readText(token);\n if (!raw.startsWith(codexAdapterTokenPrefix)) return null;\n\n const signed = raw.slice(codexAdapterTokenPrefix.length);\n const separator = signed.lastIndexOf('.');\n if (separator < 1) return null;\n\n const encodedPayload = signed.slice(0, separator);\n const signature = signed.slice(separator + 1);\n const expectedSignature = signCodexAdapterPayload(encodedPayload);\n if (!timingSafeEqualText(signature, expectedSignature)) return null;\n\n let payload;\n try {\n payload = JSON.parse(Buffer.from(encodedPayload, 'base64url').toString('utf8'));\n } catch {\n return null;\n }\n\n if (!payload || typeof payload !== 'object') return null;\n if (Number(payload.exp || 0) < Date.now()) return null;\n\n const userId = safeUserText(payload.sub);\n if (!userId) return null;\n\n return {\n user: {\n userId,\n username: safeUserText(payload.username),\n tokenHash: safeUserText(payload.tokenHash),\n isAuthenticated: Boolean(payload.isAuthenticated),\n isMember: Boolean(payload.isMember),\n isTestAccount: Boolean(payload.isTestAccount),\n membership:\n payload.membership && typeof payload.membership === 'object'\n ? payload.membership\n : {},\n },\n recordUsage: payload.recordUsage !== false,\n maxCompletionTokens:\n Number.isFinite(Number(payload.maxCompletionTokens)) && Number(payload.maxCompletionTokens) > 0\n ? Math.floor(Number(payload.maxCompletionTokens))\n : null,\n };\n}\n\nasync function resolveCodexResponsesBilling(req) {\n const token = bearerToken(req);\n const adapter = resolveCodexAdapterToken(token);\n if (adapter) return adapter;\n\n if (deepseekApiKey && token === deepseekApiKey) {\n const error = new Error('Codex DeepSeek adapter now requires first-party user billing');\n error.statusCode = 401;\n throw error;\n }\n\n return {\n user: await resolveUser(req, req.body || {}),\n recordUsage: true,\n };\n}\n\nfunction usageFor(userId) {\n const month = monthKey();\n const row = statements.getUsage.get(userId, month);\n return {\n month,\n used: Number(row?.totalTokens || 0),\n };\n}\n\nfunction enforceTokenBudget(user, estimatedTokensForRequest) {\n if (user && user.isTestAccount) {\n return { limit: 999999999, usage: { month: monthKey(), used: 0 } };\n }\n if (requireMemberForAi && !user.isMember) {\n const error = new Error('大乘 AI 需要开通会员后使用');\n error.statusCode = 403;\n error.code = 'AI_MEMBERSHIP_REQUIRED';\n error.details = {\n requireMemberForAi,\n monthlyLimit: memberMonthlyLimit,\n remainingTokens: 0,\n };\n throw error;\n }\n\n const limit = user.isMember ? memberMonthlyLimit : freeMonthlyLimit;\n const usage = usageFor(user.userId);\n if (usage.used + estimatedTokensForRequest > limit) {\n const error = new Error('本月 AI token 额度已不足');\n error.statusCode = 429;\n error.code = 'USER_TOKEN_QUOTA_EXCEEDED';\n error.details = {\n monthlyLimit: limit,\n usedTokens: usage.used,\n remainingTokens: Math.max(0, limit - usage.used),\n };\n throw error;\n }\n return { limit, usage };\n}\n\nfunction completionBudgetFor(\n budget,\n estimatedPromptTokens,\n maximumCompletionTokens = deepseekMaxCompletionTokens,\n) {\n const remainingAfterPrompt = budget.limit - budget.usage.used - estimatedPromptTokens;\n return Math.max(1, Math.min(maximumCompletionTokens, Math.floor(remainingAfterPrompt)));\n}\n\nfunction recordUsage(userId, totalTokens) {\n if (!totalTokens || totalTokens < 1) return;\n statements.upsertUsage.run({\n userId,\n month: monthKey(),\n totalTokens,\n updatedAt: nowIso(),\n });\n}\n\nfunction titleFromMessage(message) {\n const compact = message.replace(/\\s+/g, ' ').trim();\n if (!compact) return '新对话';\n return compact.length > 22 ? `${compact.slice(0, 22)}...` : compact;\n}\n\nfunction normalizeMessages(rows) {\n return rows.map((row) => ({\n role: row.role,\n content: row.content,\n }));\n}\n\nfunction normalizeMaxCompletionTokens(\n value,\n maximumCompletionTokens = deepseekMaxCompletionTokens,\n) {\n const numeric = Number(value || maximumCompletionTokens);\n if (!Number.isFinite(numeric) || numeric < 1) return maximumCompletionTokens;\n return Math.max(1, Math.min(maximumCompletionTokens, Math.floor(numeric)));\n}\n\nfunction normalizeDeepSeekModelName(value) {\n const raw = readText(value || deepseekModel);\n if (!raw) return deepseekModel;\n const modelId = raw.includes('/') ? raw.split('/').pop() : raw;\n return modelId || deepseekModel;\n}\n\nasync function callDeepSeek(messages, options = {}) {\n if (!deepseekApiKey) {\n const error = new Error('DeepSeek API key is not configured');\n error.statusCode = 503;\n throw error;\n }\n\n const model = normalizeDeepSeekModelName(options.model);\n const response = await fetch(`${deepseekBaseUrl}/chat/completions`, {\n method: 'POST',\n headers: {\n Accept: 'application/json',\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${deepseekApiKey}`,\n },\n body: JSON.stringify({\n model,\n messages,\n temperature: 0.4,\n max_tokens: normalizeMaxCompletionTokens(\n options.maxCompletionTokens,\n options.maximumCompletionTokens || deepseekMaxCompletionTokens,\n ),\n stream: false,\n }),\n signal: AbortSignal.timeout(60_000),\n });\n\n const bodyText = await response.text();\n let payload;\n try {\n payload = bodyText ? JSON.parse(bodyText) : {};\n } catch {\n payload = { error: bodyText };\n }\n\n if (!response.ok) {\n const upstreamMessage = payload?.error?.message || payload?.message || '';\n const friendlyMessage =\n response.status === 402 || /insufficient\\s+balance/i.test(upstreamMessage)\n ? '大乘 AI 服务额度暂不可用,请联系管理员充值 DeepSeek 账户后重试。'\n : upstreamMessage || `DeepSeek request failed: ${response.status}`;\n const error = new Error(friendlyMessage);\n error.statusCode = response.status;\n if (response.status === 402 || /insufficient\\s+balance/i.test(upstreamMessage)) {\n error.code = 'DEEPSEEK_PROVIDER_QUOTA';\n }\n throw error;\n }\n\n const message = payload?.choices?.[0]?.message?.content?.trim();\n if (!message) {\n const error = new Error('DeepSeek returned an empty response');\n error.statusCode = 502;\n throw error;\n }\n\n return {\n message,\n model,\n usage: {\n promptTokens: Number(payload?.usage?.prompt_tokens || 0),\n completionTokens: Number(payload?.usage?.completion_tokens || 0),\n totalTokens: Number(payload?.usage?.total_tokens || 0),\n },\n };\n}\n\nasync function callDeepSeekStream(messages, callbacks = {}) {\n if (!deepseekApiKey) {\n const error = new Error('DeepSeek API key is not configured');\n error.statusCode = 503;\n throw error;\n }\n\n const model = normalizeDeepSeekModelName(callbacks.model);\n const response = await fetch(`${deepseekBaseUrl}/chat/completions`, {\n method: 'POST',\n headers: {\n Accept: 'text/event-stream',\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${deepseekApiKey}`,\n },\n body: JSON.stringify({\n model,\n messages,\n temperature: 0.4,\n max_tokens: normalizeMaxCompletionTokens(\n callbacks.maxCompletionTokens,\n callbacks.maximumCompletionTokens || deepseekMaxCompletionTokens,\n ),\n stream: true,\n stream_options: { include_usage: true },\n }),\n signal: callbacks.signal || AbortSignal.timeout(90_000),\n });\n\n if (!response.ok) {\n const bodyText = await response.text();\n let payload;\n try {\n payload = bodyText ? JSON.parse(bodyText) : {};\n } catch {\n payload = { error: bodyText };\n }\n const upstreamMessage = payload?.error?.message || payload?.message || '';\n const friendlyMessage =\n response.status === 402 || /insufficient\\s+balance/i.test(upstreamMessage)\n ? '大乘 AI 服务额度暂不可用,请联系管理员充值 DeepSeek 账户后重试。'\n : upstreamMessage || `DeepSeek request failed: ${response.status}`;\n const error = new Error(friendlyMessage);\n error.statusCode = response.status;\n if (response.status === 402 || /insufficient\\s+balance/i.test(upstreamMessage)) {\n error.code = 'DEEPSEEK_PROVIDER_QUOTA';\n }\n throw error;\n }\n\n let buffer = '';\n let message = '';\n let usage = {\n promptTokens: 0,\n completionTokens: 0,\n totalTokens: 0,\n };\n const decoder = new TextDecoder();\n\n for await (const chunk of response.body) {\n buffer += decoder.decode(chunk, { stream: true });\n const lines = buffer.split(/\\r?\\n/);\n buffer = lines.pop() || '';\n\n for (const rawLine of lines) {\n const line = rawLine.trim();\n if (!line.startsWith('data:')) continue;\n const data = line.slice('data:'.length).trim();\n if (!data || data === '[DONE]') continue;\n\n let payload;\n try {\n payload = JSON.parse(data);\n } catch {\n continue;\n }\n\n const delta = payload?.choices?.[0]?.delta?.content || '';\n if (delta) {\n message += delta;\n callbacks.onToken?.(delta);\n }\n\n if (payload?.usage) {\n usage = {\n promptTokens: Number(payload.usage.prompt_tokens || usage.promptTokens || 0),\n completionTokens: Number(payload.usage.completion_tokens || usage.completionTokens || 0),\n totalTokens: Number(payload.usage.total_tokens || usage.totalTokens || 0),\n };\n }\n }\n }\n\n message = message.trim();\n if (!message) {\n const error = new Error('DeepSeek returned an empty response');\n error.statusCode = 502;\n throw error;\n }\n\n return { message, model, usage };\n}\n\nfunction createCodexDeepSeekRuntime(user = null, runtimeOptions = {}) {\n if (!enableCodexSdkChat || !deepseekApiKey) {\n return {\n enabled: false,\n provider: codexDeepSeekProviderId,\n reason: enableCodexSdkChat ? 'DeepSeek API key is not configured' : 'ENABLE_CODEX_SDK_CHAT is not true',\n };\n }\n\n const adapterToken = user\n ? createCodexAdapterToken(user, {\n maxCompletionTokens: runtimeOptions.maxCompletionTokens,\n })\n : '';\n return {\n enabled: true,\n provider: codexDeepSeekProviderId,\n options: {\n apiKey: adapterToken,\n baseUrl: codexResponsesBaseUrl,\n env: {\n ...process.env,\n DEEPSEEK_API_KEY: adapterToken,\n HOME: codexHomeDir,\n CODEX_HOME: codexHomeDir,\n TMPDIR: codexTempDir,\n XDG_RUNTIME_DIR: codexRuntimeDir,\n XDG_CACHE_HOME: path.join(codexHomeDir, '.cache'),\n XDG_CONFIG_HOME: path.join(codexHomeDir, '.config'),\n XDG_DATA_HOME: path.join(codexHomeDir, '.local', 'share'),\n PATH: process.env.PATH || '/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin',\n },\n config: {\n model_provider: codexDeepSeekProviderId,\n model_providers: {\n [codexDeepSeekProviderId]: {\n name: 'Dacheng DeepSeek Proxy',\n base_url: codexResponsesBaseUrl,\n env_key: 'DEEPSEEK_API_KEY',\n wire_api: 'responses',\n query_params: {},\n },\n },\n },\n },\n threadOptions: {\n model: deepseekModel,\n approvalPolicy: 'never',\n sandboxMode: runtimeOptions.sandboxMode || 'read-only',\n ...(runtimeOptions.workingDirectory\n ? { workingDirectory: runtimeOptions.workingDirectory }\n : {}),\n skipGitRepoCheck: true,\n networkAccessEnabled: true,\n },\n };\n}\n\nfunction codexPromptFromMessages(messages) {\n return messages\n .map((message) => {\n const role = String(message.role || 'user').toUpperCase();\n return `${role}:\\n${message.content}`;\n })\n .join('\\n\\n');\n}\n\nfunction responseContentText(content) {\n if (typeof content === 'string') return content;\n if (!Array.isArray(content)) return '';\n return content\n .map((part) => {\n if (typeof part === 'string') return part;\n if (!part || typeof part !== 'object') return '';\n return String(part.text || part.input_text || part.output_text || '');\n })\n .filter(Boolean)\n .join('\\n');\n}\n\nfunction codexResponsesPrompt(body) {\n const input = Array.isArray(body?.input) ? body.input : [];\n const userItems = input.filter((item) => item?.role === 'user');\n const source = userItems[userItems.length - 1] || input[input.length - 1];\n const prompt = responseContentText(source?.content).trim();\n if (prompt) return prompt;\n if (typeof body?.input === 'string') return body.input.trim();\n return '';\n}\n\nfunction writeResponsesEvent(res, event, data) {\n res.write(`event: ${event}\\n`);\n res.write(`data: ${JSON.stringify(data)}\\n\\n`);\n}\n\nfunction responseUsage(usage = {}) {\n return {\n input_tokens: Number(usage.promptTokens || usage.input_tokens || 0),\n cached_input_tokens: 0,\n output_tokens: Number(usage.completionTokens || usage.output_tokens || 0),\n reasoning_output_tokens: 0,\n total_tokens: Number(usage.totalTokens || usage.total_tokens || 0),\n };\n}\n\nfunction createLibreChatAgentRuntime() {\n if (!enableLibreChatAgentChat || !libreChatAgentApiKey || !libreChatAgentId) {\n return {\n enabled: false,\n provider: 'librechat-agent',\n reason: !enableLibreChatAgentChat\n ? 'ENABLE_LIBRECHAT_AGENT_CHAT is not true'\n : !libreChatAgentApiKey\n ? 'LIBRECHAT_AGENT_API_KEY is not configured'\n : 'LIBRECHAT_AGENT_ID is not configured',\n };\n }\n\n return {\n enabled: true,\n provider: 'librechat-agent',\n model: libreChatAgentId,\n chatCompletionsUrl: `${libreChatAgentsBaseUrl}/chat/completions`,\n };\n}\n\nfunction libreChatUsage(usage = {}) {\n return {\n promptTokens: Number(usage.prompt_tokens || usage.input_tokens || usage.promptTokens || 0),\n completionTokens: Number(usage.completion_tokens || usage.output_tokens || usage.completionTokens || 0),\n totalTokens: Number(usage.total_tokens || usage.totalTokens || 0),\n };\n}\n\nasync function callLibreChatAgent(messages) {\n const runtime = createLibreChatAgentRuntime();\n if (!runtime.enabled) {\n const error = new Error(runtime.reason || 'LibreChat Agent chat is not enabled');\n error.statusCode = 503;\n throw error;\n }\n\n const response = await fetch(runtime.chatCompletionsUrl, {\n method: 'POST',\n headers: {\n Accept: 'application/json',\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${libreChatAgentApiKey}`,\n },\n body: JSON.stringify({\n model: runtime.model,\n messages,\n temperature: 0.4,\n stream: false,\n }),\n signal: AbortSignal.timeout(120_000),\n });\n\n const bodyText = await response.text();\n let payload;\n try {\n payload = bodyText ? JSON.parse(bodyText) : {};\n } catch {\n payload = { error: bodyText };\n }\n\n if (!response.ok) {\n const upstreamMessage = payload?.error?.message || payload?.message || bodyText || '';\n const error = new Error(upstreamMessage || `LibreChat Agent request failed: ${response.status}`);\n error.statusCode = response.status;\n throw error;\n }\n\n const message = payload?.choices?.[0]?.message?.content?.trim();\n if (!message) {\n const error = new Error('LibreChat Agent returned an empty response');\n error.statusCode = 502;\n throw error;\n }\n\n return {\n message,\n usage: libreChatUsage(payload?.usage),\n };\n}\n\nasync function callLibreChatAgentStream(messages, callbacks = {}) {\n const runtime = createLibreChatAgentRuntime();\n if (!runtime.enabled) {\n const error = new Error(runtime.reason || 'LibreChat Agent chat is not enabled');\n error.statusCode = 503;\n throw error;\n }\n\n const response = await fetch(runtime.chatCompletionsUrl, {\n method: 'POST',\n headers: {\n Accept: 'text/event-stream',\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${libreChatAgentApiKey}`,\n },\n body: JSON.stringify({\n model: runtime.model,\n messages,\n temperature: 0.4,\n stream: true,\n }),\n signal: callbacks.signal || AbortSignal.timeout(180_000),\n });\n\n if (!response.ok) {\n const bodyText = await response.text();\n let payload;\n try {\n payload = bodyText ? JSON.parse(bodyText) : {};\n } catch {\n payload = { error: bodyText };\n }\n const upstreamMessage = payload?.error?.message || payload?.message || bodyText || '';\n const error = new Error(upstreamMessage || `LibreChat Agent request failed: ${response.status}`);\n error.statusCode = response.status;\n throw error;\n }\n\n let buffer = '';\n let message = '';\n let usage = { promptTokens: 0, completionTokens: 0, totalTokens: 0 };\n const decoder = new TextDecoder();\n\n for await (const chunk of response.body) {\n buffer += decoder.decode(chunk, { stream: true });\n const lines = buffer.split(/\\r?\\n/);\n buffer = lines.pop() || '';\n\n for (const rawLine of lines) {\n const line = rawLine.trim();\n if (!line.startsWith('data:')) continue;\n const data = line.slice('data:'.length).trim();\n if (!data || data === '[DONE]') continue;\n\n let payload;\n try {\n payload = JSON.parse(data);\n } catch {\n continue;\n }\n\n const delta = payload?.choices?.[0]?.delta?.content || '';\n if (delta) {\n message += delta;\n callbacks.onToken?.(delta);\n }\n\n if (payload?.usage) {\n usage = libreChatUsage(payload.usage);\n }\n }\n }\n\n message = message.trim();\n if (!message) {\n const error = new Error('LibreChat Agent returned an empty response');\n error.statusCode = 502;\n throw error;\n }\n\n return { message, usage };\n}\n\nfunction isDeepSeekQuotaError(error) {\n if (error?.code === 'DEEPSEEK_PROVIDER_QUOTA') return true;\n if (error?.code === 'USER_TOKEN_QUOTA_EXCEEDED' || error?.code === 'AI_MEMBERSHIP_REQUIRED') return false;\n const message = String(error?.message || error || '');\n return /insufficient\\s+balance|balance/i.test(message);\n}\n\nfunction publicAiErrorMessage(error) {\n if (isDeepSeekQuotaError(error)) {\n return '大乘 AI 服务额度暂不可用,请联系管理员充值 DeepSeek 账户后重试。';\n }\n return error?.message || 'Internal Server Error';\n}\n\nfunction responsesPayload({\n responseId,\n itemId,\n status,\n model,\n text = '',\n usage = null,\n error = null,\n}) {\n return {\n id: responseId,\n object: 'response',\n status,\n model,\n output:\n status === 'completed'\n ? [\n {\n id: itemId,\n type: 'message',\n role: 'assistant',\n status: 'completed',\n content: [{ type: 'output_text', text }],\n },\n ]\n : [],\n ...(usage ? { usage: responseUsage(usage) } : {}),\n ...(error\n ? {\n error: {\n message: error.message || String(error),\n type: 'server_error',\n },\n }\n : {}),\n };\n}\n\nasync function callCodexSdkDeepSeek(messages, callbacks = {}) {\n const codexRuntime = createCodexDeepSeekRuntime(callbacks.user, {\n maxCompletionTokens: callbacks.maxCompletionTokens,\n workingDirectory: callbacks.workingDirectory,\n sandboxMode: callbacks.sandboxMode,\n });\n if (!codexRuntime.enabled) {\n const error = new Error(codexRuntime.reason || 'Codex SDK chat is not enabled');\n error.statusCode = 503;\n throw error;\n }\n if (!callbacks.user) {\n const error = new Error('Codex SDK DeepSeek proxy requires a resolved user for billing');\n error.statusCode = 401;\n throw error;\n }\n\n const { Codex } = await import('@openai/codex-sdk');\n const codex = new Codex(codexRuntime.options);\n const thread = callbacks.threadId\n ? codex.resumeThread(callbacks.threadId, codexRuntime.threadOptions)\n : codex.startThread(codexRuntime.threadOptions);\n const prompt = codexPromptFromMessages(messages);\n const turnOptions = {\n ...(callbacks.outputSchema ? { outputSchema: callbacks.outputSchema } : {}),\n ...(callbacks.signal ? { signal: callbacks.signal } : {}),\n };\n\n if (callbacks.onToken || callbacks.onStep) {\n const { events } = await thread.runStreamed(prompt, turnOptions);\n let finalResponse = '';\n let usage = null;\n let threadId = callbacks.threadId || null;\n\n for await (const event of events) {\n if (event.type === 'thread.started') {\n threadId = event.thread_id || threadId;\n } else if (event.type === 'item.completed') {\n const item = event.item;\n if (item.type === 'agent_message') {\n finalResponse = item.text || finalResponse;\n callbacks.onToken?.(item.text || '');\n } else if (item.type === 'todo_list') {\n callbacks.onStep?.({\n title: 'Codex SDK 执行计划',\n message: item.items.map((todo) => `${todo.completed ? '✓' : '•'} ${todo.text}`).join('\\n'),\n });\n } else if (item.type === 'command_execution') {\n callbacks.onStep?.({\n title: 'Codex SDK 执行命令',\n message: item.command,\n });\n } else if (item.type === 'file_change') {\n callbacks.onStep?.({\n title: 'Codex SDK 修改文件',\n message: item.changes.map((change) => `${change.kind}: ${change.path}`).join('\\n'),\n });\n } else if (item.type === 'mcp_tool_call') {\n callbacks.onStep?.({\n title: 'Codex SDK 调用工具',\n message: `${item.server}.${item.tool}`,\n });\n }\n } else if (event.type === 'turn.completed') {\n usage = event.usage;\n } else if (event.type === 'turn.failed') {\n throw new Error(event.error?.message || 'Codex SDK turn failed');\n } else if (event.type === 'error') {\n throw new Error(event.message || 'Codex SDK stream failed');\n }\n }\n\n if (!finalResponse.trim()) {\n const error = new Error('Codex SDK returned an empty response');\n error.statusCode = 502;\n throw error;\n }\n\n return {\n message: finalResponse.trim(),\n usage: {\n promptTokens: Number(usage?.input_tokens || 0),\n completionTokens: Number(usage?.output_tokens || 0),\n totalTokens: Number((usage?.input_tokens || 0) + (usage?.output_tokens || 0)),\n },\n usageAlreadyRecorded: true,\n threadId: thread.id || threadId,\n };\n }\n\n const turn = await thread.run(prompt, turnOptions);\n if (!turn.finalResponse?.trim()) {\n const error = new Error('Codex SDK returned an empty response');\n error.statusCode = 502;\n throw error;\n }\n\n return {\n message: turn.finalResponse.trim(),\n usage: {\n promptTokens: Number(turn.usage?.input_tokens || 0),\n completionTokens: Number(turn.usage?.output_tokens || 0),\n totalTokens: Number((turn.usage?.input_tokens || 0) + (turn.usage?.output_tokens || 0)),\n },\n usageAlreadyRecorded: true,\n threadId: thread.id || callbacks.threadId || null,\n };\n}\n\nconst activeAgentRuns = new Map();\n\nfunction normalizeAgentMode(value) {\n const mode = safeUserText(value || 'dharma_guide');\n return /^[a-z][a-z0-9_:-]{0,48}$/i.test(mode) ? mode : 'dharma_guide';\n}\n\nfunction openClawSessionKey(user, conversationId) {\n const env = process.env.NODE_ENV === 'production' ? 'prod' : 'dev';\n return `fabushi:env:${env}:user:${user.userId}:conversation:${conversationId}`;\n}\n\nfunction createOpenClawRuntime() {\n if (!enableOpenClawAgentChat || !openClawGatewayToken || !openClawGatewayUrl) {\n return {\n enabled: false,\n provider: 'openclaw-gateway',\n reason: !enableOpenClawAgentChat\n ? 'ENABLE_OPENCLAW_AGENT_CHAT is not true'\n : !openClawGatewayToken\n ? 'OPENCLAW_GATEWAY_TOKEN is not configured'\n : 'OPENCLAW_GATEWAY_URL is not configured',\n };\n }\n\n return {\n enabled: true,\n provider: 'openclaw-gateway',\n model: openClawAgentId,\n runsUrl: openClawRunsEndpoint || `${openClawGatewayUrl}/v1/agents/${encodeURIComponent(openClawAgentId)}/runs`,\n };\n}\n\nfunction extractOpenClawText(payload) {\n if (!payload || typeof payload !== 'object') return '';\n const candidates = [\n payload.message,\n payload.content,\n payload.output_text,\n payload.finalResponse,\n payload.final_response,\n payload.result?.message,\n payload.result?.content,\n payload.output?.message,\n payload.output?.content,\n payload.choices?.[0]?.message?.content,\n ];\n for (const candidate of candidates) {\n if (typeof candidate === 'string' && candidate.trim()) return candidate.trim();\n }\n return '';\n}\n\nfunction extractOpenClawUsage(payload) {\n const usage = payload?.usage || payload?.result?.usage || {};\n return {\n promptTokens: Number(usage.input_tokens || usage.prompt_tokens || usage.promptTokens || 0),\n completionTokens: Number(usage.output_tokens || usage.completion_tokens || usage.completionTokens || 0),\n totalTokens: Number(usage.total_tokens || usage.totalTokens || 0),\n };\n}\n\nasync function callOpenClawAgent({ messages, user, conversationId, mode, signal }) {\n const runtime = createOpenClawRuntime();\n if (!runtime.enabled) {\n const error = new Error(runtime.reason || 'OpenClaw Agent chat is not enabled');\n error.statusCode = 503;\n throw error;\n }\n\n const sessionKey = openClawSessionKey(user, conversationId);\n const response = await fetch(runtime.runsUrl, {\n method: 'POST',\n headers: {\n Accept: 'application/json',\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${openClawGatewayToken}`,\n },\n body: JSON.stringify({\n agentId: openClawAgentId,\n sessionKey,\n mode,\n stream: false,\n input: { messages },\n messages,\n metadata: {\n app: 'fabushi',\n userId: user.userId,\n conversationId,\n toolPolicy: 'public-user-read-mostly',\n },\n }),\n signal: signal || AbortSignal.timeout(180_000),\n });\n\n const bodyText = await response.text();\n let payload;\n try {\n payload = bodyText ? JSON.parse(bodyText) : {};\n } catch {\n payload = { message: bodyText };\n }\n\n if (!response.ok) {\n const error = new Error(payload?.error?.message || payload?.message || `OpenClaw request failed: ${response.status}`);\n error.statusCode = response.status;\n throw error;\n }\n\n const message = extractOpenClawText(payload);\n if (!message) {\n const error = new Error('OpenClaw returned an empty response');\n error.statusCode = 502;\n throw error;\n }\n\n return {\n message,\n runId: payload.runId || payload.id || payload.result?.runId || null,\n provider: runtime.provider,\n model: runtime.model,\n usage: extractOpenClawUsage(payload),\n };\n}\n\nasync function runAgentModel({\n messages,\n user,\n conversationId,\n mode,\n callbacks = {},\n signal,\n maxCompletionTokens,\n preferCodex = false,\n requireCodex = false,\n workingDirectory,\n sandboxMode,\n outputSchema,\n threadId,\n}) {\n const codexRuntime = createCodexDeepSeekRuntime(user, {\n workingDirectory,\n sandboxMode,\n maxCompletionTokens,\n });\n if (requireCodex && !codexRuntime.enabled) {\n const error = new Error(codexRuntime.reason || 'Codex SDK chat is not enabled');\n error.statusCode = 503;\n throw error;\n }\n\n let preferredCodexFailed = false;\n if ((preferCodex || requireCodex) && codexRuntime.enabled) {\n try {\n const result = await callCodexSdkDeepSeek(messages, {\n ...callbacks,\n user,\n signal,\n maxCompletionTokens,\n workingDirectory,\n sandboxMode,\n outputSchema,\n threadId,\n });\n return {\n ...result,\n provider: codexRuntime.provider,\n model: deepseekModel,\n };\n } catch (error) {\n if (requireCodex || isDeepSeekQuotaError(error)) throw error;\n logger.warn(\n { error: String(error), mode },\n 'Preferred Codex SDK model failed, continuing through agent fallbacks',\n );\n preferredCodexFailed = true;\n }\n }\n\n const openClawRuntime = createOpenClawRuntime();\n if (openClawRuntime.enabled && !callbacks.onToken) {\n return await callOpenClawAgent({ messages, user, conversationId, mode, signal });\n }\n\n const libreChatRuntime = createLibreChatAgentRuntime();\n let provider = libreChatRuntime.enabled\n ? libreChatRuntime.provider\n : codexRuntime.enabled && !preferredCodexFailed\n ? codexRuntime.provider\n : 'deepseek';\n let model = libreChatRuntime.enabled ? libreChatRuntime.model : deepseekModel;\n\n try {\n if (libreChatRuntime.enabled) {\n const result = callbacks.onToken\n ? await callLibreChatAgentStream(messages, { ...callbacks, signal })\n : await callLibreChatAgent(messages);\n return { ...result, provider, model };\n }\n if (codexRuntime.enabled && !preferredCodexFailed) {\n const result = await callCodexSdkDeepSeek(messages, {\n ...callbacks,\n user,\n signal,\n maxCompletionTokens,\n workingDirectory,\n sandboxMode,\n outputSchema,\n threadId,\n });\n return { ...result, provider, model };\n }\n const result = callbacks.onToken\n ? await callDeepSeekStream(messages, { ...callbacks, signal, maxCompletionTokens })\n : await callDeepSeek(messages, { maxCompletionTokens });\n return { ...result, provider, model };\n } catch (error) {\n if (!codexRuntime.enabled || preferredCodexFailed || isDeepSeekQuotaError(error)) throw error;\n logger.warn({ error: String(error) }, 'Agent model failed, falling back to DeepSeek direct');\n const result = callbacks.onToken\n ? await callDeepSeekStream(messages, { ...callbacks, signal, maxCompletionTokens })\n : await callDeepSeek(messages, { maxCompletionTokens });\n return { ...result, provider: 'deepseek', model: deepseekModel };\n }\n}\n\nfunction updateAgentRunStatus(run, status, fields = {}) {\n statements.updateAgentRunStatus.run({\n id: run.id,\n userId: run.user_id || run.userId,\n status,\n openClawRunId: fields.openClawRunId ?? null,\n inputTokens: fields.inputTokens ?? null,\n outputTokens: fields.outputTokens ?? null,\n toolCallCount: fields.toolCallCount ?? null,\n completedAt: status === 'completed' || status === 'cancelled' ? nowIso() : null,\n failedAt: status === 'failed' ? nowIso() : null,\n errorCode: fields.errorCode ?? null,\n errorMessage: fields.errorMessage ?? null,\n });\n}\n\nfunction buildAgentMessages(conversationId) {\n const historyRows = statements.listMessages.all(conversationId).slice(-14);\n return [{ role: 'system', content: systemPrompt }, ...normalizeMessages(historyRows)];\n}\n\nfunction agentUsagePayload(usage, user, budget) {\n const promptTokens = usage.promptTokens || usage.inputTokens || 0;\n const completionTokens = usage.completionTokens || usage.outputTokens || 0;\n const totalTokens = usage.totalTokens || promptTokens + completionTokens;\n const latestUsage = usageFor(user.userId);\n return {\n inputTokens: promptTokens,\n outputTokens: completionTokens,\n totalTokens,\n monthlyLimit: budget.limit,\n remainingTokens: Math.max(0, budget.limit - latestUsage.used),\n };\n}\n\nfunction openAiContentToText(content) {\n if (typeof content === 'string') return content;\n if (Array.isArray(content)) {\n return content\n .map((part) => {\n if (typeof part === 'string') return part;\n if (!part || typeof part !== 'object') return '';\n return readText(part.text || part.content || part.value);\n })\n .filter(Boolean)\n .join('\\n');\n }\n return readText(content);\n}\n\nfunction normalizeOpenAiMessages(value) {\n if (!Array.isArray(value)) return [];\n const allowedRoles = new Set(['system', 'user', 'assistant', 'tool']);\n return value\n .map((item) => {\n if (!item || typeof item !== 'object') return null;\n const role = allowedRoles.has(item.role) ? item.role : 'user';\n const content = openAiContentToText(item.content);\n if (!content) return null;\n return { role, content };\n })\n .filter(Boolean);\n}\n\nfunction truncateForDeepSeekMessage(text, maxChars = 12_000) {\n const value = readText(text);\n if (value.length <= maxChars) return value;\n return `${value.slice(0, maxChars)}\\n\\n[内容已截断]`;\n}\n\nfunction truncateTailForDeepSeekMessage(text, maxChars = 2400) {\n const value = readText(text);\n if (value.length <= maxChars) return value;\n return `[前文已省略]\\n\\n${value.slice(-maxChars)}`;\n}\n\nfunction stripOpenClawPromptNoise(text) {\n const value = readText(text);\n if (!value) return '';\n\n const finalPromptMatch = value.match(/finalPromptText[\"'\\s::]+([\\s\\S]{1,4000})$/i);\n if (finalPromptMatch?.[1]) {\n return readText(finalPromptMatch[1].replace(/^[\"'`]+|[\"'`,}\\]]+$/g, ''));\n }\n\n const markers = [\n 'final prompt:',\n 'final user prompt:',\n 'user prompt:',\n '用户问题:',\n '用户消息:',\n '用户输入:',\n ];\n const lower = value.toLowerCase();\n for (const marker of markers) {\n const index = lower.lastIndexOf(marker.toLowerCase());\n if (index >= 0) {\n const tail = value.slice(index + marker.length).trim();\n if (tail) return tail;\n }\n }\n\n return value;\n}\n\nfunction compactOpenClawMessagesForDeepSeek(messages) {\n const conversationalMessages = messages\n .filter((message) => message.role === 'user' || message.role === 'assistant')\n .map((message) => ({\n role: message.role,\n content: truncateTailForDeepSeekMessage(stripOpenClawPromptNoise(message.content)),\n }))\n .filter((message) => message.content);\n\n const recent = conversationalMessages.slice(-6);\n while (recent.length > 0 && recent[0].role !== 'user') {\n recent.shift();\n }\n\n if (recent.length === 0) {\n const fallback = messages\n .slice()\n .reverse()\n .map((message) => truncateTailForDeepSeekMessage(stripOpenClawPromptNoise(message.content), 2400))\n .find(Boolean);\n if (!fallback) return messages;\n recent.push({ role: 'user', content: fallback });\n }\n\n return [\n {\n role: 'system',\n content: '你是大乘 App 的桌面 AI 助手。请直接回答用户问题,默认使用简洁、准确的中文。',\n },\n ...recent,\n ];\n}\n\nfunction isDeepSeekBlockedError(error) {\n return (\n Number(error?.statusCode) === 403 &&\n /blocked|被拦截|forbidden/i.test(String(error?.message || error || ''))\n );\n}\n\nfunction shouldRetryWithCompactedOpenClawMessages(error, messages, compactedMessages) {\n if (!isDeepSeekBlockedError(error)) return false;\n return JSON.stringify(messages) !== JSON.stringify(compactedMessages);\n}\n\nfunction openClawBlockedFallbackText(messages) {\n const lastUser = messages\n .slice()\n .reverse()\n .find((message) => message.role === 'user')?.content || '';\n const localActionIntent =\n /(file|folder|directory|project|workspace|path|文件|文件夹|目录|项目|工程|路径|本地)/i.test(lastUser) &&\n /(create|make|mkdir|write|edit|modify|rename|delete|move|copy|save|download|read|list|生成|创建|新建|写入|编辑|修改|重命名|删除|移动|复制|保存|下载|读取|列出)/i.test(lastUser);\n\n if (localActionIntent) {\n return [\n '本机工具规划被模型服务拦截了,所以这次没有实际读取、创建、修改或下载本机文件。',\n '如果只是新建文件夹,请先在底部选择项目后重新发送“新建文件夹”;桌面端会直接在已选项目里创建。',\n '如果是更复杂的本地文件操作,请在“连应用”里重启本机 AI 后重试,或把要操作的文件路径和目标说得更具体。',\n ].join('\\n');\n }\n\n return '模型服务拦截了这次回复,未能生成结果。请换一种更具体、更短的表述后重试。';\n}\nfunction usageWithFallback(usage, messages, text) {\n const promptTokens = Number(usage?.promptTokens || usage?.inputTokens || 0);\n const completionTokens = Number(usage?.completionTokens || usage?.outputTokens || 0);\n const totalTokens = Number(usage?.totalTokens || 0);\n if (totalTokens > 0) {\n return {\n promptTokens,\n completionTokens,\n totalTokens,\n };\n }\n const estimatedPrompt = promptTokens || estimateTokens(JSON.stringify(messages));\n const estimatedCompletion = completionTokens || estimateTokens(text);\n return {\n promptTokens: estimatedPrompt,\n completionTokens: estimatedCompletion,\n totalTokens: estimatedPrompt + estimatedCompletion,\n };\n}\n\nfunction openAiUsagePayload(usage, user, budget) {\n const promptTokens = Number(usage.promptTokens || 0);\n const completionTokens = Number(usage.completionTokens || 0);\n const totalTokens = Number(usage.totalTokens || promptTokens + completionTokens);\n const latestUsage = usageFor(user.userId);\n const remainingTokens = Math.max(0, budget.limit - latestUsage.used);\n return {\n prompt_tokens: promptTokens,\n completion_tokens: completionTokens,\n total_tokens: totalTokens,\n promptTokens,\n completionTokens,\n totalTokens,\n monthlyLimit: budget.limit,\n remainingTokens,\n };\n}\n\nfunction writeOpenAiSse(res, payload) {\n res.write(`data: ${JSON.stringify(payload)}\\n\\n`);\n}\n\napp.post(\n '/api/openclaw/deepseek/v1/chat/completions',\n asyncHandler(async (req, res) => {\n const user = await resolveUser(req, req.body || {});\n const incomingMessages = normalizeOpenAiMessages(req.body?.messages);\n if (incomingMessages.length === 0) {\n res.status(400).json({ error: { message: 'messages is required' } });\n return;\n }\n\n const messages = compactOpenClawMessagesForDeepSeek(incomingMessages);\n const promptEstimate = estimateTokens(JSON.stringify(messages));\n const estimated = promptEstimate + 600;\n const budget = enforceTokenBudget(user, estimated);\n const requestCompletionLimit = isBotFatherGenerationMessages(incomingMessages)\n ? botFatherMaxCompletionTokens\n : deepseekMaxCompletionTokens;\n const maxCompletionTokens = Math.min(\n completionBudgetFor(budget, promptEstimate, requestCompletionLimit),\n normalizeMaxCompletionTokens(req.body?.max_tokens, requestCompletionLimit),\n );\n const model = normalizeDeepSeekModelName(req.body?.model);\n const wantsStream =\n req.body?.stream === true ||\n String(req.get('accept') || '').includes('text/event-stream');\n const id = `chatcmpl_${crypto.randomUUID().replaceAll('-', '')}`;\n const created = Math.floor(Date.now() / 1000);\n const compactedMessages = messages;\n\n if (!wantsStream) {\n let result;\n let usageMessages = messages;\n try {\n result = await callDeepSeek(messages, {\n model,\n maxCompletionTokens,\n maximumCompletionTokens: requestCompletionLimit,\n });\n } catch (error) {\n if (!shouldRetryWithCompactedOpenClawMessages(error, messages, compactedMessages)) {\n throw error;\n }\n logger.warn(\n {\n statusCode: error?.statusCode,\n messageCount: messages.length,\n compactedMessageCount: compactedMessages.length,\n },\n 'OpenClaw DeepSeek proxy retrying with compacted messages',\n );\n usageMessages = compactedMessages;\n result = await callDeepSeek(compactedMessages, {\n model,\n maxCompletionTokens,\n maximumCompletionTokens: requestCompletionLimit,\n });\n }\n const usage = usageWithFallback(result.usage, usageMessages, result.message);\n recordUsage(user.userId, usage.totalTokens);\n res.json({\n id,\n object: 'chat.completion',\n created,\n model: result.model || model,\n choices: [\n {\n index: 0,\n message: { role: 'assistant', content: result.message },\n finish_reason: 'stop',\n },\n ],\n usage: openAiUsagePayload(usage, user, budget),\n });\n return;\n }\n\n const abortController = new AbortController();\n req.on('close', () => abortController.abort());\n\n res.status(200);\n res.set({\n 'Content-Type': 'text/event-stream; charset=utf-8',\n 'Cache-Control': 'no-cache, no-transform',\n Connection: 'keep-alive',\n 'X-Accel-Buffering': 'no',\n });\n res.flushHeaders?.();\n\n const baseChunk = {\n id,\n object: 'chat.completion.chunk',\n created,\n model,\n system_fingerprint: 'fabushi-openclaw-deepseek-proxy',\n };\n writeOpenAiSse(res, {\n ...baseChunk,\n choices: [{ index: 0, delta: { role: 'assistant' }, finish_reason: null }],\n });\n\n let text = '';\n let usageMessages = messages;\n try {\n let result;\n try {\n result = await callDeepSeekStream(messages, {\n model,\n maxCompletionTokens,\n maximumCompletionTokens: requestCompletionLimit,\n signal: abortController.signal,\n onToken: (delta) => {\n text += delta;\n writeOpenAiSse(res, {\n ...baseChunk,\n choices: [{ index: 0, delta: { content: delta }, finish_reason: null }],\n });\n },\n });\n } catch (error) {\n if (!shouldRetryWithCompactedOpenClawMessages(error, messages, compactedMessages)) {\n throw error;\n }\n logger.warn(\n {\n statusCode: error?.statusCode,\n messageCount: messages.length,\n compactedMessageCount: compactedMessages.length,\n },\n 'OpenClaw DeepSeek proxy stream retrying with compacted messages',\n );\n text = '';\n usageMessages = compactedMessages;\n result = await callDeepSeekStream(compactedMessages, {\n model,\n maxCompletionTokens,\n maximumCompletionTokens: requestCompletionLimit,\n signal: abortController.signal,\n onToken: (delta) => {\n text += delta;\n writeOpenAiSse(res, {\n ...baseChunk,\n choices: [{ index: 0, delta: { content: delta }, finish_reason: null }],\n });\n },\n });\n }\n text = result.message || text;\n const usage = usageWithFallback(result.usage, usageMessages, text);\n recordUsage(user.userId, usage.totalTokens);\n writeOpenAiSse(res, {\n ...baseChunk,\n model: result.model || model,\n choices: [{ index: 0, delta: {}, finish_reason: 'stop' }],\n });\n writeOpenAiSse(res, {\n ...baseChunk,\n model: result.model || model,\n choices: [],\n usage: openAiUsagePayload(usage, user, budget),\n });\n res.write('data: [DONE]\\n\\n');\n } catch (error) {\n if (isDeepSeekBlockedError(error)) {\n const fallbackMessage = openClawBlockedFallbackText(usageMessages);\n const usage = usageWithFallback(null, usageMessages, fallbackMessage);\n recordUsage(user.userId, usage.totalTokens);\n logger.warn(\n { statusCode: error?.statusCode, code: error?.code },\n 'OpenClaw DeepSeek proxy stream blocked; returning local fallback message',\n );\n writeOpenAiSse(res, {\n ...baseChunk,\n choices: [{ index: 0, delta: { content: fallbackMessage }, finish_reason: null }],\n });\n writeOpenAiSse(res, {\n ...baseChunk,\n choices: [{ index: 0, delta: {}, finish_reason: 'stop' }],\n });\n writeOpenAiSse(res, {\n ...baseChunk,\n choices: [],\n usage: openAiUsagePayload(usage, user, budget),\n });\n res.write('data: [DONE]\\n\\n');\n return;\n }\n logger.error(\n { error: String(error), code: error?.code, statusCode: error?.statusCode },\n 'OpenClaw DeepSeek proxy stream failed',\n );\n writeOpenAiSse(res, {\n ...baseChunk,\n choices: [],\n error: {\n message: publicAiErrorMessage(error),\n code: error?.code || 'OPENCLAW_DEEPSEEK_PROXY_ERROR',\n },\n });\n res.write('data: [DONE]\\n\\n');\n } finally {\n res.end();\n }\n }),\n);\n\napp.post(\n '/codex-deepseek/v1/responses',\n asyncHandler(async (req, res) => {\n if (!deepseekApiKey) {\n res.status(401).json({\n error: { message: 'Unauthorized Codex DeepSeek adapter request' },\n });\n return;\n }\n\n const prompt = codexResponsesPrompt(req.body);\n if (!prompt) {\n res.status(400).json({ error: { message: 'Responses input is required' } });\n return;\n }\n\n const {\n user,\n recordUsage: shouldRecordUsage,\n maxCompletionTokens: adapterMaxCompletionTokens,\n } = await resolveCodexResponsesBilling(req);\n const messages = [{ role: 'user', content: prompt }];\n const promptEstimate = estimateTokens(JSON.stringify(messages));\n const budget = enforceTokenBudget(user, promptEstimate + 600);\n const responseCompletionLimit = adapterMaxCompletionTokens || deepseekMaxCompletionTokens;\n const maxCompletionTokens = Math.min(\n completionBudgetFor(budget, promptEstimate, responseCompletionLimit),\n normalizeMaxCompletionTokens(\n req.body?.max_output_tokens || req.body?.max_tokens,\n responseCompletionLimit,\n ),\n );\n const model = normalizeDeepSeekModelName(req.body?.model);\n const responseId = `resp_${crypto.randomUUID().replaceAll('-', '')}`;\n const itemId = `msg_${crypto.randomUUID().replaceAll('-', '')}`;\n const wantsStream =\n req.body?.stream !== false ||\n String(req.get('accept') || '').includes('text/event-stream');\n\n if (!wantsStream) {\n const result = await callDeepSeek(messages, {\n model,\n maxCompletionTokens,\n maximumCompletionTokens: responseCompletionLimit,\n });\n const usage = usageWithFallback(result.usage, messages, result.message);\n if (shouldRecordUsage) recordUsage(user.userId, usage.totalTokens);\n res.json(\n responsesPayload({\n responseId,\n itemId,\n status: 'completed',\n model: result.model || model,\n text: result.message,\n usage,\n }),\n );\n return;\n }\n\n res.status(200);\n res.set({\n 'Content-Type': 'text/event-stream; charset=utf-8',\n 'Cache-Control': 'no-cache, no-transform',\n Connection: 'keep-alive',\n 'X-Accel-Buffering': 'no',\n });\n res.flushHeaders?.();\n\n writeResponsesEvent(res, 'response.created', {\n type: 'response.created',\n response: responsesPayload({\n responseId,\n itemId,\n status: 'in_progress',\n model,\n }),\n });\n writeResponsesEvent(res, 'response.output_item.added', {\n type: 'response.output_item.added',\n output_index: 0,\n item: {\n id: itemId,\n type: 'message',\n role: 'assistant',\n status: 'in_progress',\n content: [],\n },\n });\n writeResponsesEvent(res, 'response.content_part.added', {\n type: 'response.content_part.added',\n item_id: itemId,\n output_index: 0,\n content_index: 0,\n part: { type: 'output_text', text: '' },\n });\n\n let text = '';\n try {\n const result = await callDeepSeekStream(messages, {\n model,\n maxCompletionTokens,\n maximumCompletionTokens: responseCompletionLimit,\n onToken: (delta) => {\n text += delta;\n writeResponsesEvent(res, 'response.output_text.delta', {\n type: 'response.output_text.delta',\n item_id: itemId,\n output_index: 0,\n content_index: 0,\n delta,\n });\n },\n });\n text = result.message || text;\n const usage = usageWithFallback(result.usage, messages, text);\n if (shouldRecordUsage) recordUsage(user.userId, usage.totalTokens);\n writeResponsesEvent(res, 'response.output_text.done', {\n type: 'response.output_text.done',\n item_id: itemId,\n output_index: 0,\n content_index: 0,\n text,\n });\n writeResponsesEvent(res, 'response.content_part.done', {\n type: 'response.content_part.done',\n item_id: itemId,\n output_index: 0,\n content_index: 0,\n part: { type: 'output_text', text },\n });\n writeResponsesEvent(res, 'response.output_item.done', {\n type: 'response.output_item.done',\n output_index: 0,\n item: {\n id: itemId,\n type: 'message',\n role: 'assistant',\n status: 'completed',\n content: [{ type: 'output_text', text }],\n },\n });\n writeResponsesEvent(res, 'response.completed', {\n type: 'response.completed',\n response: responsesPayload({\n responseId,\n itemId,\n status: 'completed',\n model: result.model || model,\n text,\n usage,\n }),\n });\n res.end();\n } catch (error) {\n writeResponsesEvent(res, 'response.failed', {\n type: 'response.failed',\n response: responsesPayload({\n responseId,\n itemId,\n status: 'failed',\n model,\n text,\n error,\n }),\n });\n res.end();\n }\n }),\n);\n\napp.use(\n '/api/openclaw/runtime/files',\n express.static(openClawRuntimeUpdatesDir, {\n fallthrough: false,\n immutable: true,\n maxAge: '7d',\n setHeaders(res) {\n res.setHeader('X-Content-Type-Options', 'nosniff');\n },\n }),\n);\n\napp.get(\n '/api/openclaw/runtime/manifest',\n asyncHandler(async (req, res) => {\n const platform = readText(req.query.platform);\n const currentVersion = readText(req.query.currentVersion);\n const appVersion = readText(req.query.appVersion);\n\n let manifest;\n try {\n manifest = await readJsonFile(openClawRuntimeManifestPath);\n } catch (error) {\n logger.info(\n { manifestPath: openClawRuntimeManifestPath, error: String(error) },\n 'OpenClaw runtime manifest unavailable',\n );\n return jsonResponse(res, 200, {\n success: true,\n schema: 1,\n updateAvailable: false,\n reason: 'manifest-unavailable',\n platform,\n currentVersion,\n appVersion,\n });\n }\n\n const latest = manifest.latest && typeof manifest.latest === 'object'\n ? manifest.latest\n : manifest;\n const platforms = latest.platforms && typeof latest.platforms === 'object'\n ? latest.platforms\n : {};\n const platformRelease = platform ? platforms[platform] : null;\n const version = readText(latest.version || manifest.version);\n\n if (!platform || !platformRelease || typeof platformRelease !== 'object') {\n return jsonResponse(res, 200, {\n success: true,\n schema: Number(manifest.schema || 1),\n channel: readText(manifest.channel || latest.channel || 'stable'),\n updateAvailable: false,\n reason: platform ? 'platform-unavailable' : 'platform-required',\n platform,\n currentVersion,\n appVersion,\n latestVersion: version,\n });\n }\n\n if (platform.startsWith('macos-') && !openClawRuntimeEnableMacosUpdates) {\n return jsonResponse(res, 200, {\n success: true,\n schema: Number(manifest.schema || 1),\n channel: readText(manifest.channel || latest.channel || 'stable'),\n updateAvailable: false,\n reason: 'macos-runtime-updates-disabled',\n platform,\n currentVersion,\n appVersion,\n latestVersion: version,\n });\n }\n\n const downloadUrl = absolutizeOpenClawRuntimeUrl(\n req,\n platformRelease.archiveUrl || platformRelease.url || platformRelease.fileName,\n );\n const release = {\n version,\n channel: readText(manifest.channel || latest.channel || 'stable'),\n minAppVersion: readText(latest.minAppVersion || platformRelease.minAppVersion),\n notes: readText(latest.notes || platformRelease.notes),\n nodeExecutable: readText(\n platformRelease.nodeExecutable || latest.nodeExecutable || 'node/bin/node',\n ),\n cliEntrypoint: readText(\n platformRelease.cliEntrypoint || latest.cliEntrypoint || 'openclaw/openclaw.mjs',\n ),\n defaultPort: Number(latest.defaultPort || manifest.defaultPort || 18789),\n defaultModel: readText(latest.defaultModel || manifest.defaultModel || 'openclaw/default'),\n defaultModelOverride: readText(latest.defaultModelOverride || manifest.defaultModelOverride),\n gatewayArgs: Array.isArray(latest.gatewayArgs)\n ? latest.gatewayArgs.map((item) => String(item))\n : ['gateway', '--port', '{port}', '--force'],\n archiveUrl: downloadUrl,\n sha256: readText(platformRelease.sha256),\n size: Number(platformRelease.size || 0),\n };\n\n jsonResponse(res, 200, {\n success: true,\n schema: Number(manifest.schema || 1),\n updateAvailable: Boolean(version && downloadUrl && version !== currentVersion),\n platform,\n currentVersion,\n appVersion,\n latest: release,\n generatedAt: readText(manifest.generatedAt || latest.generatedAt),\n });\n }),\n);\n\napp.get('/health', (_req, res) => {\n const codexRuntime = createCodexDeepSeekRuntime();\n const libreChatRuntime = createLibreChatAgentRuntime();\n const openClawRuntime = createOpenClawRuntime();\n jsonResponse(res, 200, {\n status: 'ok',\n service: 'dacheng-ai-backend',\n provider: libreChatRuntime.enabled ? libreChatRuntime.provider : 'deepseek',\n model: libreChatRuntime.enabled ? libreChatRuntime.model : deepseekModel,\n deepseekModel,\n deepseekAvailableModels,\n libreChatAgentChatEnabled: libreChatRuntime.enabled,\n libreChatAgentProvider: libreChatRuntime.provider,\n libreChatAgentReason: libreChatRuntime.enabled ? undefined : libreChatRuntime.reason,\n codexSdkAvailable: true,\n codexSdkChatEnabled: codexRuntime.enabled,\n codexSdkProvider: codexRuntime.provider,\n openClawAgentChatEnabled: openClawRuntime.enabled,\n openClawAgentProvider: openClawRuntime.provider,\n openClawAgentReason: openClawRuntime.enabled ? undefined : openClawRuntime.reason,\n requireMemberForAi,\n memberMonthlyLimit,\n freeMonthlyLimit,\n deepseekMaxCompletionTokens,\n botFatherMaxCompletionTokens,\n openClawRuntimeUpdatesDir,\n openClawRuntimeManifestConfigured: fs.existsSync(openClawRuntimeManifestPath),\n cbetaApiRoot,\n timestamp: nowIso(),\n });\n});\n\napp.get(\n '/api/miniapps/registry',\n asyncHandler(async (req, res) => {\n const user = await resolveUser(req, { username: req.query.username });\n const official = officialMiniAppRegistry(req);\n const store = await readMiniAppStore();\n const visibleSandbox = store.apps.filter((appRecord) => {\n if (appRecord.reviewStatus === 'approved' && appRecord.source === 'marketplace') {\n return true;\n }\n return appRecord.ownerId === user.userId;\n });\n const bots = [\n ...official.bots,\n ...visibleSandbox.map((record) => miniAppRecordToBot(record)),\n ];\n const miniApps = [\n ...official.miniApps,\n ...visibleSandbox.map((record) => miniAppRecordToManifest(req, record)),\n ];\n const registry = {\n schemaVersion: 1,\n hostApiVersion: miniAppHostApiVersion,\n bots,\n miniApps,\n updatedAt: nowIso(),\n };\n registry.signature = sha256(JSON.stringify({ bots, miniApps }));\n jsonResponse(res, 200, { success: true, registry });\n }),\n);\n\napp.get(\n '/api/miniapps/dev/:miniAppId/index.html',\n asyncHandler(async (req, res) => {\n const miniAppId = safeMiniAppId(req.params.miniAppId);\n const store = await readMiniAppStore();\n const record = store.apps.find((appRecord) => appRecord.miniAppId === miniAppId);\n if (!record) {\n res.status(404).type('text/html').send('<h1>Mini app not found</h1>');\n return;\n }\n res.setHeader('Content-Type', 'text/html; charset=utf-8');\n res.setHeader('Cache-Control', 'no-store');\n res.send(record.sourceHtml || generatedMiniAppHtml(record));\n }),\n);\n\napp.post(\n '/api/miniapps/dev/create',\n asyncHandler(async (req, res) => {\n const user = await resolveUser(req, req.body || {});\n const title = safeUserText(req.body?.title, '个人小程序').slice(0, 80);\n const subtitle = safeUserText(req.body?.subtitle, '个人沙箱小程序').slice(0, 120);\n const permissions = sanitizeMiniAppPermissions(req.body?.permissions);\n const prompt = safeUserText(req.body?.prompt || title);\n const idSeed = crypto.randomUUID().replaceAll('-', '').slice(0, 12);\n const miniAppId = `sandbox.${idSeed}`;\n const botId = `bot.${miniAppId}`;\n const sourceHtml = generatedMiniAppHtml({ title, prompt });\n const scan = miniAppScan(permissions, sourceHtml);\n const createdAt = nowIso();\n const record = {\n ownerId: user.userId,\n miniAppId,\n botId,\n title,\n subtitle,\n initials: title.slice(0, 1) || '小',\n icon: 'code',\n avatarColor: '#3D8BFF',\n greeting: '你好,我是这个个人沙箱小程序的机器人。',\n inputHint: '输入消息,或打开小程序面板',\n version: '0.0.1',\n permissions,\n surfaces: ['chatPanel'],\n theme: 'telegramDark',\n source: 'sandbox',\n reviewStatus: 'sandbox',\n sourceHtml,\n scan,\n createdAt,\n updatedAt: createdAt,\n };\n const store = await readMiniAppStore();\n store.apps.push(record);\n await writeMiniAppStore(store);\n jsonResponse(res, 200, {\n success: true,\n miniApp: miniAppRecordToManifest(req, record),\n bot: miniAppRecordToBot(record),\n scan,\n });\n }),\n);\n\napp.post(\n '/api/miniapps/dev/:miniAppId/version',\n asyncHandler(async (req, res) => {\n const user = await resolveUser(req, req.body || {});\n const miniAppId = safeMiniAppId(req.params.miniAppId);\n const store = await readMiniAppStore();\n const index = store.apps.findIndex((appRecord) => appRecord.miniAppId === miniAppId);\n if (index < 0) {\n return jsonResponse(res, 404, { success: false, message: 'mini app not found' });\n }\n const record = store.apps[index];\n if (record.ownerId !== user.userId) {\n return jsonResponse(res, 403, { success: false, message: 'not your mini app' });\n }\n const title = safeUserText(req.body?.title, record.title).slice(0, 80);\n const subtitle = safeUserText(req.body?.subtitle, record.subtitle).slice(0, 120);\n const sourceHtml =\n safeUserText(req.body?.sourceHtml) ||\n generatedMiniAppHtml({ title, prompt: safeUserText(req.body?.prompt || title) });\n const permissions = sanitizeMiniAppPermissions(req.body?.permissions || record.permissions);\n const scan = miniAppScan(permissions, sourceHtml);\n store.apps[index] = {\n ...record,\n title,\n subtitle,\n sourceHtml,\n permissions,\n scan,\n reviewStatus: 'sandbox',\n version: safeUserText(req.body?.version, record.version || '0.0.1'),\n updatedAt: nowIso(),\n };\n await writeMiniAppStore(store);\n jsonResponse(res, 200, {\n success: true,\n miniApp: miniAppRecordToManifest(req, store.apps[index]),\n bot: miniAppRecordToBot(store.apps[index]),\n scan,\n });\n }),\n);\n\napp.post(\n '/api/miniapps/:miniAppId/submit-review',\n asyncHandler(async (req, res) => {\n const user = await resolveUser(req, req.body || {});\n const miniAppId = safeMiniAppId(req.params.miniAppId);\n const store = await readMiniAppStore();\n const index = store.apps.findIndex((appRecord) => appRecord.miniAppId === miniAppId);\n if (index < 0) {\n return jsonResponse(res, 404, { success: false, message: 'mini app not found' });\n }\n const record = store.apps[index];\n if (record.ownerId !== user.userId) {\n return jsonResponse(res, 403, { success: false, message: 'not your mini app' });\n }\n const scan = miniAppScan(record.permissions || [], record.sourceHtml || '');\n if (!scan.passed) {\n return jsonResponse(res, 400, {\n success: false,\n message: 'security scan failed',\n scan,\n });\n }\n store.apps[index] = {\n ...record,\n scan,\n reviewStatus: 'pending_review',\n updatedAt: nowIso(),\n };\n await writeMiniAppStore(store);\n jsonResponse(res, 200, {\n success: true,\n miniApp: miniAppRecordToManifest(req, store.apps[index]),\n scan,\n });\n }),\n);\n\napp.post(\n '/api/botfather/generate-miniapp',\n asyncHandler(async (req, res) => {\n const user = await resolveUser(req, req.body || {});\n const prompt = safeUserText(req.body?.prompt);\n if (!prompt) {\n return jsonResponse(res, 400, { success: false, message: 'prompt is required' });\n }\n const title = titleFromPromptForMiniApp(prompt);\n const subtitle = '机器人之父生成的个人沙箱小程序';\n const requestedPermissions = Array.isArray(req.body?.permissions) ? req.body.permissions : [];\n const generated = await generateBotFatherMiniAppHtml({ title, prompt, user, miniAppId: crypto.randomUUID() });\n let sourceHtml = generated.sourceHtml;\n let generation = generated.generation;\n const permissions = sanitizeMiniAppPermissions([\n ...requestedPermissions,\n ...inferMiniAppPermissionsFromText(prompt, sourceHtml),\n ]);\n let scan = miniAppScan(permissions, sourceHtml);\n if (!scan.passed) {\n const error = new Error('Codex generated mini app failed security scan');\n error.statusCode = 400;\n error.scan = scan;\n throw error;\n }\n const idSeed = crypto.randomUUID().replaceAll('-', '').slice(0, 12);\n const miniAppId = `sandbox.${idSeed}`;\n const createdAt = nowIso();\n const record = {\n ownerId: user.userId,\n miniAppId,\n botId: `bot.${miniAppId}`,\n title,\n subtitle,\n initials: title.slice(0, 1) || '小',\n icon: 'code',\n avatarColor: '#3D8BFF',\n greeting: `我是「${title}」的机器人。`,\n inputHint: '继续描述修改需求,或打开小程序面板',\n version: '0.0.1',\n permissions,\n surfaces: ['chatPanel'],\n theme: 'telegramDark',\n source: 'sandbox',\n reviewStatus: 'sandbox',\n sourceHtml,\n scan,\n generation,\n prompt,\n createdAt,\n updatedAt: createdAt,\n };\n const store = await readMiniAppStore();\n store.apps.push(record);\n await writeMiniAppStore(store);\n jsonResponse(res, 200, {\n success: true,\n // Bot Father previews are rendered immediately by the native clients.\n // Keep the persisted entryUrl for subsequent launches, and return the\n // scanned source in the create response so the first preview does not\n // depend on a second network round trip.\n miniApp: {\n ...miniAppRecordToManifest(req, record),\n sourceHtml,\n },\n bot: miniAppRecordToBot(record),\n scan,\n generation,\n actions: ['openMiniApp', 'continueEditing', 'submitReview'],\n });\n }),\n);\n\napp.get('/api/ai/models', (_req, res) => {\n jsonResponse(res, 200, {\n success: true,\n provider: 'deepseek',\n defaultModel: deepseekModel,\n models: deepseekAvailableModels.map((model) => ({\n id: model,\n label: model === 'deepseek-reasoner' ? 'DeepSeek Reasoner' : 'DeepSeek Chat',\n })),\n });\n});\napp.get(\n '/api/ai/quota',\n asyncHandler(async (req, res) => {\n const user = await resolveUser(req, { username: req.query.username });\n const budget = enforceTokenBudget(user, 0);\n const usage = usageFor(user.userId);\n jsonResponse(res, 200, {\n success: true,\n isMember: user.isMember,\n monthlyLimit: budget.limit,\n usedTokens: usage.used,\n remainingTokens: Math.max(0, budget.limit - usage.used),\n month: usage.month,\n });\n }),\n);\n\napp.post(\n '/api/agent/chat',\n asyncHandler(async (req, res) => {\n const message = safeUserText(req.body?.message);\n if (!message) {\n return jsonResponse(res, 400, { success: false, message: 'message is required' });\n }\n\n const user = await resolveUser(req, req.body || {});\n const promptEstimate = estimateTokens(message);\n const estimated = promptEstimate + 600;\n const budget = enforceTokenBudget(user, estimated);\n const maxCompletionTokens = completionBudgetFor(budget, promptEstimate);\n const createdAt = nowIso();\n const conversationId = safeUserText(req.body?.conversationId) || crypto.randomUUID();\n const mode = normalizeAgentMode(req.body?.mode);\n const stream = req.body?.stream === true;\n let conversation = statements.getConversation.get(conversationId, user.userId);\n const isNewConversation = !conversation;\n\n if (isNewConversation) {\n conversation = { id: conversationId, user_id: user.userId, title: titleFromMessage(message) };\n statements.insertConversation.run({\n id: conversationId,\n userId: user.userId,\n title: conversation.title,\n provider: 'agent',\n model: openClawAgentId,\n createdAt,\n updatedAt: createdAt,\n });\n }\n\n const messageId = safeUserText(req.body?.messageId) || crypto.randomUUID();\n statements.insertMessage.run({\n id: messageId,\n conversationId,\n role: 'user',\n content: message,\n promptTokens: 0,\n completionTokens: 0,\n totalTokens: 0,\n createdAt,\n });\n\n const runId = `run_${crypto.randomUUID().replaceAll('-', '')}`;\n const sessionKey = openClawSessionKey(user, conversationId);\n const openClawRuntime = createOpenClawRuntime();\n statements.insertAgentRun.run({\n id: runId,\n userId: user.userId,\n conversationId,\n messageId,\n openClawRunId: null,\n openClawSessionKey: sessionKey,\n status: stream ? 'queued' : 'running',\n mode,\n provider: openClawRuntime.enabled ? openClawRuntime.provider : 'dacheng-ai-fallback',\n model: openClawRuntime.enabled ? openClawAgentId : deepseekModel,\n startedAt: createdAt,\n });\n\n if (stream) {\n return jsonResponse(res, 200, {\n success: true,\n runId,\n conversationId,\n streamUrl: `/api/agent/runs/${runId}/events`,\n });\n }\n\n const controller = new AbortController();\n activeAgentRuns.set(runId, controller);\n try {\n const skillResult = await runResourceFinderSkill({ message, user });\n const skillContext = resourceContextMessage(skillResult);\n const historyRows = statements.listMessages.all(conversationId).slice(-14);\n const modelMessages = [\n { role: 'system', content: systemPrompt },\n ...(skillContext ? [{ role: 'system', content: skillContext }] : []),\n ...normalizeMessages(historyRows),\n ];\n\n const aiResult = await runAgentModel({\n messages: modelMessages,\n user,\n conversationId,\n mode,\n signal: controller.signal,\n maxCompletionTokens,\n });\n const usage = {\n promptTokens: aiResult.usage?.promptTokens || estimateTokens(JSON.stringify(modelMessages)),\n completionTokens: aiResult.usage?.completionTokens || estimateTokens(aiResult.message),\n totalTokens:\n aiResult.usage?.totalTokens ||\n estimateTokens(JSON.stringify(modelMessages)) + estimateTokens(aiResult.message),\n };\n if (!aiResult.usageAlreadyRecorded) recordUsage(user.userId, usage.totalTokens);\n const answeredAt = nowIso();\n const assistantMessageId = crypto.randomUUID();\n statements.insertMessage.run({\n id: assistantMessageId,\n conversationId,\n role: 'assistant',\n content: aiResult.message,\n promptTokens: usage.promptTokens,\n completionTokens: usage.completionTokens,\n totalTokens: usage.totalTokens,\n createdAt: answeredAt,\n });\n statements.updateConversation.run({\n id: conversationId,\n userId: user.userId,\n title: isNewConversation ? titleFromMessage(message) : conversation.title,\n updatedAt: answeredAt,\n });\n updateAgentRunStatus({ id: runId, user_id: user.userId }, 'completed', {\n openClawRunId: aiResult.runId,\n inputTokens: usage.promptTokens,\n outputTokens: usage.completionTokens,\n });\n const action = determineClientAction(skillResult, message);\n return jsonResponse(res, 200, {\n success: true,\n runId,\n conversationId,\n messageId: assistantMessageId,\n provider: aiResult.provider,\n model: aiResult.model,\n message: aiResult.message,\n usage: agentUsagePayload(usage, user, budget),\n ...(action ? { clientAction: action } : {}),\n });\n } catch (error) {\n const status = error.name === 'AbortError' ? 'cancelled' : 'failed';\n updateAgentRunStatus({ id: runId, user_id: user.userId }, status, {\n errorCode: error.name || 'AGENT_RUN_FAILED',\n errorMessage: publicAiErrorMessage(error),\n });\n if (status === 'cancelled') {\n return jsonResponse(res, 499, { success: false, runId, status: 'cancelled' });\n }\n throw error;\n } finally {\n activeAgentRuns.delete(runId);\n }\n }),\n);\n\napp.get(\n '/api/agent/runs/:runId/events',\n asyncHandler(async (req, res) => {\n const user = await resolveUser(req, {});\n const run = statements.getAgentRun.get(req.params.runId, user.userId);\n if (!run) {\n return jsonResponse(res, 404, { success: false, message: 'run not found' });\n }\n if (run.status === 'completed' || run.status === 'failed' || run.status === 'cancelled') {\n return jsonResponse(res, 409, { success: false, message: `run is already ${run.status}` });\n }\n\n sseHeaders(res);\n const controller = new AbortController();\n activeAgentRuns.set(run.id, controller);\n updateAgentRunStatus(run, 'running');\n writeSse(res, 'run.started', { runId: run.id, conversationId: run.conversation_id });\n\n try {\n let streamedText = '';\n const modelMessages = buildAgentMessages(run.conversation_id);\n const lastUserMsg = [...modelMessages].reverse().find(m => m.role === 'user');\n const userMsgText = lastUserMsg ? lastUserMsg.content : '';\n const skillResult = userMsgText ? await runResourceFinderSkill({\n message: userMsgText,\n user,\n onStep: (step) => writeSse(res, 'tool.call.completed', {\n tool: 'agent.step',\n displayName: step.title || 'Agent step',\n summary: step.message || '',\n }),\n }) : null;\n const skillContext = resourceContextMessage(skillResult);\n const finalMessages = skillContext ? [\n modelMessages[0],\n { role: 'system', content: skillContext },\n ...modelMessages.slice(1)\n ] : modelMessages;\n\n const promptEstimate = estimateTokens(JSON.stringify(finalMessages));\n const estimated = promptEstimate + 600;\n const budget = enforceTokenBudget(user, estimated);\n const maxCompletionTokens = completionBudgetFor(budget, promptEstimate);\n const aiResult = await runAgentModel({\n messages: finalMessages,\n user,\n conversationId: run.conversation_id,\n mode: run.mode,\n signal: controller.signal,\n maxCompletionTokens,\n callbacks: {\n onStep: (step) => writeSse(res, 'tool.call.completed', {\n tool: 'agent.step',\n displayName: step.title || 'Agent step',\n summary: step.message || '',\n }),\n onToken: (delta) => {\n streamedText += delta;\n writeSse(res, 'assistant.delta', { text: delta });\n },\n },\n });\n const message = aiResult.message || streamedText.trim();\n const usage = {\n promptTokens: aiResult.usage?.promptTokens || estimateTokens(JSON.stringify(finalMessages)),\n completionTokens: aiResult.usage?.completionTokens || estimateTokens(message),\n totalTokens:\n aiResult.usage?.totalTokens || estimateTokens(JSON.stringify(finalMessages)) + estimateTokens(message),\n };\n if (!aiResult.usageAlreadyRecorded) recordUsage(user.userId, usage.totalTokens);\n const assistantMessageId = crypto.randomUUID();\n const answeredAt = nowIso();\n statements.insertMessage.run({\n id: assistantMessageId,\n conversationId: run.conversation_id,\n role: 'assistant',\n content: message,\n promptTokens: usage.promptTokens,\n completionTokens: usage.completionTokens,\n totalTokens: usage.totalTokens,\n createdAt: answeredAt,\n });\n statements.updateConversation.run({\n id: run.conversation_id,\n userId: user.userId,\n title: titleFromMessage(message),\n updatedAt: answeredAt,\n });\n updateAgentRunStatus(run, 'completed', {\n openClawRunId: aiResult.runId,\n inputTokens: usage.promptTokens,\n outputTokens: usage.completionTokens,\n });\n const action = determineClientAction(skillResult, userMsgText);\n writeSse(res, 'assistant.message', { messageId: assistantMessageId, content: message });\n writeSse(res, 'run.completed', {\n usage: agentUsagePayload(usage, user, budget),\n ...(action ? { clientAction: action } : {}),\n });\n res.end();\n } catch (error) {\n const status = error.name === 'AbortError' ? 'cancelled' : 'failed';\n updateAgentRunStatus(run, status, {\n errorCode: error.name || 'AGENT_RUN_FAILED',\n errorMessage: publicAiErrorMessage(error),\n });\n writeSse(res, status === 'cancelled' ? 'run.cancelled' : 'run.failed', {\n runId: run.id,\n message: publicAiErrorMessage(error),\n });\n res.end();\n } finally {\n activeAgentRuns.delete(run.id);\n }\n }),\n);\n\napp.post(\n '/api/agent/runs/:runId/cancel',\n asyncHandler(async (req, res) => {\n const user = await resolveUser(req, req.body || {});\n const run = statements.getAgentRun.get(req.params.runId, user.userId);\n if (!run) {\n return jsonResponse(res, 404, { success: false, message: 'run not found' });\n }\n const controller = activeAgentRuns.get(run.id);\n if (controller) controller.abort();\n updateAgentRunStatus(run, 'cancelled');\n return jsonResponse(res, 200, { success: true, runId: run.id, status: 'cancelled' });\n }),\n);\n\napp.post(\n '/api/agent/messages/:messageId/feedback',\n asyncHandler(async (req, res) => {\n const user = await resolveUser(req, req.body || {});\n const rating = safeUserText(req.body?.rating);\n if (!['up', 'down'].includes(rating)) {\n return jsonResponse(res, 400, { success: false, message: 'rating must be up or down' });\n }\n statements.insertAgentFeedback.run({\n id: crypto.randomUUID(),\n messageId: req.params.messageId,\n userId: user.userId,\n rating,\n reason: safeUserText(req.body?.reason),\n comment: safeUserText(req.body?.comment),\n createdAt: nowIso(),\n });\n return jsonResponse(res, 200, { success: true });\n }),\n);\n\napp.post(\n '/api/ai/chat',\n asyncHandler(async (req, res) => {\n const message = safeUserText(req.body?.message);\n if (!message) {\n return jsonResponse(res, 400, { success: false, message: 'message is required' });\n }\n\n const user = await resolveUser(req, req.body || {});\n const promptEstimate = estimateTokens(message);\n const estimated = promptEstimate + 600;\n const budget = enforceTokenBudget(user, estimated);\n const maxCompletionTokens = completionBudgetFor(budget, promptEstimate);\n const createdAt = nowIso();\n const conversationId = safeUserText(req.body?.conversationId) || crypto.randomUUID();\n const requestedModel = normalizeDeepSeekModelName(req.body?.model);\n const clientContext = clientContextMessage(req.body?.client);\n let conversation = statements.getConversation.get(conversationId, user.userId);\n const isNewConversation = !conversation;\n\n if (isNewConversation) {\n conversation = {\n id: conversationId,\n user_id: user.userId,\n title: titleFromMessage(message),\n };\n statements.insertConversation.run({\n id: conversationId,\n userId: user.userId,\n title: conversation.title,\n provider: 'deepseek',\n model: requestedModel,\n createdAt,\n updatedAt: createdAt,\n });\n }\n\n statements.insertMessage.run({\n id: crypto.randomUUID(),\n conversationId,\n role: 'user',\n content: message,\n promptTokens: 0,\n completionTokens: 0,\n totalTokens: 0,\n createdAt,\n });\n\n const libreChatRuntime = createLibreChatAgentRuntime();\n const skillResult = await runResourceFinderSkill({ message, user });\n const skillContext = resourceContextMessage(skillResult);\n const historyRows = statements.listMessages.all(conversationId).slice(-14);\n const modelMessages = [\n { role: 'system', content: systemPrompt },\n ...(clientContext ? [{ role: 'system', content: clientContext }] : []),\n ...(skillContext ? [{ role: 'system', content: skillContext }] : []),\n ...normalizeMessages(historyRows),\n ];\n\n const openClawRuntime = createOpenClawRuntime();\n const codexRuntime = createCodexDeepSeekRuntime(user);\n let provider = openClawRuntime.enabled\n ? openClawRuntime.provider\n : libreChatRuntime.enabled\n ? libreChatRuntime.provider\n : 'deepseek';\n let responseModel = openClawRuntime.enabled\n ? openClawRuntime.model\n : libreChatRuntime.enabled\n ? libreChatRuntime.model\n : requestedModel;\n let aiResult;\n try {\n if (openClawRuntime.enabled) {\n aiResult = await callOpenClawAgent({\n messages: modelMessages,\n user,\n conversationId,\n mode: normalizeAgentMode(req.body?.mode),\n });\n provider = aiResult.provider || provider;\n responseModel = aiResult.model || responseModel;\n } else if (libreChatRuntime.enabled) {\n aiResult = await callLibreChatAgent(modelMessages);\n } else if (codexRuntime.enabled) {\n provider = codexRuntime.provider;\n aiResult = await callCodexSdkDeepSeek(modelMessages, { user, maxCompletionTokens });\n } else {\n aiResult = await callDeepSeek(modelMessages, { model: requestedModel, maxCompletionTokens });\n }\n } catch (error) {\n if (libreChatRuntime.enabled) {\n logger.warn({ error: String(error) }, 'LibreChat Agent chat failed, falling back to Codex SDK or DeepSeek direct');\n provider = codexRuntime.enabled ? codexRuntime.provider : 'deepseek';\n responseModel = requestedModel;\n if (codexRuntime.enabled) {\n try {\n aiResult = await callCodexSdkDeepSeek(modelMessages, { user, maxCompletionTokens });\n } catch (codexError) {\n if (isDeepSeekQuotaError(codexError)) throw codexError;\n logger.warn({ error: String(codexError) }, 'Codex SDK chat failed, falling back to DeepSeek direct');\n provider = 'deepseek';\n aiResult = await callDeepSeek(modelMessages, { model: requestedModel, maxCompletionTokens });\n }\n } else {\n aiResult = await callDeepSeek(modelMessages, { model: requestedModel, maxCompletionTokens });\n }\n } else {\n if (!codexRuntime.enabled || isDeepSeekQuotaError(error)) throw error;\n logger.warn({ error: String(error) }, 'Codex SDK chat failed, falling back to DeepSeek direct');\n provider = 'deepseek';\n responseModel = requestedModel;\n aiResult = await callDeepSeek(modelMessages, { model: requestedModel, maxCompletionTokens });\n }\n }\n const usage = {\n promptTokens: aiResult.usage.promptTokens || estimateTokens(JSON.stringify(modelMessages)),\n completionTokens: aiResult.usage.completionTokens || estimateTokens(aiResult.message),\n totalTokens:\n aiResult.usage.totalTokens ||\n estimateTokens(JSON.stringify(modelMessages)) + estimateTokens(aiResult.message),\n };\n if (!aiResult.usageAlreadyRecorded) recordUsage(user.userId, usage.totalTokens);\n\n const answeredAt = nowIso();\n statements.insertMessage.run({\n id: crypto.randomUUID(),\n conversationId,\n role: 'assistant',\n content: aiResult.message,\n promptTokens: usage.promptTokens,\n completionTokens: usage.completionTokens,\n totalTokens: usage.totalTokens,\n createdAt: answeredAt,\n });\n statements.updateConversation.run({\n id: conversationId,\n userId: user.userId,\n title: isNewConversation ? titleFromMessage(message) : conversation.title,\n updatedAt: answeredAt,\n });\n\n const latestUsage = usageFor(user.userId);\n const monthlyLimit = budget.limit;\n const action = determineClientAction(skillResult, message);\n jsonResponse(res, 200, {\n success: true,\n conversationId,\n provider,\n model: responseModel,\n message: aiResult.message,\n usage: {\n ...usage,\n monthlyLimit,\n remainingTokens: Math.max(0, monthlyLimit - latestUsage.used),\n },\n ...(action ? { clientAction: action } : {}),\n });\n }),\n);\n\napp.post('/api/ai/chat/stream', async (req, res) => {\n sseHeaders(res);\n\n try {\n const message = safeUserText(req.body?.message);\n if (!message) {\n writeSse(res, 'error', { message: 'message is required' });\n res.end();\n return;\n }\n\n const user = await resolveUser(req, req.body || {});\n const promptEstimate = estimateTokens(message);\n const estimated = promptEstimate + 600;\n const budget = enforceTokenBudget(user, estimated);\n const maxCompletionTokens = completionBudgetFor(budget, promptEstimate);\n const createdAt = nowIso();\n const conversationId = safeUserText(req.body?.conversationId) || crypto.randomUUID();\n const requestedModel = normalizeDeepSeekModelName(req.body?.model);\n const clientContext = clientContextMessage(req.body?.client);\n let conversation = statements.getConversation.get(conversationId, user.userId);\n const isNewConversation = !conversation;\n\n if (isNewConversation) {\n conversation = {\n id: conversationId,\n user_id: user.userId,\n title: titleFromMessage(message),\n };\n statements.insertConversation.run({\n id: conversationId,\n userId: user.userId,\n title: conversation.title,\n provider: 'deepseek',\n model: requestedModel,\n createdAt,\n updatedAt: createdAt,\n });\n }\n\n statements.insertMessage.run({\n id: crypto.randomUUID(),\n conversationId,\n role: 'user',\n content: message,\n promptTokens: 0,\n completionTokens: 0,\n totalTokens: 0,\n createdAt,\n });\n\n const codexRuntime = createCodexDeepSeekRuntime(user);\n const libreChatRuntime = createLibreChatAgentRuntime();\n const initialOpenClawRuntime = createOpenClawRuntime();\n let provider = initialOpenClawRuntime.enabled\n ? initialOpenClawRuntime.provider\n : libreChatRuntime.enabled\n ? libreChatRuntime.provider\n : codexRuntime.enabled\n ? codexRuntime.provider\n : 'deepseek';\n let responseModel = initialOpenClawRuntime.enabled\n ? initialOpenClawRuntime.model\n : libreChatRuntime.enabled\n ? libreChatRuntime.model\n : requestedModel;\n writeSse(res, 'meta', {\n conversationId,\n provider,\n model: responseModel,\n });\n const skillResult = await runResourceFinderSkill({\n message,\n user,\n onStep: (step) => writeSse(res, 'step', step),\n });\n const skillContext = resourceContextMessage(skillResult);\n const historyRows = statements.listMessages.all(conversationId).slice(-14);\n const modelMessages = [\n { role: 'system', content: systemPrompt },\n ...(clientContext ? [{ role: 'system', content: clientContext }] : []),\n ...(skillContext ? [{ role: 'system', content: skillContext }] : []),\n ...normalizeMessages(historyRows),\n ];\n\n let aiResult;\n const openClawRuntime = initialOpenClawRuntime;\n try {\n if (openClawRuntime.enabled) {\n aiResult = await callOpenClawAgent({\n messages: modelMessages,\n user,\n conversationId,\n mode: normalizeAgentMode(req.body?.mode),\n });\n provider = aiResult.provider || provider;\n responseModel = aiResult.model || responseModel;\n writeSse(res, 'delta', { text: aiResult.message });\n } else if (libreChatRuntime.enabled) {\n aiResult = await callLibreChatAgentStream(modelMessages, {\n onToken: (token) => writeSse(res, 'delta', { text: token }),\n });\n } else if (codexRuntime.enabled) {\n aiResult = await callCodexSdkDeepSeek(modelMessages, {\n user,\n maxCompletionTokens,\n onToken: (token) => writeSse(res, 'delta', { text: token }),\n onStep: (step) => writeSse(res, 'step', step),\n });\n } else {\n aiResult = await callDeepSeekStream(modelMessages, {\n model: requestedModel,\n maxCompletionTokens,\n onToken: (token) => writeSse(res, 'delta', { text: token }),\n });\n }\n } catch (error) {\n if (libreChatRuntime.enabled) {\n logger.warn({ error: String(error) }, 'LibreChat Agent streaming chat failed, falling back to Codex SDK or DeepSeek direct');\n provider = codexRuntime.enabled ? codexRuntime.provider : 'deepseek';\n responseModel = requestedModel;\n writeSse(res, 'meta', {\n conversationId,\n provider,\n model: responseModel,\n });\n if (codexRuntime.enabled) {\n try {\n aiResult = await callCodexSdkDeepSeek(modelMessages, {\n user,\n maxCompletionTokens,\n onToken: (token) => writeSse(res, 'delta', { text: token }),\n onStep: (step) => writeSse(res, 'step', step),\n });\n } catch (codexError) {\n if (isDeepSeekQuotaError(codexError)) throw codexError;\n logger.warn({ error: String(codexError) }, 'Codex SDK streaming chat failed, falling back to DeepSeek direct');\n provider = 'deepseek';\n responseModel = requestedModel;\n writeSse(res, 'meta', {\n conversationId,\n provider,\n model: responseModel,\n });\n aiResult = await callDeepSeekStream(modelMessages, {\n model: requestedModel,\n maxCompletionTokens,\n onToken: (token) => writeSse(res, 'delta', { text: token }),\n });\n }\n } else {\n aiResult = await callDeepSeekStream(modelMessages, {\n model: requestedModel,\n maxCompletionTokens,\n onToken: (token) => writeSse(res, 'delta', { text: token }),\n });\n }\n } else {\n if (!codexRuntime.enabled || isDeepSeekQuotaError(error)) throw error;\n logger.warn({ error: String(error) }, 'Codex SDK streaming chat failed, falling back to DeepSeek direct');\n provider = 'deepseek';\n responseModel = requestedModel;\n writeSse(res, 'meta', {\n conversationId,\n provider,\n model: responseModel,\n });\n aiResult = await callDeepSeekStream(modelMessages, {\n model: requestedModel,\n maxCompletionTokens,\n onToken: (token) => writeSse(res, 'delta', { text: token }),\n });\n }\n }\n\n const usage = {\n promptTokens: aiResult.usage.promptTokens || estimateTokens(JSON.stringify(modelMessages)),\n completionTokens: aiResult.usage.completionTokens || estimateTokens(aiResult.message),\n totalTokens:\n aiResult.usage.totalTokens ||\n estimateTokens(JSON.stringify(modelMessages)) + estimateTokens(aiResult.message),\n };\n if (!aiResult.usageAlreadyRecorded) recordUsage(user.userId, usage.totalTokens);\n\n const answeredAt = nowIso();\n statements.insertMessage.run({\n id: crypto.randomUUID(),\n conversationId,\n role: 'assistant',\n content: aiResult.message,\n promptTokens: usage.promptTokens,\n completionTokens: usage.completionTokens,\n totalTokens: usage.totalTokens,\n createdAt: answeredAt,\n });\n statements.updateConversation.run({\n id: conversationId,\n userId: user.userId,\n title: isNewConversation ? titleFromMessage(message) : conversation.title,\n updatedAt: answeredAt,\n });\n\n const latestUsage = usageFor(user.userId);\n const monthlyLimit = budget.limit;\n const action = determineClientAction(skillResult, message);\n writeSse(res, 'done', {\n conversationId,\n provider,\n model: responseModel,\n message: aiResult.message,\n usage: {\n ...usage,\n monthlyLimit,\n remainingTokens: Math.max(0, monthlyLimit - latestUsage.used),\n },\n ...(action ? { clientAction: action } : {}),\n });\n res.end();\n } catch (error) {\n logger.error({ error: error.stack || String(error) }, 'streaming chat failed');\n writeSse(res, 'error', {\n message: publicAiErrorMessage(error),\n details: error.details,\n });\n res.end();\n }\n});\n\napp.get(\n '/api/ai/conversations',\n asyncHandler(async (req, res) => {\n const user = await resolveUser(req, { username: req.query.username });\n const items = statements.listConversations.all(user.userId).map((item) => ({\n id: item.id,\n title: item.title,\n updatedAt: item.updatedAt,\n }));\n jsonResponse(res, 200, { success: true, items });\n }),\n);\n\napp.get(\n '/api/ai/conversations/:id',\n asyncHandler(async (req, res) => {\n const user = await resolveUser(req);\n const conversation = statements.getConversation.get(req.params.id, user.userId);\n if (!conversation) {\n return jsonResponse(res, 404, { success: false, message: 'conversation not found' });\n }\n const messages = statements.listMessages.all(req.params.id).map((item) => ({\n role: item.role,\n content: item.content,\n createdAt: item.createdAt,\n }));\n jsonResponse(res, 200, {\n success: true,\n conversation: {\n id: conversation.id,\n title: conversation.title,\n updatedAt: conversation.updated_at,\n },\n messages,\n });\n }),\n);\n\nfunction buildUrl(base, endpoint, params = {}) {\n const url = new URL(endpoint, `${base}/`);\n for (const [key, value] of Object.entries(params)) {\n if (Array.isArray(value)) {\n for (const item of value) {\n if (item !== undefined && item !== null && item !== '') {\n url.searchParams.append(key, String(item));\n }\n }\n } else if (value !== undefined && value !== null && value !== '') {\n url.searchParams.set(key, String(value));\n }\n }\n return url;\n}\n\nasync function fetchJson(url) {\n const response = await fetch(url, {\n headers: { Accept: 'application/json' },\n signal: AbortSignal.timeout(15_000),\n });\n if (!response.ok) {\n throw new Error(`Request failed: ${response.status}`);\n }\n return response.json();\n}\n\nfunction stripHtml(value) {\n return String(value || '')\n .replace(/<script[\\s\\S]*?<\\/script>/gi, ' ')\n .replace(/<style[\\s\\S]*?<\\/style>/gi, ' ')\n .replace(/<[^>]+>/g, ' ')\n .replace(/ /g, ' ')\n .replace(/</g, '<')\n .replace(/>/g, '>')\n .replace(/&/g, '&')\n .replace(/&#(\\d+);/g, (_match, code) => String.fromCharCode(Number(code)))\n .replace(/\\s+/g, ' ')\n .trim();\n}\n\nfunction cleanExtractedText(value) {\n let text = String(value || '')\n .replace(/\\b[A-Z]{1,3}\\d{2}n\\d{4,5}[a-z]?_p?\\d{4}[a-z]\\d{2}\\b/g, ' ')\n .replace(/\\uFFFD/g, '')\n .replace(/\\s+([,。;:!?、」』)》】])/g, '$1')\n .replace(/([「『《(【])\\s+/g, '$1');\n for (let i = 0; i < 3; i += 1) {\n text = text.replace(/([\\u3400-\\u9fff])\\s+([\\u3400-\\u9fff])/g, '$1$2');\n }\n return text.replace(/[ \\t]{2,}/g, ' ').trim();\n}\n\nfunction compactText(value, limit = 140) {\n const text = stripHtml(value);\n return text.length > limit ? `${text.slice(0, limit)}...` : text;\n}\n\nfunction collectSearchResults(data) {\n if (!data || typeof data !== 'object') return [];\n if (Array.isArray(data.results)) return data.results;\n if (Array.isArray(data?.results?.docs)) return data.results.docs;\n if (Array.isArray(data?.response?.docs)) return data.response.docs;\n if (Array.isArray(data.docs)) return data.docs;\n return [];\n}\n\nfunction firstText(item, keys) {\n for (const key of keys) {\n const value = item?.[key];\n if (Array.isArray(value)) {\n const first = value.map((entry) => safeUserText(String(entry))).find(Boolean);\n if (first) return first;\n }\n if (typeof value === 'string' && value.trim()) return value.trim();\n if (typeof value === 'number') return String(value);\n }\n return '';\n}\n\nfunction firstNumber(item, keys, fallback = undefined) {\n for (const key of keys) {\n const value = item?.[key];\n if (typeof value === 'number') return value;\n if (typeof value === 'string' && value.trim() && Number.isFinite(Number(value))) {\n return Number(value);\n }\n }\n return fallback;\n}\n\nasync function searchCbetaTitle(query, limit) {\n const url = buildUrl(cbetaApiRoot, 'search/title', { q: query, start: 0, rows: limit });\n const data = await fetchJson(url);\n return collectSearchResults(data).flatMap((item) => {\n const work = firstText(item, ['work', 'work_id', 'workId']);\n const title = firstText(item, ['content', 'title', 'work_title', 'workTitle']);\n if (!work || !title) return [];\n const juan = firstNumber(item, ['juan', 'juan_num', 'juanNum'], 1);\n return [\n {\n id: `cbeta-title-${work}-${juan}`,\n title,\n sourceName: 'CBETA 佛典',\n url: `cbeta:${work}:${juan}`,\n snippet: firstText(item, ['byline', 'creators', 'time_dynasty']) || '可下载经文正文并用于全球法布施。',\n resourceType: 'scripture',\n work,\n juan,\n },\n ];\n });\n}\n\nasync function searchCbetaContent(query, limit) {\n const endpoints = ['search/fulltext', 'search/content', 'search/all_in_one'];\n const settled = await Promise.allSettled(\n endpoints.map((endpoint) =>\n fetchJson(buildUrl(cbetaApiRoot, endpoint, { q: query, start: 0, rows: limit, field: 'content' })),\n ),\n );\n const results = [];\n for (const response of settled) {\n if (response.status !== 'fulfilled') continue;\n for (const item of collectSearchResults(response.value)) {\n const work = firstText(item, ['work', 'work_id', 'workId', 'sutra', 'id']);\n const snippet = compactText(firstText(item, ['kwic', 'highlight', 'snippet', 'content', 'text', 'body', 'p']));\n if (!work || !snippet) continue;\n const juan = firstNumber(item, ['juan', 'juan_num', 'juanNum', '卷'], 1);\n const title = firstText(item, ['title', 'work_title', 'workTitle', 'sutra_name', 'book', 'name']) || work;\n results.push({\n id: `cbeta-content-${work}-${juan}-${sha256(snippet).slice(0, 8)}`,\n title,\n sourceName: 'CBETA 佛典',\n url: `cbeta:${work}:${juan}`,\n snippet,\n resourceType: 'scripture',\n work,\n juan,\n });\n if (results.length >= limit) return results;\n }\n }\n return results;\n}\n\nfunction directUrlResource(query) {\n const url = safeUserText(query);\n let parsed;\n try {\n parsed = new URL(url);\n } catch {\n return null;\n }\n if (!['http:', 'https:'].includes(parsed.protocol)) return null;\n return {\n id: `url-${sha256(url).slice(0, 12)}`,\n title: parsed.hostname,\n sourceName: '网页资源',\n url,\n snippet: '直接下载并提取这个链接的可读正文。',\n resourceType: 'web',\n };\n}\n\nfunction looksLikeAudioQuery(query) {\n return /音乐|音樂|音频|音訊|mp3|audio|念诵|念誦|诵经|誦經|唱诵|唱誦|读诵|讀誦/.test(\n safeUserText(query).toLowerCase(),\n );\n}\n\nfunction archiveSearchTerm(query) {\n return (\n safeUserText(query)\n .replace(/下载|查找|寻找|找一?个|资源|音频|音訊|音乐|音樂|mp3|audio|念诵|念誦|诵经|誦經|唱诵|唱誦|读诵|讀誦/gi, ' ')\n .replace(/\\s+/g, ' ')\n .trim() || safeUserText(query)\n );\n}\n\nfunction solrPhrase(value) {\n return String(value).replace(/\\\\/g, '\\\\\\\\').replace(/\"/g, '\\\\\"');\n}\n\nasync function searchInternetArchiveAudio(query, limit) {\n if (!looksLikeAudioQuery(query)) return [];\n const term = archiveSearchTerm(query);\n if (!term) return [];\n const variants = Array.from(\n new Set([\n term,\n term.replaceAll('金刚经', '金剛經'),\n term.replaceAll('金剛經', '金刚经'),\n ].filter(Boolean)),\n );\n const phraseQuery = variants\n .flatMap((item) => [`title:\"${solrPhrase(item)}\"`, `description:\"${solrPhrase(item)}\"`])\n .join(' OR ');\n const url = buildUrl('https://archive.org', 'advancedsearch.php', {\n q: `(${phraseQuery}) AND mediatype:audio`,\n rows: Math.min(Math.max(limit, 1), 10),\n output: 'json',\n 'fl[]': ['identifier', 'title', 'description'],\n });\n const data = await fetchJson(url);\n const docs = Array.isArray(data?.response?.docs) ? data.response.docs : [];\n return docs\n .map((item) => {\n const identifier = firstText(item, ['identifier']);\n const title = firstText(item, ['title']) || identifier;\n if (!identifier || !title) return null;\n return {\n id: `ia-audio-${identifier}`,\n title,\n sourceName: 'Internet Archive 音频',\n url: `ia:${identifier}`,\n snippet: compactText(firstText(item, ['description'])) || '公开可访问的音频资源,将下载第一个可用 MP3 文件。',\n resourceType: 'audio',\n identifier,\n };\n })\n .filter(Boolean);\n}\n\nconst curatedCbetaWorks = [\n {\n work: 'T0235',\n juan: 1,\n title: '金剛般若波羅蜜經',\n keywords: ['金剛般若波羅蜜經', '金刚般若波罗蜜经', '金剛經', '金刚经'],\n snippet: '姚秦 鳩摩羅什譯。常用于读诵、共修与法布施的般若经典。',\n },\n {\n work: 'T0251',\n juan: 1,\n title: '般若波羅蜜多心經',\n keywords: ['般若波羅蜜多心經', '般若波罗蜜多心经', '心經', '心经'],\n snippet: '唐 玄奘譯。篇幅短,适合加入禅室功课本与日常持诵。',\n },\n {\n work: 'T0262',\n juan: 1,\n title: '妙法蓮華經',\n keywords: ['妙法蓮華經', '妙法莲华经', '法華經', '法华经'],\n snippet: '姚秦 鳩摩羅什譯。可按卷下载正文,用于长期功课与分享。',\n },\n {\n work: 'T0279',\n juan: 1,\n title: '大方廣佛華嚴經',\n keywords: ['大方廣佛華嚴經', '大方广佛华严经', '華嚴經', '华严经'],\n snippet: '唐 實叉難陀譯。可按卷下载正文,用于系统研读与法布施。',\n },\n {\n work: 'T0360',\n juan: 1,\n title: '佛說無量壽經',\n keywords: ['佛說無量壽經', '佛说无量寿经', '無量壽經', '无量寿经'],\n snippet: '净土经典。可下载正文,用于净土功课本与全球法布施。',\n },\n {\n work: 'T0365',\n juan: 1,\n title: '佛說觀無量壽佛經',\n keywords: ['佛說觀無量壽佛經', '佛说观无量寿佛经', '觀無量壽經', '观无量寿经'],\n snippet: '净土三经之一。适合查找下载后加入净土功课。',\n },\n {\n work: 'T0366',\n juan: 1,\n title: '佛說阿彌陀經',\n keywords: ['佛說阿彌陀經', '佛说阿弥陀经', '阿彌陀經', '阿弥陀经'],\n snippet: '姚秦 鳩摩羅什譯。篇幅适中,适合共修、持诵与分享。',\n },\n {\n work: 'T0475',\n juan: 1,\n title: '維摩詰所說經',\n keywords: ['維摩詰所說經', '维摩诘所说经', '維摩詰經', '维摩诘经'],\n snippet: '姚秦 鳩摩羅什譯。大乘经典,可按卷下载用于研读。',\n },\n {\n work: 'T0676',\n juan: 1,\n title: '解深密經',\n keywords: ['解深密經', '解深密经'],\n snippet: '唯识相关大乘经典。适合研读、整理笔记与法布施。',\n },\n {\n work: 'T1666',\n juan: 1,\n title: '大乘起信論',\n keywords: ['大乘起信論', '大乘起信论', '起信論', '起信论'],\n snippet: '大乘论典。适合下载后作为研习功课资料。',\n },\n];\n\nfunction localCbetaResults(query, limit) {\n const normalizedQuery = safeUserText(query).toLowerCase();\n if (!normalizedQuery) return [];\n return curatedCbetaWorks\n .filter((item) =>\n item.keywords.some((keyword) => {\n const normalizedKeyword = keyword.toLowerCase();\n return normalizedKeyword.includes(normalizedQuery) || normalizedQuery.includes(normalizedKeyword);\n }),\n )\n .slice(0, limit)\n .map((item) => ({\n id: `cbeta-curated-${item.work}-${item.juan}`,\n title: item.title,\n sourceName: 'CBETA 佛典',\n url: `cbeta:${item.work}:${item.juan}`,\n snippet: item.snippet,\n resourceType: 'scripture',\n work: item.work,\n juan: item.juan,\n }));\n}\n\nfunction uniqueResources(items, limit) {\n const seen = new Set();\n const results = [];\n for (const item of items) {\n const key = `${item.url}:${item.title}`;\n if (seen.has(key)) continue;\n seen.add(key);\n results.push(item);\n if (results.length >= limit) break;\n }\n return results;\n}\n\nasync function findResourceCandidates(query, limit) {\n const direct = directUrlResource(query);\n const results = [];\n if (direct) results.push(direct);\n try {\n results.push(...(await searchInternetArchiveAudio(query, limit)));\n } catch (error) {\n logger.warn({ error: String(error), query }, 'Internet Archive audio search skipped');\n }\n results.push(...localCbetaResults(query, limit));\n\n const [titleResults, contentResults] = await Promise.allSettled([\n searchCbetaTitle(query, limit),\n searchCbetaContent(query, limit),\n ]);\n if (titleResults.status === 'fulfilled') results.push(...titleResults.value);\n if (contentResults.status === 'fulfilled') results.push(...contentResults.value);\n\n return uniqueResources(results, limit);\n}\n\nfunction looksLikeResourceTask(message) {\n const text = safeUserText(message).toLowerCase();\n if (!text) return false;\n return /下载|查找|寻找|找一?个|资源|經文|经文|佛经|佛經|仪轨|儀軌|音乐|音樂|mp3|audio|念诵|念誦|诵经|誦經/.test(text);\n}\n\nfunction extractResourceQuery(message) {\n const text = safeUserText(message)\n .replace(/请|請|帮我|幫我|麻烦|麻煩|可以|能不能|需要|我要|想要/g, ' ')\n .replace(/下载|下載|查找|寻找|尋找|找一个|找一個|找|资源|資源|并|並|然后|然後|用于|用於|全球法布施|加入功课本|加入功課本/g, ' ')\n .replace(/\\s+/g, ' ')\n .trim();\n return text || safeUserText(message);\n}\n\nfunction resourceContextMessage(skillResult) {\n if (!skillResult) return '';\n const lines = [\n '本轮资源检索/下载已经由大乘 App 后端完成。',\n '不要再次调用 search_dharma_resources、download_dharma_resource 或其他资源 MCP 工具;请直接基于以下结果回答用户。',\n `后端已调用 skill: ${skillResult.skillName}`,\n `执行目标: ${skillResult.query}`,\n ];\n if (skillResult.selected) {\n lines.push(`候选资源: ${skillResult.selected.title}`);\n lines.push(`来源: ${skillResult.selected.sourceName}`);\n lines.push(`资源地址: ${skillResult.selected.url}`);\n }\n if (skillResult.downloaded) {\n lines.push(`下载标题: ${skillResult.downloaded.title}`);\n lines.push(`下载文件: ${skillResult.downloaded.fileName}`);\n const text = safeUserText(skillResult.downloaded.contentText);\n if (text) {\n lines.push('下载正文摘录:');\n lines.push(text.slice(0, 8000));\n }\n }\n return lines.join('\\n');\n}\n\nfunction determineClientAction(skillResult, message) {\n if (skillResult && skillResult.downloaded) {\n const text = safeUserText(message);\n if (/法布施|全球法布施|分享/.test(text)) {\n return {\n type: 'prepare_dharma_share_text',\n title: skillResult.downloaded.title,\n text: skillResult.downloaded.contentText || '',\n };\n }\n }\n return null;\n}\n\nasync function runResourceFinderSkill({ message, user, onStep }) {\n if (!looksLikeResourceTask(message)) return null;\n\n const skillName = 'resource-finder-downloader';\n const query = extractResourceQuery(message);\n const audioTask = looksLikeAudioQuery(message) || looksLikeAudioQuery(query);\n onStep?.({\n type: 'skill.step',\n skillName,\n stage: 'start',\n title: '调用资源查找下载 skill',\n message: `识别到资源任务: ${query}`,\n });\n\n onStep?.({\n type: 'skill.step',\n skillName,\n stage: 'search',\n title: '搜索可分享资源',\n message: audioTask\n ? '优先检索公开可访问音频资源,失败时再回落到佛典正文资源。'\n : '优先检索 CBETA 佛典正文接口,搜索索引不可用时使用内置经典映射。',\n });\n const candidates = await findResourceCandidates(query, 8);\n if (candidates.length === 0) {\n onStep?.({\n type: 'skill.step',\n skillName,\n stage: 'empty',\n title: '没有找到可下载资源',\n message: '后端没有找到明确可下载且可分享的资源。',\n });\n return {\n skillName,\n query,\n candidates: [],\n selected: null,\n downloaded: null,\n };\n }\n\n const selected = candidates[0];\n onStep?.({\n type: 'skill.step',\n skillName,\n stage: 'select',\n title: '选择可信候选资源',\n message: `${selected.title} / ${selected.sourceName}`,\n });\n\n onStep?.({\n type: 'skill.step',\n skillName,\n stage: 'download',\n title: '下载并提取资源',\n message: selected.url,\n });\n const cbeta = parseCbetaResource(selected);\n const internetArchive = parseInternetArchiveResource(selected);\n const downloaded = cbeta\n ? await downloadCbetaResource({ ...selected, ...cbeta })\n : internetArchive\n ? await downloadInternetArchiveAudio({ ...selected, ...internetArchive })\n : await downloadWebResource(selected);\n const persisted = persistDownloadedResource(user, downloaded);\n\n onStep?.({\n type: 'skill.step',\n skillName,\n stage: 'done',\n title: '资源已准备完成',\n message: downloaded.fileName,\n });\n\n return {\n skillName,\n query,\n candidates,\n selected,\n downloaded: {\n ...downloaded,\n downloadId: persisted.id,\n },\n };\n}\n\napp.post(\n '/api/resources/search',\n asyncHandler(async (req, res) => {\n const query = safeUserText(req.body?.query);\n const limit = Math.min(Math.max(Number(req.body?.limit || 12), 1), 20);\n if (query.length < 2) {\n return jsonResponse(res, 400, { success: false, message: 'query is required' });\n }\n\n const items = await findResourceCandidates(query, limit);\n\n jsonResponse(res, 200, {\n success: true,\n source: 'resource-index',\n items,\n });\n }),\n);\n\nfunction parseCbetaResource(input) {\n const work = safeUserText(input.work);\n const juan = Number(input.juan || 1);\n if (work) return { work, juan: Number.isFinite(juan) ? juan : 1 };\n\n const url = safeUserText(input.url);\n const match = /^cbeta:([^:]+):(\\d+)$/i.exec(url);\n if (!match) return null;\n return {\n work: match[1].toUpperCase(),\n juan: Number(match[2] || 1),\n };\n}\n\nfunction parseInternetArchiveResource(input) {\n const url = safeUserText(input.url);\n const match = /^ia:([^:]+)$/i.exec(url);\n const identifier = match?.[1] || safeUserText(input.identifier);\n if (!identifier) return null;\n return { identifier };\n}\n\nfunction safeFileName(value) {\n const normalized = String(value || 'dharma-resource')\n .replace(/[\\\\/:*?\"<>|]+/g, '-')\n .replace(/\\s+/g, '-')\n .replace(/-+/g, '-')\n .replace(/^-|-$/g, '')\n .slice(0, 80);\n return normalized || 'dharma-resource';\n}\n\nfunction extractCbetaHtml(data) {\n const first = Array.isArray(data?.results) ? data.results[0] : null;\n if (typeof first === 'string') return first;\n if (first && typeof first === 'object' && typeof first.html === 'string') return first.html;\n if (typeof data?.html === 'string') return data.html;\n return '';\n}\n\nasync function downloadCbetaResource(resource) {\n const url = buildUrl(cbetaApiRoot, 'juans', {\n work: resource.work,\n juan: resource.juan,\n work_info: 1,\n toc: 1,\n });\n const data = await fetchJson(url);\n const workInfo = data.work_info || {};\n const title = safeUserText(workInfo.title) || safeUserText(resource.title) || resource.work;\n const html = extractCbetaHtml(data);\n const text = cleanExtractedText(stripHtml(html));\n if (!text) throw new Error('CBETA resource returned empty content');\n return {\n title,\n sourceName: 'CBETA 佛典',\n url: `cbeta:${resource.work}:${resource.juan}`,\n contentText: text.slice(0, maxResourceTextChars),\n fileName: `${resource.work}_${resource.juan}_${safeFileName(title)}.txt`,\n };\n}\n\nasync function downloadInternetArchiveAudio(resource) {\n const identifier = safeUserText(resource.identifier);\n if (!identifier) throw new Error('Internet Archive identifier is required');\n const metadata = await fetchJson(`https://archive.org/metadata/${encodeURIComponent(identifier)}`);\n const title = safeUserText(metadata?.metadata?.title) || safeUserText(resource.title) || identifier;\n const files = Array.isArray(metadata?.files) ? metadata.files : [];\n const audioFile = files.find((file) => {\n const name = safeUserText(file?.name);\n const format = safeUserText(file?.format).toLowerCase();\n return /\\.(mp3|m4a|ogg)$/i.test(name) || format.includes('mp3');\n });\n if (!audioFile?.name) {\n throw new Error('Internet Archive audio item has no downloadable audio file');\n }\n const sourceFileName = audioFile.name;\n const fileUrl = `https://archive.org/download/${encodeURIComponent(identifier)}/${sourceFileName\n .split('/')\n .map((part) => encodeURIComponent(part))\n .join('/')}`;\n const response = await fetch(fileUrl, {\n headers: {\n Accept: 'audio/mpeg, audio/*;q=0.9, */*;q=0.5',\n 'User-Agent': 'DachengResourceDownloader/1.0 (+https://ombhrum.com)',\n },\n signal: AbortSignal.timeout(60_000),\n });\n if (!response.ok) {\n throw new Error(`Internet Archive audio download failed: ${response.status}`);\n }\n const binaryContent = Buffer.from(await response.arrayBuffer());\n if (!binaryContent.length) throw new Error('Internet Archive audio returned empty content');\n const sizeMb = (binaryContent.length / 1024 / 1024).toFixed(1);\n return {\n title,\n sourceName: 'Internet Archive 音频',\n url: fileUrl,\n contentText: `已下载音频资源: ${title}\\n文件名: ${sourceFileName}\\n大小: ${sizeMb} MB\\n来源: ${fileUrl}`,\n binaryContent,\n fileName: safeFileName(sourceFileName),\n };\n}\n\nasync function downloadWebResource(input) {\n const url = safeUserText(input.url);\n const parsed = new URL(url);\n if (!['http:', 'https:'].includes(parsed.protocol)) {\n throw new Error('Only http/https resources can be downloaded');\n }\n\n const response = await fetch(parsed, {\n headers: {\n Accept: 'text/html, text/plain, application/xhtml+xml, application/xml;q=0.8, */*;q=0.5',\n 'User-Agent': 'DachengResourceDownloader/1.0 (+https://ombhrum.com)',\n },\n signal: AbortSignal.timeout(25_000),\n });\n if (!response.ok) throw new Error(`Resource download failed: ${response.status}`);\n const contentType = response.headers.get('content-type') || '';\n const body = await response.text();\n const title = safeUserText(input.title) || parsed.hostname;\n const contentText = contentType.includes('html') ? cleanExtractedText(stripHtml(body)) : body.trim();\n if (!contentText) throw new Error('Resource returned empty content');\n return {\n title,\n sourceName: safeUserText(input.sourceName) || parsed.hostname,\n url,\n contentText: contentText.slice(0, maxResourceTextChars),\n fileName: `${safeFileName(title)}.txt`,\n };\n}\n\nfunction persistDownloadedResource(user, content) {\n const id = crypto.randomUUID();\n const fileName = `${id}-${safeFileName(content.fileName)}`;\n const filePath = path.join(resourcesDir, fileName);\n if (content.binaryContent) {\n fs.writeFileSync(filePath, content.binaryContent);\n } else {\n fs.writeFileSync(filePath, content.contentText, 'utf8');\n }\n statements.insertResourceDownload.run({\n id,\n userId: user.userId,\n title: content.title,\n sourceName: content.sourceName,\n sourceUrl: content.url,\n fileName: content.fileName,\n filePath,\n createdAt: nowIso(),\n });\n return { id, fileName, filePath };\n}\n\nfunction createLibreChatMcpServer() {\n const server = new McpServer({\n name: 'dacheng-ai-tools',\n version: '0.1.0',\n });\n\n server.registerTool(\n 'search_dharma_resources',\n {\n title: 'Search dharma resources',\n description:\n 'Search for legally shareable Buddhist scripture, text, web, or audio resources that can be prepared for Fabushi sharing.',\n inputSchema: {\n query: z.string().min(2).max(200),\n limit: z.number().int().min(1).max(12).optional(),\n },\n outputSchema: {\n query: z.string(),\n items: z.array(\n z.object({\n id: z.string(),\n title: z.string(),\n sourceName: z.string(),\n url: z.string(),\n snippet: z.string(),\n resourceType: z.string(),\n work: z.string().optional(),\n juan: z.number().optional(),\n }),\n ),\n },\n annotations: {\n readOnlyHint: true,\n destructiveHint: false,\n openWorldHint: true,\n },\n },\n async ({ query, limit }) => {\n const items = await findResourceCandidates(query, limit ?? 8);\n return textToolResult(`Found ${items.length} candidate resources for \"${query}\".`, {\n query,\n items,\n });\n },\n );\n\n server.registerTool(\n 'download_dharma_resource',\n {\n title: 'Download dharma resource',\n description:\n 'Download and extract a selected resource. Use the url/work/juan returned by search_dharma_resources whenever possible.',\n inputSchema: {\n url: z.string().min(1).max(1000),\n title: z.string().max(300).optional(),\n sourceName: z.string().max(120).optional(),\n work: z.string().max(40).optional(),\n juan: z.number().int().min(1).optional(),\n identifier: z.string().max(300).optional(),\n },\n outputSchema: {\n title: z.string(),\n sourceName: z.string(),\n url: z.string(),\n fileName: z.string(),\n downloadId: z.string(),\n contentText: z.string(),\n },\n annotations: {\n readOnlyHint: false,\n destructiveHint: false,\n openWorldHint: true,\n },\n },\n async (input) => {\n const user = { userId: 'librechat:mcp' };\n const cbeta = parseCbetaResource(input);\n const internetArchive = parseInternetArchiveResource(input);\n const content = cbeta\n ? await downloadCbetaResource({ ...input, ...cbeta })\n : internetArchive\n ? await downloadInternetArchiveAudio({ ...input, ...internetArchive })\n : await downloadWebResource(input);\n const persisted = persistDownloadedResource(user, content);\n const { binaryContent: _binaryContent, ...publicContent } = content;\n return textToolResult(`Downloaded ${publicContent.title} as ${publicContent.fileName}.`, {\n ...publicContent,\n downloadId: persisted.id,\n });\n },\n );\n\n server.registerTool(\n 'prepare_dharma_share_text',\n {\n title: 'Prepare dharma sharing text',\n description:\n 'Prepare a text payload for the Fabushi app to confirm and add to global dharma sharing. This returns a clientAction; it does not start sending.',\n inputSchema: {\n title: z.string().min(1).max(120),\n text: z.string().min(1).max(20000),\n },\n outputSchema: {\n clientAction: z.object({\n type: z.literal('prepare_dharma_share_text'),\n title: z.string(),\n text: z.string(),\n }),\n },\n annotations: {\n readOnlyHint: false,\n destructiveHint: false,\n openWorldHint: false,\n },\n },\n async ({ title, text }) =>\n textToolResult(`Prepared \"${title}\" for user confirmation in the Fabushi app.`, {\n clientAction: {\n type: 'prepare_dharma_share_text',\n title: safeUserText(title).slice(0, 120),\n text: String(text || '').trim().slice(0, 20000),\n },\n }),\n );\n\n server.registerTool(\n 'prepare_practice_book_item',\n {\n title: 'Prepare practice book item',\n description:\n 'Prepare a scripture, chant, or practice item for the Fabushi app practice book. This returns a clientAction; it does not mutate the app by itself.',\n inputSchema: {\n title: z.string().min(1).max(120),\n sourceName: z.string().max(120).optional(),\n text: z.string().min(1).max(20000),\n },\n outputSchema: {\n clientAction: z.object({\n type: z.literal('prepare_practice_book_item'),\n title: z.string(),\n sourceName: z.string(),\n text: z.string(),\n }),\n },\n annotations: {\n readOnlyHint: false,\n destructiveHint: false,\n openWorldHint: false,\n },\n },\n async ({ title, sourceName, text }) =>\n textToolResult(`Prepared \"${title}\" for the Fabushi practice book confirmation flow.`, {\n clientAction: {\n type: 'prepare_practice_book_item',\n title: safeUserText(title).slice(0, 120),\n sourceName: safeUserText(sourceName || '大乘 AI').slice(0, 120),\n text: String(text || '').trim().slice(0, 20000),\n },\n }),\n );\n\n return server;\n}\n\nasync function handleMcpRequest(req, res) {\n const server = createLibreChatMcpServer();\n const transport = new StreamableHTTPServerTransport({\n sessionIdGenerator: undefined,\n enableJsonResponse: true,\n });\n\n res.on('close', () => {\n transport.close();\n server.close();\n });\n\n await server.connect(transport);\n await transport.handleRequest(req, res, req.body);\n}\n\napp.all('/mcp', async (req, res) => {\n try {\n await handleMcpRequest(req, res);\n } catch (error) {\n logger.error({ error: error.stack || String(error) }, 'MCP request failed');\n if (!res.headersSent) {\n res.status(500).json({\n error: 'MCP request failed',\n message: publicAiErrorMessage(error),\n });\n }\n }\n});\n\napp.post(\n '/api/resources/download',\n asyncHandler(async (req, res) => {\n const user = await resolveUser(req, req.body || {});\n const cbeta = parseCbetaResource(req.body || {});\n const internetArchive = parseInternetArchiveResource(req.body || {});\n const content = cbeta\n ? await downloadCbetaResource({ ...req.body, ...cbeta })\n : internetArchive\n ? await downloadInternetArchiveAudio({ ...req.body, ...internetArchive })\n : await downloadWebResource(req.body || {});\n const persisted = persistDownloadedResource(user, content);\n const { binaryContent: _binaryContent, ...publicContent } = content;\n jsonResponse(res, 200, {\n success: true,\n downloadId: persisted.id,\n ...publicContent,\n });\n }),\n);\n\napp.post(\n '/api/codex/resource-task',\n asyncHandler(async (req, res) => {\n if (process.env.ENABLE_CODEX_SDK !== 'true') {\n return jsonResponse(res, 403, {\n success: false,\n message: 'Codex SDK task runner is disabled. Set ENABLE_CODEX_SDK=true on the server to enable it.',\n });\n }\n const prompt = safeUserText(req.body?.prompt);\n if (!prompt) {\n return jsonResponse(res, 400, { success: false, message: 'prompt is required' });\n }\n const user = await resolveUser(req, req.body || {});\n const { Codex } = await import('@openai/codex-sdk');\n const codexRuntime = createCodexDeepSeekRuntime(user);\n const codex = new Codex(codexRuntime.enabled ? codexRuntime.options : {});\n const thread = codex.startThread(\n codexRuntime.enabled\n ? codexRuntime.threadOptions\n : { skipGitRepoCheck: true, networkAccessEnabled: true },\n );\n const result = await thread.run(prompt);\n const usage = {\n promptTokens: Number(result.usage?.input_tokens || 0),\n completionTokens: Number(result.usage?.output_tokens || 0),\n totalTokens: Number((result.usage?.input_tokens || 0) + (result.usage?.output_tokens || 0)),\n };\n if (!codexRuntime.enabled) {\n recordUsage(user.userId, usage.totalTokens || estimateTokens(JSON.stringify(result)));\n }\n jsonResponse(res, 200, {\n success: true,\n threadId: thread.id,\n provider: codexRuntime.provider,\n model: deepseekModel,\n usage,\n result,\n });\n }),\n);\n\nregisterPlatformApi({ app, db, resolveUser, asyncHandler });\n\napp.use((error, _req, res, _next) => {\n const status = Number(error.statusCode || error.status || 500);\n const safeStatus = status >= 400 && status < 600 ? status : 500;\n logger.error({ error: error.stack || String(error) }, 'request failed');\n jsonResponse(res, safeStatus, {\n success: false,\n message: error.message || 'Internal Server Error',\n details: error.details,\n });\n});\n\nconst port = Number(process.env.PORT || 8788);\napp.listen(port, '0.0.0.0', () => {\n logger.info(\n {\n port,\n dbPath,\n dataDir,\n deepseekModel,\n cbetaApiRoot,\n },\n 'Dacheng AI backend started',\n );\n});\n"}, "c6d87d08632cabf53741e725c958f696bb2c7e02": {"t": 1785757955.757209, "n": 151352, "h": "605ecbd5b60ceb88", "c": "import crypto from 'node:crypto';\nimport fs from 'node:fs';\nimport path from 'node:path';\nimport process from 'node:process';\nimport { fileURLToPath } from 'node:url';\n\nimport { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';\nimport { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';\nimport Database from 'better-sqlite3';\nimport cors from 'cors';\nimport dotenv from 'dotenv';\nimport express from 'express';\nimport rateLimit from 'express-rate-limit';\nimport helmet from 'helmet';\nimport pino from 'pino';\nimport { z } from 'zod';\n\nimport { registerPlatformApi } from './platform_api.js';\n\ndotenv.config();\n\nconst __dirname = path.dirname(fileURLToPath(import.meta.url));\nconst rootDir = path.resolve(__dirname, '..');\nconst dataDir = process.env.DATA_DIR || path.join(rootDir, 'data');\nconst resourcesDir = path.join(dataDir, 'resources');\nconst miniAppsDir = path.join(dataDir, 'miniapps');\nconst miniAppsStorePath = path.join(miniAppsDir, 'sandbox-miniapps.json');\nconst openClawRuntimeUpdatesDir =\n process.env.OPENCLAW_RUNTIME_UPDATES_DIR || path.join(dataDir, 'openclaw-runtime');\nconst openClawRuntimeEnableMacosUpdates =\n process.env.OPENCLAW_RUNTIME_ENABLE_MACOS_UPDATES === 'true';\nconst codexHomeDir = process.env.CODEX_HOME || path.join(dataDir, 'codex-home');\nconst codexTempDir = process.env.CODEX_TMPDIR || path.join(dataDir, 'codex-tmp');\nconst codexRuntimeDir = process.env.XDG_RUNTIME_DIR || path.join(dataDir, 'codex-runtime');\nconst dbPath = process.env.SQLITE_PATH || path.join(dataDir, 'dacheng-ai.sqlite');\n\nfs.mkdirSync(resourcesDir, { recursive: true });\nfs.mkdirSync(miniAppsDir, { recursive: true });\nfs.mkdirSync(openClawRuntimeUpdatesDir, { recursive: true });\nfs.mkdirSync(codexHomeDir, { recursive: true });\nfs.mkdirSync(codexTempDir, { recursive: true });\nfs.mkdirSync(codexRuntimeDir, { recursive: true });\n\nconst logger = pino({\n level: process.env.LOG_LEVEL || 'info',\n});\n\nconst app = express();\napp.disable('x-powered-by');\napp.set('trust proxy', Number(process.env.TRUST_PROXY_HOPS || 1));\napp.use(helmet());\nconst corsOrigin =\n !process.env.CORS_ORIGIN || process.env.CORS_ORIGIN.trim() === '*'\n ? true\n : process.env.CORS_ORIGIN.split(',').map((item) => item.trim()).filter(Boolean);\napp.use(cors({ origin: corsOrigin }));\napp.use(express.json({ limit: '1mb' }));\napp.use(\n rateLimit({\n windowMs: 60_000,\n limit: Number(process.env.RATE_LIMIT_PER_MINUTE || 90),\n standardHeaders: true,\n legacyHeaders: false,\n }),\n);\n\nconst db = new Database(dbPath);\ndb.pragma('journal_mode = WAL');\ndb.pragma('foreign_keys = ON');\n\ndb.exec(`\n CREATE TABLE IF NOT EXISTS conversations (\n id TEXT PRIMARY KEY,\n user_id TEXT NOT NULL,\n title TEXT NOT NULL,\n provider TEXT NOT NULL,\n model TEXT NOT NULL,\n created_at TEXT NOT NULL,\n updated_at TEXT NOT NULL\n );\n\n CREATE INDEX IF NOT EXISTS idx_conversations_user_updated\n ON conversations (user_id, updated_at DESC);\n\n CREATE TABLE IF NOT EXISTS messages (\n id TEXT PRIMARY KEY,\n conversation_id TEXT NOT NULL,\n role TEXT NOT NULL,\n content TEXT NOT NULL,\n prompt_tokens INTEGER NOT NULL DEFAULT 0,\n completion_tokens INTEGER NOT NULL DEFAULT 0,\n total_tokens INTEGER NOT NULL DEFAULT 0,\n created_at TEXT NOT NULL,\n FOREIGN KEY (conversation_id) REFERENCES conversations(id) ON DELETE CASCADE\n );\n\n CREATE INDEX IF NOT EXISTS idx_messages_conversation_created\n ON messages (conversation_id, created_at ASC);\n\n CREATE TABLE IF NOT EXISTS usage_months (\n user_id TEXT NOT NULL,\n month TEXT NOT NULL,\n total_tokens INTEGER NOT NULL DEFAULT 0,\n updated_at TEXT NOT NULL,\n PRIMARY KEY (user_id, month)\n );\n\n CREATE TABLE IF NOT EXISTS resource_downloads (\n id TEXT PRIMARY KEY,\n user_id TEXT NOT NULL,\n title TEXT NOT NULL,\n source_name TEXT NOT NULL,\n source_url TEXT NOT NULL,\n file_name TEXT NOT NULL,\n file_path TEXT NOT NULL,\n created_at TEXT NOT NULL\n );\n\n CREATE TABLE IF NOT EXISTS agent_runs (\n id TEXT PRIMARY KEY,\n user_id TEXT NOT NULL,\n conversation_id TEXT NOT NULL,\n message_id TEXT NOT NULL,\n openclaw_run_id TEXT,\n openclaw_session_key TEXT NOT NULL,\n status TEXT NOT NULL,\n mode TEXT NOT NULL,\n provider TEXT NOT NULL,\n model TEXT,\n input_tokens INTEGER NOT NULL DEFAULT 0,\n output_tokens INTEGER NOT NULL DEFAULT 0,\n tool_call_count INTEGER NOT NULL DEFAULT 0,\n cost_microusd INTEGER NOT NULL DEFAULT 0,\n started_at TEXT NOT NULL,\n completed_at TEXT,\n failed_at TEXT,\n error_code TEXT,\n error_message TEXT\n );\n\n CREATE INDEX IF NOT EXISTS idx_agent_runs_user_started\n ON agent_runs (user_id, started_at DESC);\n\n CREATE INDEX IF NOT EXISTS idx_agent_runs_status_started\n ON agent_runs (status, started_at DESC);\n\n CREATE TABLE IF NOT EXISTS agent_message_feedback (\n id TEXT PRIMARY KEY,\n message_id TEXT NOT NULL,\n user_id TEXT NOT NULL,\n rating TEXT NOT NULL,\n reason TEXT,\n comment TEXT,\n created_at TEXT NOT NULL\n );\n`);\n\nconst statements = {\n insertConversation: db.prepare(`\n INSERT INTO conversations (id, user_id, title, provider, model, created_at, updated_at)\n VALUES (@id, @userId, @title, @provider, @model, @createdAt, @updatedAt)\n `),\n updateConversation: db.prepare(`\n UPDATE conversations\n SET title = @title, updated_at = @updatedAt\n WHERE id = @id AND user_id = @userId\n `),\n getConversation: db.prepare(`\n SELECT * FROM conversations WHERE id = ? AND user_id = ? LIMIT 1\n `),\n listConversations: db.prepare(`\n SELECT id, title, updated_at AS updatedAt\n FROM conversations\n WHERE user_id = ?\n ORDER BY updated_at DESC\n LIMIT 30\n `),\n insertMessage: db.prepare(`\n INSERT INTO messages (\n id, conversation_id, role, content,\n prompt_tokens, completion_tokens, total_tokens, created_at\n )\n VALUES (\n @id, @conversationId, @role, @content,\n @promptTokens, @completionTokens, @totalTokens, @createdAt\n )\n `),\n listMessages: db.prepare(`\n SELECT role, content, prompt_tokens AS promptTokens,\n completion_tokens AS completionTokens, total_tokens AS totalTokens,\n created_at AS createdAt\n FROM messages\n WHERE conversation_id = ?\n ORDER BY created_at ASC\n `),\n getUsage: db.prepare(`\n SELECT total_tokens AS totalTokens FROM usage_months WHERE user_id = ? AND month = ?\n `),\n upsertUsage: db.prepare(`\n INSERT INTO usage_months (user_id, month, total_tokens, updated_at)\n VALUES (@userId, @month, @totalTokens, @updatedAt)\n ON CONFLICT(user_id, month)\n DO UPDATE SET total_tokens = usage_months.total_tokens + excluded.total_tokens,\n updated_at = excluded.updated_at\n `),\n insertResourceDownload: db.prepare(`\n INSERT INTO resource_downloads (\n id, user_id, title, source_name, source_url, file_name, file_path, created_at\n )\n VALUES (\n @id, @userId, @title, @sourceName, @sourceUrl, @fileName, @filePath, @createdAt\n )\n `),\n insertAgentRun: db.prepare(`\n INSERT INTO agent_runs (\n id, user_id, conversation_id, message_id, openclaw_run_id, openclaw_session_key,\n status, mode, provider, model, started_at\n )\n VALUES (\n @id, @userId, @conversationId, @messageId, @openClawRunId, @openClawSessionKey,\n @status, @mode, @provider, @model, @startedAt\n )\n `),\n getAgentRun: db.prepare(`\n SELECT * FROM agent_runs WHERE id = ? AND user_id = ? LIMIT 1\n `),\n updateAgentRunStatus: db.prepare(`\n UPDATE agent_runs\n SET status = @status,\n openclaw_run_id = COALESCE(@openClawRunId, openclaw_run_id),\n input_tokens = COALESCE(@inputTokens, input_tokens),\n output_tokens = COALESCE(@outputTokens, output_tokens),\n tool_call_count = COALESCE(@toolCallCount, tool_call_count),\n completed_at = @completedAt,\n failed_at = @failedAt,\n error_code = @errorCode,\n error_message = @errorMessage\n WHERE id = @id AND user_id = @userId\n `),\n insertAgentFeedback: db.prepare(`\n INSERT INTO agent_message_feedback (id, message_id, user_id, rating, reason, comment, created_at)\n VALUES (@id, @messageId, @userId, @rating, @reason, @comment, @createdAt)\n `),\n};\n\nconst deepseekApiKey = process.env.DEEPSEEK_API_KEY || '';\nconst deepseekBaseUrl = (process.env.DEEPSEEK_BASE_URL || 'https://api.deepseek.com').replace(/\\/+$/, '');\nconst deepseekModel = process.env.DEEPSEEK_MODEL || 'deepseek-chat';\nconst deepseekAvailableModels = Array.from(\n new Set(\n (process.env.DEEPSEEK_MODELS || `${deepseekModel},deepseek-chat,deepseek-reasoner`)\n .split(',')\n .map((item) => normalizeDeepSeekModelName(item))\n .filter(Boolean),\n ),\n);\nconst cbetaApiRoot = (process.env.CBETA_API_ROOT || 'https://api.ombhrum.com/api/cbeta').replace(/\\/+$/, '');\nconst fabushiApiBaseUrl = (process.env.FABUSHI_API_BASE_URL || 'https://api.ombhrum.com').replace(/\\/+$/, '');\nconst memberMonthlyLimit = Number(process.env.MEMBER_MONTHLY_TOKEN_LIMIT || 1_000_000);\nconst freeMonthlyLimit = Number(process.env.FREE_MONTHLY_TOKEN_LIMIT || 1_000);\nconst requireMemberForAi = process.env.REQUIRE_MEMBER_FOR_AI === 'true';\nconst deepseekMaxCompletionTokens = Math.max(1, Number(process.env.DEEPSEEK_MAX_COMPLETION_TOKENS || 1400));\nconst maxResourceTextChars = Number(process.env.MAX_RESOURCE_TEXT_CHARS || 80_000);\nconst openClawRuntimeManifestPath =\n process.env.OPENCLAW_RUNTIME_MANIFEST_PATH || path.join(openClawRuntimeUpdatesDir, 'manifest.json');\nconst openClawRuntimePublicBaseUrl = (process.env.OPENCLAW_RUNTIME_PUBLIC_BASE_URL || '').replace(/\\/+$/, '');\nconst enableLibreChatAgentChat = process.env.ENABLE_LIBRECHAT_AGENT_CHAT === 'true';\nconst libreChatAgentsBaseUrl = (\n process.env.LIBRECHAT_AGENTS_BASE_URL || 'http://127.0.0.1:3080/api/agents/v1'\n).replace(/\\/+$/, '');\nconst libreChatAgentApiKey = process.env.LIBRECHAT_AGENT_API_KEY || '';\nconst libreChatAgentId = process.env.LIBRECHAT_AGENT_ID || '';\nconst enableCodexSdkChat = process.env.ENABLE_CODEX_SDK_CHAT === 'true';\nconst enableOpenClawAgentChat = process.env.ENABLE_OPENCLAW_AGENT_CHAT === 'true';\nconst openClawGatewayUrl = (process.env.OPENCLAW_GATEWAY_URL || 'http://127.0.0.1:18789').replace(/\\/+$/, '');\nconst openClawGatewayToken = process.env.OPENCLAW_GATEWAY_TOKEN || '';\nconst openClawAgentId = process.env.OPENCLAW_AGENT_ID || 'fabushi-public-agent';\nconst openClawRunsEndpoint = process.env.OPENCLAW_RUNS_ENDPOINT || '';\nconst codexDeepSeekProviderId = 'dacheng-deepseek-proxy';\nconst codexResponsesBaseUrl = (\n process.env.CODEX_DEEPSEEK_RESPONSES_BASE_URL ||\n `http://127.0.0.1:${process.env.PORT || 8788}/codex-deepseek/v1`\n).replace(/\\/+$/, '');\nconst codexAdapterTokenPrefix = 'dacheng-codex-proxy.';\nconst codexAdapterTokenTtlMs = Math.max(\n 60_000,\n Number(process.env.CODEX_DEEPSEEK_ADAPTER_TOKEN_TTL_MS || 15 * 60_000),\n);\nconst codexAdapterSecret =\n process.env.CODEX_DEEPSEEK_ADAPTER_SECRET ||\n deepseekApiKey ||\n openClawGatewayToken ||\n 'dacheng-codex-local-dev-secret';\n\nconst systemPrompt = [\n '你是“大乘”App 的 AI 助手。',\n '你的核心任务是帮助用户查找、整理、理解并全球法布施合法可分享的佛法资源。',\n '回答要庄重、简洁、可执行;涉及经典、仪轨或资源时,提醒用户尊重版权、来源和当地法规。',\n '如果用户想找资源、下载经文、寻找音频或准备可分享资料,你必须先在后端自动执行搜索、验证和下载步骤;不要要求用户去前端 + 菜单手动查找。',\n '你可以把执行进度总结给用户,但不要输出隐藏推理原文。',\n].join('\\n');\n\nfunction nowIso() {\n return new Date().toISOString();\n}\n\nfunction monthKey(date = new Date()) {\n return `${date.getUTCFullYear()}-${String(date.getUTCMonth() + 1).padStart(2, '0')}`;\n}\n\nfunction jsonResponse(res, status, payload) {\n res.status(status).json(payload);\n}\n\nasync function readJsonFile(filePath) {\n const raw = await fs.promises.readFile(filePath, 'utf8');\n return JSON.parse(raw);\n}\n\nfunction requestOrigin(req) {\n if (openClawRuntimePublicBaseUrl) return openClawRuntimePublicBaseUrl;\n const proto = req.get('X-Forwarded-Proto') || req.protocol || 'https';\n const host = req.get('X-Forwarded-Host') || req.get('Host');\n return host ? `${proto}://${host}` : '';\n}\n\nfunction absolutizeOpenClawRuntimeUrl(req, value) {\n const text = readText(value);\n if (!text) return '';\n if (/^https?:\\/\\//i.test(text)) return text;\n const normalized = text.startsWith('/')\n ? text\n : `/api/openclaw/runtime/files/${text.replace(/^\\/+/, '')}`;\n const origin = requestOrigin(req);\n return origin ? new URL(normalized, `${origin}/`).toString() : normalized;\n}\n\nfunction textToolResult(text, structuredContent = {}) {\n return {\n structuredContent,\n content: [{ type: 'text', text }],\n };\n}\n\nfunction sseHeaders(res) {\n res.status(200);\n res.setHeader('Content-Type', 'text/event-stream; charset=utf-8');\n res.setHeader('Cache-Control', 'no-cache, no-transform');\n res.setHeader('Connection', 'keep-alive');\n res.setHeader('X-Accel-Buffering', 'no');\n if (typeof res.flushHeaders === 'function') res.flushHeaders();\n}\n\nfunction writeSse(res, event, payload) {\n res.write(`event: ${event}\\n`);\n res.write(`data: ${JSON.stringify(payload)}\\n\\n`);\n}\n\nfunction asyncHandler(fn) {\n return (req, res, next) => {\n Promise.resolve(fn(req, res, next)).catch(next);\n };\n}\n\nfunction bearerToken(req) {\n const delegated = readText(req.get('x-dacheng-auth-token'));\n if (\n delegated &&\n delegated !== 'DACHENG_AUTH_TOKEN' &&\n !delegated.startsWith('secretref-env:') &&\n delegated !== 'dacheng-openclaw-proxy'\n ) {\n return delegated;\n }\n const header = req.get('Authorization') || '';\n if (!header.startsWith('Bearer ')) return '';\n const token = header.slice('Bearer '.length).trim();\n return token === 'dacheng-openclaw-proxy' ? '' : token;\n}\n\nfunction sha256(value) {\n return crypto.createHash('sha256').update(value).digest('hex');\n}\n\nfunction safeUserText(value, fallback = '') {\n if (typeof value !== 'string') return fallback;\n return value.replace(/\\u0000/g, '').trim();\n}\n\nfunction estimateTokens(text) {\n const normalized = String(text || '').trim();\n if (!normalized) return 0;\n return Math.max(1, Math.ceil(normalized.length / 3));\n}\n\nfunction readText(value) {\n if (value === undefined || value === null) return '';\n return String(value).replace(/\\u0000/g, '').trim();\n}\n\nconst miniAppHostApiVersion = '2.0';\nconst miniAppHighRiskPermissions = new Set([\n 'auth.token',\n 'payments.alipay',\n 'payments.fudeGold',\n 'desktop.control',\n 'network.http',\n 'network.udp',\n 'network.interfaces',\n 'system.keepAwake',\n 'hotspot.settings',\n 'local.loopback',\n 'fs.readWrite',\n 'runtime.process',\n 'shell.execute',\n 'browser.external',\n 'files.write',\n 'projects.write',\n 'openclaw.restart',\n]);\n\nfunction officialMiniAppRegistry(req) {\n const webBase = process.env.MINIAPP_WEB_BASE_URL || 'https://fabushi.ombhrum.com';\n const official = [\n {\n botId: 'bot.global-dharma',\n miniAppId: 'official.global-dharma',\n title: '全球法布施',\n subtitle: '地区、循环、场能都在对话框上方设置',\n initials: '法',\n icon: 'public',\n avatarColor: '#4CAF7A',\n kind: 'global_dharma',\n entryUrl: process.env.GLOBAL_DHARMA_MINIAPP_URL,\n greeting: '把要分享的文字、链接或素材发给我,我会按上方选择的地区与模式启动全球法布施。',\n inputHint: '输入要法布施的文字/链接,或点 + 添加素材',\n permissions: [\n 'app.context',\n 'bot.chat',\n 'auth.session',\n 'wallet.balance',\n 'payments.entitlement',\n 'payments.fudeGold',\n 'payments.alipay',\n 'files.pick',\n 'network.http',\n 'network.udp',\n 'network.interfaces',\n 'system.keepAwake',\n 'hotspot.settings',\n 'local.loopback',\n 'openclaw.status',\n 'openclaw.chat',\n 'desktop.control',\n 'fs.readWrite',\n 'runtime.process',\n ],\n },\n {\n botId: 'bot.flashcards',\n miniAppId: 'official.flashcards',\n title: '背诵闪卡制作',\n subtitle: '随机挖空 / AI 制卡从顶部模式按钮选择',\n initials: '卡',\n icon: 'flashcards',\n avatarColor: '#7E57C2',\n kind: 'flashcards',\n entryUrl: process.env.FLASHCARDS_MINIAPP_URL,\n greeting: '粘贴经文、文章正文或链接即可制作背诵闪卡。制卡模式请在上方按钮切换。',\n inputHint: '粘贴链接或正文,发送后制作闪卡',\n permissions: ['app.context', 'bot.chat', 'flashcards.create', 'openclaw.status', 'openclaw.chat'],\n },\n {\n botId: 'bot.platform-publish',\n miniAppId: 'official.platform-publish',\n title: '法布施到平台',\n subtitle: '选择平台后生成发布草稿并打开入口',\n initials: '发',\n icon: 'publish',\n avatarColor: '#FF9F43',\n kind: 'platform_publish',\n entryUrl: process.env.PLATFORM_PUBLISH_MINIAPP_URL,\n greeting: '把要发布的正文或链接发给我,上方选择平台后,我会生成发布草稿并打开对应平台入口。',\n inputHint: '输入要发布到平台的正文/链接',\n permissions: [\n 'app.context',\n 'bot.chat',\n 'platform.publish',\n 'files.pick',\n 'fs.readWrite',\n 'runtime.process',\n 'shell.execute',\n 'browser.external',\n ],\n },\n {\n botId: 'bot.father',\n miniAppId: 'official.bot-father',\n title: '机器人之父',\n subtitle: '用对话生成个人沙箱小程序',\n initials: '父',\n icon: 'bot_father',\n avatarColor: '#3D8BFF',\n kind: 'bot_father',\n entryUrl: process.env.BOT_FATHER_MINIAPP_URL,\n greeting: '告诉我你想要的小程序,我会生成 manifest、界面和权限声明,并放进你的个人沙箱。',\n inputHint: '描述你想创建的小程序',\n permissions: ['app.context', 'bot.chat', 'miniapps.dev'],\n },\n ];\n return {\n bots: official.map((bot, index) => ({\n ...bot,\n destinationIndex: index,\n source: 'official',\n })),\n miniApps: official.map((bot) => ({\n miniAppId: bot.miniAppId,\n botId: bot.botId,\n title: bot.title,\n subtitle: bot.subtitle,\n entryUrl:\n bot.entryUrl ||\n `${webBase.replace(/\\/+$/, '')}/miniapps/${encodeURIComponent(bot.miniAppId)}`,\n version: '2.0.0',\n permissions: bot.permissions,\n surfaces: ['homePinned', 'chatPanel'],\n theme: 'telegramDark',\n signature: 'official',\n reviewStatus: 'trusted',\n source: 'official',\n })),\n };\n}\n\nasync function readMiniAppStore() {\n try {\n const raw = await fs.promises.readFile(miniAppsStorePath, 'utf8');\n const decoded = JSON.parse(raw);\n return {\n apps: Array.isArray(decoded.apps) ? decoded.apps : [],\n };\n } catch (error) {\n if (error?.code === 'ENOENT') return { apps: [] };\n throw error;\n }\n}\n\nasync function writeMiniAppStore(store) {\n await fs.promises.mkdir(miniAppsDir, { recursive: true });\n await fs.promises.writeFile(\n miniAppsStorePath,\n JSON.stringify({ apps: store.apps || [] }, null, 2),\n );\n}\n\nfunction miniAppEntryUrl(req, miniAppId) {\n const origin = requestOrigin(req) || '';\n const pathName = `/api/miniapps/dev/${encodeURIComponent(miniAppId)}/index.html`;\n return origin ? new URL(pathName, `${origin}/`).toString() : pathName;\n}\n\nfunction sanitizeMiniAppPermissions(value, { allowHighRisk = false } = {}) {\n const raw = Array.isArray(value) ? value : [];\n const normalized = raw\n .map((item) => readText(item).toLowerCase())\n .filter((item) => /^[a-z][a-z0-9_.:-]{0,64}$/.test(item));\n const deduped = [...new Set(['app.context', 'bot.chat', ...normalized])];\n return deduped.filter((permission) => allowHighRisk || !miniAppHighRiskPermissions.has(permission));\n}\n\nfunction miniAppScan(permissions, sourceHtml = '') {\n const highRisk = permissions.filter((permission) => miniAppHighRiskPermissions.has(permission));\n const blockedPatterns = [\n /authorization\\s*:\\s*['\"`]bearer/i,\n /desktop_control_bridge_token/i,\n /openclaw_gateway_token/i,\n /eval\\s*\\(/i,\n ];\n const blocked = blockedPatterns\n .filter((pattern) => pattern.test(sourceHtml))\n .map((pattern) => pattern.toString());\n return {\n passed: blocked.length === 0,\n highRiskPermissions: highRisk,\n blockedPatterns: blocked,\n };\n}\n\nfunction inferMiniAppPermissionsFromText(...values) {\n const text = values.map((value) => readText(value)).join('\\n').toLowerCase();\n const rules = [\n ['auth.session', [/auth\\.(getsession|requirelogin)/, /登录|用户信息|宿主账号/]],\n ['auth.token', [/auth\\.getaccesstoken/, /access token|访问 token/]],\n ['payments.alipay', [/payments\\.alipay/, /支付宝|支付|购买|订单/]],\n ['payments.entitlement', [/payments\\.checkentitlement|权益|解锁状态|购买状态/]],\n ['payments.fudeGold', [/payments\\.requestpayment|福德金|代币|钱包|余额/]],\n ['wallet.balance', [/wallet\\.getbalance|福德金|代币|钱包|余额/]],\n ['network.http', [/network\\.http|http\\.fetch|fetch\\s*\\(/, /链接正文|抓取网页|http 请求|网页内容/]],\n ['network.udp', [/network\\.udp|udp|datagram/, /全球法布施|法布施发送|数据报/]],\n ['network.interfaces', [/network\\.interfaces|网卡|网络接口|广播地址/]],\n ['system.keepAwake', [/system\\.keepawake|保持唤醒|后台任务|keep awake/]],\n ['hotspot.settings', [/hotspot\\.opensettings|热点|本地场能/]],\n ['local.loopback', [/localloopback\\./, /localhost|127\\.0\\.0\\.1|本地回环|本地转经轮/]],\n ['flashcards.create', [/flashcards\\./, /闪卡|背诵卡|挖空/]],\n ['platform.publish', [/platformpublish\\./, /发布草稿|发布到平台|公众号|小红书|知乎/]],\n ['files.pick', [/files\\.pick/, /选择文件|上传文件|本地素材/]],\n ['fs.readWrite', [/fs\\.(writefile|readfile)/, /本地文件|写文件|读文件|文件处理/]],\n ['runtime.process', [/runtime\\.process|process\\.execute/, /本地进程|执行程序|命令行|cli|playwright/]],\n ['shell.execute', [/shell\\.execute/, /shell/]],\n ['browser.external', [/browser\\.open/, /浏览器|打开网页|打开平台/]],\n ['desktop.control', [/desktopcontrol\\./, /桌面控制|点击|键盘|鼠标/]],\n ['openclaw.chat', [/openclaw\\.chat/, /openclaw/]],\n ];\n return rules\n .filter(([, patterns]) => patterns.some((pattern) => pattern.test(text)))\n .map(([permission]) => permission);\n}\n\nfunction miniAppRecordToBot(record) {\n return {\n botId: record.botId,\n miniAppId: record.miniAppId,\n title: record.title,\n subtitle: record.subtitle,\n initials: record.initials || '小',\n icon: record.icon || 'code',\n avatarColor: record.avatarColor || '#3D8BFF',\n kind: 'third_party',\n greeting: record.greeting || '你好,我是这个小程序的机器人。',\n inputHint: record.inputHint || '输入消息,或打开小程序面板',\n permissions: record.permissions || ['app.context', 'bot.chat'],\n source: record.source || 'sandbox',\n };\n}\n\nfunction miniAppRecordToManifest(req, record) {\n return {\n miniAppId: record.miniAppId,\n botId: record.botId,\n title: record.title,\n subtitle: record.subtitle,\n entryUrl: miniAppEntryUrl(req, record.miniAppId),\n version: record.version || '0.0.1',\n permissions: record.permissions || ['app.context', 'bot.chat'],\n surfaces: record.surfaces || ['chatPanel'],\n theme: record.theme || 'telegramDark',\n signature: record.signature || `sandbox:${record.ownerId}`,\n reviewStatus: record.reviewStatus || 'sandbox',\n source: record.source || 'sandbox',\n };\n}\n\nfunction safeMiniAppId(value) {\n return readText(value).replace(/[^a-zA-Z0-9_.-]/g, '-').slice(0, 80);\n}\n\nfunction titleFromPromptForMiniApp(prompt) {\n const text = readText(prompt).replace(/\\s+/g, ' ');\n if (!text) return '个人小程序';\n return text.length <= 18 ? text : text.slice(0, 18);\n}\n\nfunction escapeHtml(value) {\n return String(value || '')\n .replace(/&/g, '&')\n .replace(/</g, '<')\n .replace(/>/g, '>')\n .replace(/\"/g, '"')\n .replace(/'/g, ''');\n}\n\nfunction generatedMiniAppHtml({ title, prompt }) {\n const safeTitle = escapeHtml(title);\n const safePrompt = escapeHtml(prompt);\n const promptJson = JSON.stringify(readText(prompt));\n return `<!doctype html>\n<html lang=\"zh-Hans\">\n<head>\n <meta charset=\"utf-8\" />\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n <title>${safeTitle}</title>\n <style>\n body { margin:0; font-family:-apple-system,BlinkMacSystemFont,\"Segoe UI\",sans-serif; background:#0f1722; color:#fff; }\n main { padding:20px; display:grid; gap:14px; }\n .card { border:1px solid #263445; background:#17212b; border-radius:16px; padding:16px; }\n button { border:0; border-radius:999px; padding:10px 14px; background:#3390ec; color:white; font-weight:800; }\n pre { white-space:pre-wrap; color:#9ec7ff; }\n </style>\n</head>\n<body>\n <main>\n <section class=\"card\">\n <h1>${safeTitle}</h1>\n <p>这是机器人之父根据你的描述生成的个人沙箱小程序。</p>\n <pre>${safePrompt}</pre>\n </section>\n <section class=\"card\">\n <button id=\"context\">读取宿主上下文</button>\n <button id=\"spec\">读取宿主能力</button>\n <button id=\"chat\">让机器人解释这个小程序</button>\n <pre id=\"output\">等待操作...</pre>\n </section>\n </main>\n <script>\n const out = document.getElementById('output');\n async function invoke(method, params) {\n if (!window.FabushiMiniApp) throw new Error('FabushiMiniApp host SDK not ready');\n const result = await window.FabushiMiniApp.invoke(method, params || {});\n out.textContent = JSON.stringify(result, null, 2);\n }\n document.getElementById('context').onclick = () => invoke('app.getContext');\n document.getElementById('spec').onclick = () => invoke('app.getHostApiSpec');\n const prompt = ${promptJson};\n document.getElementById('chat').onclick = () => invoke('bot.sendMessage', {\n message: '请简要说明这个小程序能做什么:' + prompt\n });\n const seenCommands = new Set();\n function deliverMiniAppCommand(detail) {\n if (!detail || typeof detail !== 'object') return;\n const commandKey = detail.commandId || detail.id || [detail.createdAt, detail.command, detail.rawText || detail.text].filter(Boolean).join(':');\n if (commandKey) {\n if (seenCommands.has(commandKey)) return;\n seenCommands.add(commandKey);\n }\n handleMiniAppCommand(detail);\n }\n async function handleMiniAppCommand(detail) {\n out.textContent = '收到后台命令:' + JSON.stringify(detail, null, 2);\n try {\n await invoke('bot.postMessage', {\n commandId: detail.commandId || detail.id,\n message: '「${safeTitle}」已收到后台命令:' + (detail.args || detail.text || '')\n });\n await invoke('bot.reportCommandResult', {\n commandId: detail.commandId || detail.id,\n status: 'completed',\n message: '「${safeTitle}」后台命令已处理。'\n });\n } catch (error) {\n out.textContent = String(error && error.message || error);\n }\n }\n window.addEventListener('fabushi-miniapp-command', (event) => {\n deliverMiniAppCommand(event.detail || {});\n });\n if (window.__fabushiLastMiniAppCommand) {\n const cmd = window.__fabushiLastMiniAppCommand;\n window.__fabushiLastMiniAppCommand = null;\n (window.queueMicrotask || ((fn) => setTimeout(fn, 0)))(() => {\n deliverMiniAppCommand(cmd);\n });\n }\n </script>\n</body>\n</html>`;\n}\n\nfunction extractMiniAppHtml(value) {\n const text = readText(value);\n if (!text) return '';\n const fenced = text.match(/```(?:html)?\\s*([\\s\\S]*?)```/i);\n const candidate = (fenced ? fenced[1] : text).trim();\n if (!/<html[\\s>]/i.test(candidate) || !/<script[\\s>]/i.test(candidate)) return '';\n return candidate.startsWith('<!doctype') || candidate.startsWith('<html')\n ? candidate\n : `<!doctype html>\\n${candidate}`;\n}\n\nasync function generateBotFatherMiniAppHtml({ title, prompt, user }) {\n const fallback = generatedMiniAppHtml({ title, prompt });\n try {\n const result = await runAgentModel({\n user,\n mode: 'miniapp_generation',\n maxCompletionTokens: deepseekMaxCompletionTokens,\n messages: [\n {\n role: 'system',\n content: [\n '你是 Fabushi App 的小程序生成器。',\n '只输出一个完整静态 HTML 文件,不要 Markdown,不要解释。',\n '小程序只能通过 window.FabushiMiniApp.invoke(method, params) 调用宿主能力。',\n '先调用 app.getHostApiSpec 或 app.getCapabilities 判断宿主版本和已授权权限,再显示对应 UI。',\n '聊天后台调用协议:优先使用 window.FabushiMiniApp.bot.onCommand(\"/start\", handler) 或 bot.onAnyCommand(handler);如果手写监听 window 的 fabushi-miniapp-command 事件,注册后也要读取 window.__fabushiLastMiniAppCommand 并按 commandId 去重补处理。detail.command 默认为 /start,detail.args 是用户文本;处理完成后调用 bot.postMessage 或 bot.reportCommandResult 把进度/结果写回聊天框。',\n '标准能力:auth.getSession/auth.requireLogin 复用宿主登录;payments.requestPayment 使用福德金支付并由宿主登记权益;payments.checkEntitlement 查询商品是否已解锁;payments.alipay.pay/createOrder 仅作为宿主官方现金支付兜底;network.http.fetch/network.udp/network.interfaces 提供 HTTP、UDP 和网卡原语;system.keepAwake/hotspot.openSettings 提供系统级辅助;flashcards.createDeck 接入背诵闪卡;platformPublish.createDraft 接入发布草稿;fs.writeFile/fs.readFile、runtime.process.execute、browser.open 用于本地文件、进程和浏览器。',\n '高危权限包括 auth.token、payments.alipay、payments.fudeGold、network.http、network.udp、network.interfaces、system.keepAwake、hotspot.settings、local.loopback、fs.readWrite、runtime.process、shell.execute、browser.external、desktop.control;只有用户需求明确需要时才使用。',\n '禁止外链脚本、eval、Authorization token、桌面桥 token、OpenClaw token。',\n '不要调用 auth.getAccessToken,除非用户明确要求对接需要 token 的受信服务。',\n '优先实现一个可点击、可立即使用的小面板,包含 app.getContext 或 bot.sendMessage 示例。',\n ].join('\\n'),\n },\n {\n role: 'user',\n content: [\n `小程序标题:${title}`,\n `用户需求:${prompt}`,\n '请生成 HTML/CSS/JS 单文件。',\n ].join('\\n'),\n },\n ],\n });\n const sourceHtml = extractMiniAppHtml(result.message);\n if (!sourceHtml) {\n return {\n sourceHtml: fallback,\n generation: { provider: 'template', fallbackReason: 'ai_output_not_html' },\n };\n }\n return {\n sourceHtml,\n generation: {\n provider: result.provider || 'ai',\n model: result.model || '',\n },\n };\n } catch (error) {\n return {\n sourceHtml: fallback,\n generation: {\n provider: 'template',\n fallbackReason: readText(error?.message || error),\n },\n };\n }\n}\n\nfunction clientContextMessage(client) {\n if (!client || typeof client !== 'object') return '';\n const selectedModel = readText(client.selectedModel || client.model);\n const project = client.project && typeof client.project === 'object' ? client.project : {};\n const projectName = readText(project.name);\n const projectPath = readText(project.path);\n const lines = ['桌面端上下文:'];\n if (selectedModel) lines.push(`用户选择的 DeepSeek 模型:${normalizeDeepSeekModelName(selectedModel)}`);\n if (projectPath) {\n lines.push(`当前项目名称:${projectName || projectPath}`);\n lines.push(`当前项目路径:${projectPath}`);\n lines.push('如果本轮任务涉及读取、修改、运行、分析或整理项目,请优先围绕当前项目路径工作;不要擅自操作其他目录。');\n }\n return lines.length > 1 ? lines.join('\\n') : '';\n}\nfunction unwrapPayload(payload) {\n if (!payload || typeof payload !== 'object') return {};\n return payload.data && typeof payload.data === 'object' ? payload.data : payload;\n}\n\nfunction readFirstText(values) {\n for (const value of values) {\n const text = readText(value);\n if (text) return text;\n }\n return '';\n}\n\nfunction activeFlag(value) {\n if (value === true) return true;\n if (value === 1) return true;\n if (typeof value !== 'string') return false;\n return ['true', '1', 'yes', 'active'].includes(value.trim().toLowerCase());\n}\n\nfunction futureDate(value) {\n const text = readText(value);\n if (!text) return false;\n const timestamp = Date.parse(text);\n return Number.isFinite(timestamp) && timestamp > Date.now();\n}\n\nfunction normalizeMembershipPayload(payload) {\n if (!payload || typeof payload !== 'object') return false;\n const data = unwrapPayload(payload);\n const membership = data.membership && typeof data.membership === 'object' ? data.membership : data;\n const type = readText(membership.type || membership.membershipType || data.membershipType).toLowerCase();\n const status = readText(membership.status || data.status).toLowerCase();\n const expiry = membership.expiresAt ||\n membership.expires_at ||\n membership.membershipExpiry ||\n membership.membership_expires_at ||\n data.membershipExpiry ||\n data.membership_expires_at;\n const hasExplicitActiveFlag =\n membership.isActive !== undefined ||\n membership.active !== undefined ||\n membership.hasPremiumAccess !== undefined ||\n membership.hasPremiumMembership !== undefined ||\n membership.isPremium !== undefined;\n\n if (hasExplicitActiveFlag) {\n return Boolean(\n activeFlag(membership.isActive) ||\n activeFlag(membership.active) ||\n activeFlag(membership.hasPremiumAccess) ||\n activeFlag(membership.hasPremiumMembership) ||\n activeFlag(membership.isPremium),\n );\n }\n\n return Boolean(\n status === 'active' ||\n type === 'paid' ||\n type === 'premium' ||\n type === 'trial' ||\n futureDate(expiry),\n );\n}\n\nfunction extractRemoteAccount(payload) {\n const data = unwrapPayload(payload);\n const nestedUser = data.user && typeof data.user === 'object' ? data.user : {};\n const id = readFirstText([\n data.userId,\n data.user_id,\n data.userNo,\n data.user_no,\n data.id,\n nestedUser.userId,\n nestedUser.user_id,\n nestedUser.userNo,\n nestedUser.user_no,\n nestedUser.id,\n ]);\n const username = readFirstText([data.username, data.name, nestedUser.username, nestedUser.name]);\n const email = readFirstText([data.email, nestedUser.email]);\n const membership = data.membership && typeof data.membership === 'object' ? data.membership : null;\n return {\n authenticated: Boolean(id || username || email || membership || data.success === true),\n id,\n username,\n email,\n isMember: normalizeMembershipPayload(payload),\n membership,\n };\n}\n\nasync function resolveRemoteAccount(token) {\n const fallback = {\n authenticated: false,\n id: '',\n username: '',\n email: '',\n isMember: false,\n membership: null,\n };\n if (!token || !fabushiApiBaseUrl) return fallback;\n\n const endpoints = [\n '/api/stripe/membership-status',\n '/api/alipay/check-membership',\n '/api/auth/user-info',\n ];\n\n for (const endpoint of endpoints) {\n try {\n const response = await fetch(`${fabushiApiBaseUrl}${endpoint}`, {\n headers: {\n Accept: 'application/json',\n Authorization: `Bearer ${token}`,\n },\n signal: AbortSignal.timeout(8_000),\n });\n if (response.status === 401 || response.status === 403) return fallback;\n if (!response.ok) continue;\n const payload = await response.json();\n const account = extractRemoteAccount(payload);\n if (account.authenticated || account.isMember) {\n return {\n ...fallback,\n ...account,\n authenticated: true,\n };\n }\n } catch (error) {\n logger.debug({ endpoint, error: String(error) }, 'membership lookup skipped');\n }\n }\n\n return fallback;\n}\n\nasync function resolveUser(req, body = {}) {\n const token = bearerToken(req);\n const usernameHint = safeUserText(body.username || req.query.username);\n\n const testToken = process.env.TEST_ACCOUNT_TOKEN || 'dacheng-test-token-bypass-limits';\n if (token && token === testToken) {\n return {\n userId: 'user:test_account',\n username: 'TestAccount',\n tokenHash: sha256(token).slice(0, 24),\n isAuthenticated: true,\n isMember: true,\n isTestAccount: true,\n membership: { type: 'lifetime', active: true },\n };\n }\n\n const tokenHash = token ? sha256(token).slice(0, 24) : '';\n const anonymousHash = sha256(`${req.ip}|${req.get('User-Agent') || ''}`).slice(0, 24);\n const remoteAccount = await resolveRemoteAccount(token);\n const remoteIdentity = remoteAccount.id ||\n remoteAccount.username ||\n remoteAccount.email ||\n (remoteAccount.authenticated ? tokenHash : '');\n const userId = remoteAccount.authenticated\n ? `user:${remoteIdentity}`\n : tokenHash\n ? `token:${tokenHash}`\n : `anon:${anonymousHash}`;\n return {\n userId,\n username: remoteAccount.username || usernameHint,\n tokenHash,\n isAuthenticated: remoteAccount.authenticated,\n isMember: remoteAccount.isMember,\n membership: remoteAccount.membership,\n };\n}\n\nfunction base64UrlJson(value) {\n return Buffer.from(JSON.stringify(value), 'utf8').toString('base64url');\n}\n\nfunction timingSafeEqualText(left, right) {\n const leftBuffer = Buffer.from(String(left || ''));\n const rightBuffer = Buffer.from(String(right || ''));\n if (leftBuffer.length !== rightBuffer.length) return false;\n return crypto.timingSafeEqual(leftBuffer, rightBuffer);\n}\n\nfunction signCodexAdapterPayload(encodedPayload) {\n return crypto\n .createHmac('sha256', codexAdapterSecret)\n .update(encodedPayload)\n .digest('base64url');\n}\n\nfunction createCodexAdapterToken(user, options = {}) {\n const payload = {\n sub: user.userId,\n username: user.username || '',\n tokenHash: user.tokenHash || '',\n isAuthenticated: Boolean(user.isAuthenticated),\n isMember: Boolean(user.isMember),\n isTestAccount: Boolean(user.isTestAccount),\n membership: user.membership || {},\n recordUsage: options.recordUsage !== false,\n exp: Date.now() + codexAdapterTokenTtlMs,\n };\n const encodedPayload = base64UrlJson(payload);\n return `${codexAdapterTokenPrefix}${encodedPayload}.${signCodexAdapterPayload(encodedPayload)}`;\n}\n\nfunction resolveCodexAdapterToken(token) {\n const raw = readText(token);\n if (!raw.startsWith(codexAdapterTokenPrefix)) return null;\n\n const signed = raw.slice(codexAdapterTokenPrefix.length);\n const separator = signed.lastIndexOf('.');\n if (separator < 1) return null;\n\n const encodedPayload = signed.slice(0, separator);\n const signature = signed.slice(separator + 1);\n const expectedSignature = signCodexAdapterPayload(encodedPayload);\n if (!timingSafeEqualText(signature, expectedSignature)) return null;\n\n let payload;\n try {\n payload = JSON.parse(Buffer.from(encodedPayload, 'base64url').toString('utf8'));\n } catch {\n return null;\n }\n\n if (!payload || typeof payload !== 'object') return null;\n if (Number(payload.exp || 0) < Date.now()) return null;\n\n const userId = safeUserText(payload.sub);\n if (!userId) return null;\n\n return {\n user: {\n userId,\n username: safeUserText(payload.username),\n tokenHash: safeUserText(payload.tokenHash),\n isAuthenticated: Boolean(payload.isAuthenticated),\n isMember: Boolean(payload.isMember),\n isTestAccount: Boolean(payload.isTestAccount),\n membership:\n payload.membership && typeof payload.membership === 'object'\n ? payload.membership\n : {},\n },\n recordUsage: payload.recordUsage !== false,\n };\n}\n\nasync function resolveCodexResponsesBilling(req) {\n const token = bearerToken(req);\n const adapter = resolveCodexAdapterToken(token);\n if (adapter) return adapter;\n\n if (deepseekApiKey && token === deepseekApiKey) {\n const error = new Error('Codex DeepSeek adapter now requires first-party user billing');\n error.statusCode = 401;\n throw error;\n }\n\n return {\n user: await resolveUser(req, req.body || {}),\n recordUsage: true,\n };\n}\n\nfunction usageFor(userId) {\n const month = monthKey();\n const row = statements.getUsage.get(userId, month);\n return {\n month,\n used: Number(row?.totalTokens || 0),\n };\n}\n\nfunction enforceTokenBudget(user, estimatedTokensForRequest) {\n if (user && user.isTestAccount) {\n return { limit: 999999999, usage: { month: monthKey(), used: 0 } };\n }\n if (requireMemberForAi && !user.isMember) {\n const error = new Error('大乘 AI 需要开通会员后使用');\n error.statusCode = 403;\n error.code = 'AI_MEMBERSHIP_REQUIRED';\n error.details = {\n requireMemberForAi,\n monthlyLimit: memberMonthlyLimit,\n remainingTokens: 0,\n };\n throw error;\n }\n\n const limit = user.isMember ? memberMonthlyLimit : freeMonthlyLimit;\n const usage = usageFor(user.userId);\n if (usage.used + estimatedTokensForRequest > limit) {\n const error = new Error('本月 AI token 额度已不足');\n error.statusCode = 429;\n error.code = 'USER_TOKEN_QUOTA_EXCEEDED';\n error.details = {\n monthlyLimit: limit,\n usedTokens: usage.used,\n remainingTokens: Math.max(0, limit - usage.used),\n };\n throw error;\n }\n return { limit, usage };\n}\n\nfunction completionBudgetFor(budget, estimatedPromptTokens) {\n const remainingAfterPrompt = budget.limit - budget.usage.used - estimatedPromptTokens;\n return Math.max(1, Math.min(deepseekMaxCompletionTokens, Math.floor(remainingAfterPrompt)));\n}\n\nfunction recordUsage(userId, totalTokens) {\n if (!totalTokens || totalTokens < 1) return;\n statements.upsertUsage.run({\n userId,\n month: monthKey(),\n totalTokens,\n updatedAt: nowIso(),\n });\n}\n\nfunction titleFromMessage(message) {\n const compact = message.replace(/\\s+/g, ' ').trim();\n if (!compact) return '新对话';\n return compact.length > 22 ? `${compact.slice(0, 22)}...` : compact;\n}\n\nfunction normalizeMessages(rows) {\n return rows.map((row) => ({\n role: row.role,\n content: row.content,\n }));\n}\n\nfunction normalizeMaxCompletionTokens(value) {\n const numeric = Number(value || deepseekMaxCompletionTokens);\n if (!Number.isFinite(numeric) || numeric < 1) return deepseekMaxCompletionTokens;\n return Math.max(1, Math.min(deepseekMaxCompletionTokens, Math.floor(numeric)));\n}\n\nfunction normalizeDeepSeekModelName(value) {\n const raw = readText(value || deepseekModel);\n if (!raw) return deepseekModel;\n const modelId = raw.includes('/') ? raw.split('/').pop() : raw;\n return modelId || deepseekModel;\n}\n\nasync function callDeepSeek(messages, options = {}) {\n if (!deepseekApiKey) {\n const error = new Error('DeepSeek API key is not configured');\n error.statusCode = 503;\n throw error;\n }\n\n const model = normalizeDeepSeekModelName(options.model);\n const response = await fetch(`${deepseekBaseUrl}/chat/completions`, {\n method: 'POST',\n headers: {\n Accept: 'application/json',\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${deepseekApiKey}`,\n },\n body: JSON.stringify({\n model,\n messages,\n temperature: 0.4,\n max_tokens: normalizeMaxCompletionTokens(options.maxCompletionTokens),\n stream: false,\n }),\n signal: AbortSignal.timeout(60_000),\n });\n\n const bodyText = await response.text();\n let payload;\n try {\n payload = bodyText ? JSON.parse(bodyText) : {};\n } catch {\n payload = { error: bodyText };\n }\n\n if (!response.ok) {\n const upstreamMessage = payload?.error?.message || payload?.message || '';\n const friendlyMessage =\n response.status === 402 || /insufficient\\s+balance/i.test(upstreamMessage)\n ? '大乘 AI 服务额度暂不可用,请联系管理员充值 DeepSeek 账户后重试。'\n : upstreamMessage || `DeepSeek request failed: ${response.status}`;\n const error = new Error(friendlyMessage);\n error.statusCode = response.status;\n if (response.status === 402 || /insufficient\\s+balance/i.test(upstreamMessage)) {\n error.code = 'DEEPSEEK_PROVIDER_QUOTA';\n }\n throw error;\n }\n\n const message = payload?.choices?.[0]?.message?.content?.trim();\n if (!message) {\n const error = new Error('DeepSeek returned an empty response');\n error.statusCode = 502;\n throw error;\n }\n\n return {\n message,\n model,\n usage: {\n promptTokens: Number(payload?.usage?.prompt_tokens || 0),\n completionTokens: Number(payload?.usage?.completion_tokens || 0),\n totalTokens: Number(payload?.usage?.total_tokens || 0),\n },\n };\n}\n\nasync function callDeepSeekStream(messages, callbacks = {}) {\n if (!deepseekApiKey) {\n const error = new Error('DeepSeek API key is not configured');\n error.statusCode = 503;\n throw error;\n }\n\n const model = normalizeDeepSeekModelName(callbacks.model);\n const response = await fetch(`${deepseekBaseUrl}/chat/completions`, {\n method: 'POST',\n headers: {\n Accept: 'text/event-stream',\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${deepseekApiKey}`,\n },\n body: JSON.stringify({\n model,\n messages,\n temperature: 0.4,\n max_tokens: normalizeMaxCompletionTokens(callbacks.maxCompletionTokens),\n stream: true,\n stream_options: { include_usage: true },\n }),\n signal: callbacks.signal || AbortSignal.timeout(90_000),\n });\n\n if (!response.ok) {\n const bodyText = await response.text();\n let payload;\n try {\n payload = bodyText ? JSON.parse(bodyText) : {};\n } catch {\n payload = { error: bodyText };\n }\n const upstreamMessage = payload?.error?.message || payload?.message || '';\n const friendlyMessage =\n response.status === 402 || /insufficient\\s+balance/i.test(upstreamMessage)\n ? '大乘 AI 服务额度暂不可用,请联系管理员充值 DeepSeek 账户后重试。'\n : upstreamMessage || `DeepSeek request failed: ${response.status}`;\n const error = new Error(friendlyMessage);\n error.statusCode = response.status;\n if (response.status === 402 || /insufficient\\s+balance/i.test(upstreamMessage)) {\n error.code = 'DEEPSEEK_PROVIDER_QUOTA';\n }\n throw error;\n }\n\n let buffer = '';\n let message = '';\n let usage = {\n promptTokens: 0,\n completionTokens: 0,\n totalTokens: 0,\n };\n const decoder = new TextDecoder();\n\n for await (const chunk of response.body) {\n buffer += decoder.decode(chunk, { stream: true });\n const lines = buffer.split(/\\r?\\n/);\n buffer = lines.pop() || '';\n\n for (const rawLine of lines) {\n const line = rawLine.trim();\n if (!line.startsWith('data:')) continue;\n const data = line.slice('data:'.length).trim();\n if (!data || data === '[DONE]') continue;\n\n let payload;\n try {\n payload = JSON.parse(data);\n } catch {\n continue;\n }\n\n const delta = payload?.choices?.[0]?.delta?.content || '';\n if (delta) {\n message += delta;\n callbacks.onToken?.(delta);\n }\n\n if (payload?.usage) {\n usage = {\n promptTokens: Number(payload.usage.prompt_tokens || usage.promptTokens || 0),\n completionTokens: Number(payload.usage.completion_tokens || usage.completionTokens || 0),\n totalTokens: Number(payload.usage.total_tokens || usage.totalTokens || 0),\n };\n }\n }\n }\n\n message = message.trim();\n if (!message) {\n const error = new Error('DeepSeek returned an empty response');\n error.statusCode = 502;\n throw error;\n }\n\n return { message, model, usage };\n}\n\nfunction createCodexDeepSeekRuntime(user = null) {\n if (!enableCodexSdkChat || !deepseekApiKey) {\n return {\n enabled: false,\n provider: codexDeepSeekProviderId,\n reason: enableCodexSdkChat ? 'DeepSeek API key is not configured' : 'ENABLE_CODEX_SDK_CHAT is not true',\n };\n }\n\n const adapterToken = user ? createCodexAdapterToken(user) : '';\n return {\n enabled: true,\n provider: codexDeepSeekProviderId,\n options: {\n apiKey: adapterToken,\n baseUrl: codexResponsesBaseUrl,\n env: {\n ...process.env,\n DEEPSEEK_API_KEY: adapterToken,\n HOME: codexHomeDir,\n CODEX_HOME: codexHomeDir,\n TMPDIR: codexTempDir,\n XDG_RUNTIME_DIR: codexRuntimeDir,\n XDG_CACHE_HOME: path.join(codexHomeDir, '.cache'),\n XDG_CONFIG_HOME: path.join(codexHomeDir, '.config'),\n XDG_DATA_HOME: path.join(codexHomeDir, '.local', 'share'),\n PATH: process.env.PATH || '/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin',\n },\n config: {\n model_provider: codexDeepSeekProviderId,\n model_providers: {\n [codexDeepSeekProviderId]: {\n name: 'Dacheng DeepSeek Proxy',\n base_url: codexResponsesBaseUrl,\n env_key: 'DEEPSEEK_API_KEY',\n wire_api: 'responses',\n query_params: {},\n },\n },\n },\n },\n threadOptions: {\n model: deepseekModel,\n approvalPolicy: 'never',\n sandboxMode: 'read-only',\n skipGitRepoCheck: true,\n networkAccessEnabled: true,\n },\n };\n}\n\nfunction codexPromptFromMessages(messages) {\n return messages\n .map((message) => {\n const role = String(message.role || 'user').toUpperCase();\n return `${role}:\\n${message.content}`;\n })\n .join('\\n\\n');\n}\n\nfunction responseContentText(content) {\n if (typeof content === 'string') return content;\n if (!Array.isArray(content)) return '';\n return content\n .map((part) => {\n if (typeof part === 'string') return part;\n if (!part || typeof part !== 'object') return '';\n return String(part.text || part.input_text || part.output_text || '');\n })\n .filter(Boolean)\n .join('\\n');\n}\n\nfunction codexResponsesPrompt(body) {\n const input = Array.isArray(body?.input) ? body.input : [];\n const userItems = input.filter((item) => item?.role === 'user');\n const source = userItems[userItems.length - 1] || input[input.length - 1];\n const prompt = responseContentText(source?.content).trim();\n if (prompt) return prompt;\n if (typeof body?.input === 'string') return body.input.trim();\n return '';\n}\n\nfunction writeResponsesEvent(res, event, data) {\n res.write(`event: ${event}\\n`);\n res.write(`data: ${JSON.stringify(data)}\\n\\n`);\n}\n\nfunction responseUsage(usage = {}) {\n return {\n input_tokens: Number(usage.promptTokens || usage.input_tokens || 0),\n cached_input_tokens: 0,\n output_tokens: Number(usage.completionTokens || usage.output_tokens || 0),\n reasoning_output_tokens: 0,\n total_tokens: Number(usage.totalTokens || usage.total_tokens || 0),\n };\n}\n\nfunction createLibreChatAgentRuntime() {\n if (!enableLibreChatAgentChat || !libreChatAgentApiKey || !libreChatAgentId) {\n return {\n enabled: false,\n provider: 'librechat-agent',\n reason: !enableLibreChatAgentChat\n ? 'ENABLE_LIBRECHAT_AGENT_CHAT is not true'\n : !libreChatAgentApiKey\n ? 'LIBRECHAT_AGENT_API_KEY is not configured'\n : 'LIBRECHAT_AGENT_ID is not configured',\n };\n }\n\n return {\n enabled: true,\n provider: 'librechat-agent',\n model: libreChatAgentId,\n chatCompletionsUrl: `${libreChatAgentsBaseUrl}/chat/completions`,\n };\n}\n\nfunction libreChatUsage(usage = {}) {\n return {\n promptTokens: Number(usage.prompt_tokens || usage.input_tokens || usage.promptTokens || 0),\n completionTokens: Number(usage.completion_tokens || usage.output_tokens || usage.completionTokens || 0),\n totalTokens: Number(usage.total_tokens || usage.totalTokens || 0),\n };\n}\n\nasync function callLibreChatAgent(messages) {\n const runtime = createLibreChatAgentRuntime();\n if (!runtime.enabled) {\n const error = new Error(runtime.reason || 'LibreChat Agent chat is not enabled');\n error.statusCode = 503;\n throw error;\n }\n\n const response = await fetch(runtime.chatCompletionsUrl, {\n method: 'POST',\n headers: {\n Accept: 'application/json',\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${libreChatAgentApiKey}`,\n },\n body: JSON.stringify({\n model: runtime.model,\n messages,\n temperature: 0.4,\n stream: false,\n }),\n signal: AbortSignal.timeout(120_000),\n });\n\n const bodyText = await response.text();\n let payload;\n try {\n payload = bodyText ? JSON.parse(bodyText) : {};\n } catch {\n payload = { error: bodyText };\n }\n\n if (!response.ok) {\n const upstreamMessage = payload?.error?.message || payload?.message || bodyText || '';\n const error = new Error(upstreamMessage || `LibreChat Agent request failed: ${response.status}`);\n error.statusCode = response.status;\n throw error;\n }\n\n const message = payload?.choices?.[0]?.message?.content?.trim();\n if (!message) {\n const error = new Error('LibreChat Agent returned an empty response');\n error.statusCode = 502;\n throw error;\n }\n\n return {\n message,\n usage: libreChatUsage(payload?.usage),\n };\n}\n\nasync function callLibreChatAgentStream(messages, callbacks = {}) {\n const runtime = createLibreChatAgentRuntime();\n if (!runtime.enabled) {\n const error = new Error(runtime.reason || 'LibreChat Agent chat is not enabled');\n error.statusCode = 503;\n throw error;\n }\n\n const response = await fetch(runtime.chatCompletionsUrl, {\n method: 'POST',\n headers: {\n Accept: 'text/event-stream',\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${libreChatAgentApiKey}`,\n },\n body: JSON.stringify({\n model: runtime.model,\n messages,\n temperature: 0.4,\n stream: true,\n }),\n signal: callbacks.signal || AbortSignal.timeout(180_000),\n });\n\n if (!response.ok) {\n const bodyText = await response.text();\n let payload;\n try {\n payload = bodyText ? JSON.parse(bodyText) : {};\n } catch {\n payload = { error: bodyText };\n }\n const upstreamMessage = payload?.error?.message || payload?.message || bodyText || '';\n const error = new Error(upstreamMessage || `LibreChat Agent request failed: ${response.status}`);\n error.statusCode = response.status;\n throw error;\n }\n\n let buffer = '';\n let message = '';\n let usage = { promptTokens: 0, completionTokens: 0, totalTokens: 0 };\n const decoder = new TextDecoder();\n\n for await (const chunk of response.body) {\n buffer += decoder.decode(chunk, { stream: true });\n const lines = buffer.split(/\\r?\\n/);\n buffer = lines.pop() || '';\n\n for (const rawLine of lines) {\n const line = rawLine.trim();\n if (!line.startsWith('data:')) continue;\n const data = line.slice('data:'.length).trim();\n if (!data || data === '[DONE]') continue;\n\n let payload;\n try {\n payload = JSON.parse(data);\n } catch {\n continue;\n }\n\n const delta = payload?.choices?.[0]?.delta?.content || '';\n if (delta) {\n message += delta;\n callbacks.onToken?.(delta);\n }\n\n if (payload?.usage) {\n usage = libreChatUsage(payload.usage);\n }\n }\n }\n\n message = message.trim();\n if (!message) {\n const error = new Error('LibreChat Agent returned an empty response');\n error.statusCode = 502;\n throw error;\n }\n\n return { message, usage };\n}\n\nfunction isDeepSeekQuotaError(error) {\n if (error?.code === 'DEEPSEEK_PROVIDER_QUOTA') return true;\n if (error?.code === 'USER_TOKEN_QUOTA_EXCEEDED' || error?.code === 'AI_MEMBERSHIP_REQUIRED') return false;\n const message = String(error?.message || error || '');\n return /insufficient\\s+balance|balance/i.test(message);\n}\n\nfunction publicAiErrorMessage(error) {\n if (isDeepSeekQuotaError(error)) {\n return '大乘 AI 服务额度暂不可用,请联系管理员充值 DeepSeek 账户后重试。';\n }\n return error?.message || 'Internal Server Error';\n}\n\nfunction responsesPayload({\n responseId,\n itemId,\n status,\n model,\n text = '',\n usage = null,\n error = null,\n}) {\n return {\n id: responseId,\n object: 'response',\n status,\n model,\n output:\n status === 'completed'\n ? [\n {\n id: itemId,\n type: 'message',\n role: 'assistant',\n status: 'completed',\n content: [{ type: 'output_text', text }],\n },\n ]\n : [],\n ...(usage ? { usage: responseUsage(usage) } : {}),\n ...(error\n ? {\n error: {\n message: error.message || String(error),\n type: 'server_error',\n },\n }\n : {}),\n };\n}\n\nasync function callCodexSdkDeepSeek(messages, callbacks = {}) {\n const codexRuntime = createCodexDeepSeekRuntime(callbacks.user);\n if (!codexRuntime.enabled) {\n const error = new Error(codexRuntime.reason || 'Codex SDK chat is not enabled');\n error.statusCode = 503;\n throw error;\n }\n if (!callbacks.user) {\n const error = new Error('Codex SDK DeepSeek proxy requires a resolved user for billing');\n error.statusCode = 401;\n throw error;\n }\n\n const { Codex } = await import('@openai/codex-sdk');\n const codex = new Codex(codexRuntime.options);\n const thread = codex.startThread(codexRuntime.threadOptions);\n const prompt = codexPromptFromMessages(messages);\n\n if (callbacks.onToken || callbacks.onStep) {\n const { events } = await thread.runStreamed(prompt, { signal: callbacks.signal });\n let finalResponse = '';\n let usage = null;\n\n for await (const event of events) {\n if (event.type === 'item.completed') {\n const item = event.item;\n if (item.type === 'agent_message') {\n finalResponse = item.text || finalResponse;\n callbacks.onToken?.(item.text || '');\n } else if (item.type === 'todo_list') {\n callbacks.onStep?.({\n title: 'Codex SDK 执行计划',\n message: item.items.map((todo) => `${todo.completed ? '✓' : '•'} ${todo.text}`).join('\\n'),\n });\n } else if (item.type === 'command_execution') {\n callbacks.onStep?.({\n title: 'Codex SDK 执行命令',\n message: item.command,\n });\n } else if (item.type === 'mcp_tool_call') {\n callbacks.onStep?.({\n title: 'Codex SDK 调用工具',\n message: `${item.server}.${item.tool}`,\n });\n }\n } else if (event.type === 'turn.completed') {\n usage = event.usage;\n } else if (event.type === 'turn.failed') {\n throw new Error(event.error?.message || 'Codex SDK turn failed');\n } else if (event.type === 'error') {\n throw new Error(event.message || 'Codex SDK stream failed');\n }\n }\n\n if (!finalResponse.trim()) {\n const error = new Error('Codex SDK returned an empty response');\n error.statusCode = 502;\n throw error;\n }\n\n return {\n message: finalResponse.trim(),\n usage: {\n promptTokens: Number(usage?.input_tokens || 0),\n completionTokens: Number(usage?.output_tokens || 0),\n totalTokens: Number((usage?.input_tokens || 0) + (usage?.output_tokens || 0)),\n },\n usageAlreadyRecorded: true,\n };\n }\n\n const turn = await thread.run(prompt);\n if (!turn.finalResponse?.trim()) {\n const error = new Error('Codex SDK returned an empty response');\n error.statusCode = 502;\n throw error;\n }\n\n return {\n message: turn.finalResponse.trim(),\n usage: {\n promptTokens: Number(turn.usage?.input_tokens || 0),\n completionTokens: Number(turn.usage?.output_tokens || 0),\n totalTokens: Number((turn.usage?.input_tokens || 0) + (turn.usage?.output_tokens || 0)),\n },\n usageAlreadyRecorded: true,\n };\n}\n\nconst activeAgentRuns = new Map();\n\nfunction normalizeAgentMode(value) {\n const mode = safeUserText(value || 'dharma_guide');\n return /^[a-z][a-z0-9_:-]{0,48}$/i.test(mode) ? mode : 'dharma_guide';\n}\n\nfunction openClawSessionKey(user, conversationId) {\n const env = process.env.NODE_ENV === 'production' ? 'prod' : 'dev';\n return `fabushi:env:${env}:user:${user.userId}:conversation:${conversationId}`;\n}\n\nfunction createOpenClawRuntime() {\n if (!enableOpenClawAgentChat || !openClawGatewayToken || !openClawGatewayUrl) {\n return {\n enabled: false,\n provider: 'openclaw-gateway',\n reason: !enableOpenClawAgentChat\n ? 'ENABLE_OPENCLAW_AGENT_CHAT is not true'\n : !openClawGatewayToken\n ? 'OPENCLAW_GATEWAY_TOKEN is not configured'\n : 'OPENCLAW_GATEWAY_URL is not configured',\n };\n }\n\n return {\n enabled: true,\n provider: 'openclaw-gateway',\n model: openClawAgentId,\n runsUrl: openClawRunsEndpoint || `${openClawGatewayUrl}/v1/agents/${encodeURIComponent(openClawAgentId)}/runs`,\n };\n}\n\nfunction extractOpenClawText(payload) {\n if (!payload || typeof payload !== 'object') return '';\n const candidates = [\n payload.message,\n payload.content,\n payload.output_text,\n payload.finalResponse,\n payload.final_response,\n payload.result?.message,\n payload.result?.content,\n payload.output?.message,\n payload.output?.content,\n payload.choices?.[0]?.message?.content,\n ];\n for (const candidate of candidates) {\n if (typeof candidate === 'string' && candidate.trim()) return candidate.trim();\n }\n return '';\n}\n\nfunction extractOpenClawUsage(payload) {\n const usage = payload?.usage || payload?.result?.usage || {};\n return {\n promptTokens: Number(usage.input_tokens || usage.prompt_tokens || usage.promptTokens || 0),\n completionTokens: Number(usage.output_tokens || usage.completion_tokens || usage.completionTokens || 0),\n totalTokens: Number(usage.total_tokens || usage.totalTokens || 0),\n };\n}\n\nasync function callOpenClawAgent({ messages, user, conversationId, mode, signal }) {\n const runtime = createOpenClawRuntime();\n if (!runtime.enabled) {\n const error = new Error(runtime.reason || 'OpenClaw Agent chat is not enabled');\n error.statusCode = 503;\n throw error;\n }\n\n const sessionKey = openClawSessionKey(user, conversationId);\n const response = await fetch(runtime.runsUrl, {\n method: 'POST',\n headers: {\n Accept: 'application/json',\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${openClawGatewayToken}`,\n },\n body: JSON.stringify({\n agentId: openClawAgentId,\n sessionKey,\n mode,\n stream: false,\n input: { messages },\n messages,\n metadata: {\n app: 'fabushi',\n userId: user.userId,\n conversationId,\n toolPolicy: 'public-user-read-mostly',\n },\n }),\n signal: signal || AbortSignal.timeout(180_000),\n });\n\n const bodyText = await response.text();\n let payload;\n try {\n payload = bodyText ? JSON.parse(bodyText) : {};\n } catch {\n payload = { message: bodyText };\n }\n\n if (!response.ok) {\n const error = new Error(payload?.error?.message || payload?.message || `OpenClaw request failed: ${response.status}`);\n error.statusCode = response.status;\n throw error;\n }\n\n const message = extractOpenClawText(payload);\n if (!message) {\n const error = new Error('OpenClaw returned an empty response');\n error.statusCode = 502;\n throw error;\n }\n\n return {\n message,\n runId: payload.runId || payload.id || payload.result?.runId || null,\n provider: runtime.provider,\n model: runtime.model,\n usage: extractOpenClawUsage(payload),\n };\n}\n\nasync function runAgentModel({ messages, user, conversationId, mode, callbacks = {}, signal, maxCompletionTokens }) {\n const openClawRuntime = createOpenClawRuntime();\n if (openClawRuntime.enabled && !callbacks.onToken) {\n return await callOpenClawAgent({ messages, user, conversationId, mode, signal });\n }\n\n const codexRuntime = createCodexDeepSeekRuntime(user);\n const libreChatRuntime = createLibreChatAgentRuntime();\n let provider = libreChatRuntime.enabled\n ? libreChatRuntime.provider\n : codexRuntime.enabled\n ? codexRuntime.provider\n : 'deepseek';\n let model = libreChatRuntime.enabled ? libreChatRuntime.model : deepseekModel;\n\n try {\n if (libreChatRuntime.enabled) {\n const result = callbacks.onToken\n ? await callLibreChatAgentStream(messages, { ...callbacks, signal })\n : await callLibreChatAgent(messages);\n return { ...result, provider, model };\n }\n if (codexRuntime.enabled) {\n const result = await callCodexSdkDeepSeek(messages, { ...callbacks, user, signal });\n return { ...result, provider, model };\n }\n const result = callbacks.onToken\n ? await callDeepSeekStream(messages, { ...callbacks, signal, maxCompletionTokens })\n : await callDeepSeek(messages, { maxCompletionTokens });\n return { ...result, provider, model };\n } catch (error) {\n if (!codexRuntime.enabled || isDeepSeekQuotaError(error)) throw error;\n logger.warn({ error: String(error) }, 'Agent model failed, falling back to DeepSeek direct');\n const result = callbacks.onToken\n ? await callDeepSeekStream(messages, { ...callbacks, signal, maxCompletionTokens })\n : await callDeepSeek(messages, { maxCompletionTokens });\n return { ...result, provider: 'deepseek', model: deepseekModel };\n }\n}\n\nfunction updateAgentRunStatus(run, status, fields = {}) {\n statements.updateAgentRunStatus.run({\n id: run.id,\n userId: run.user_id || run.userId,\n status,\n openClawRunId: fields.openClawRunId ?? null,\n inputTokens: fields.inputTokens ?? null,\n outputTokens: fields.outputTokens ?? null,\n toolCallCount: fields.toolCallCount ?? null,\n completedAt: status === 'completed' || status === 'cancelled' ? nowIso() : null,\n failedAt: status === 'failed' ? nowIso() : null,\n errorCode: fields.errorCode ?? null,\n errorMessage: fields.errorMessage ?? null,\n });\n}\n\nfunction buildAgentMessages(conversationId) {\n const historyRows = statements.listMessages.all(conversationId).slice(-14);\n return [{ role: 'system', content: systemPrompt }, ...normalizeMessages(historyRows)];\n}\n\nfunction agentUsagePayload(usage, user, budget) {\n const promptTokens = usage.promptTokens || usage.inputTokens || 0;\n const completionTokens = usage.completionTokens || usage.outputTokens || 0;\n const totalTokens = usage.totalTokens || promptTokens + completionTokens;\n const latestUsage = usageFor(user.userId);\n return {\n inputTokens: promptTokens,\n outputTokens: completionTokens,\n totalTokens,\n monthlyLimit: budget.limit,\n remainingTokens: Math.max(0, budget.limit - latestUsage.used),\n };\n}\n\nfunction openAiContentToText(content) {\n if (typeof content === 'string') return content;\n if (Array.isArray(content)) {\n return content\n .map((part) => {\n if (typeof part === 'string') return part;\n if (!part || typeof part !== 'object') return '';\n return readText(part.text || part.content || part.value);\n })\n .filter(Boolean)\n .join('\\n');\n }\n return readText(content);\n}\n\nfunction normalizeOpenAiMessages(value) {\n if (!Array.isArray(value)) return [];\n const allowedRoles = new Set(['system', 'user', 'assistant', 'tool']);\n return value\n .map((item) => {\n if (!item || typeof item !== 'object') return null;\n const role = allowedRoles.has(item.role) ? item.role : 'user';\n const content = openAiContentToText(item.content);\n if (!content) return null;\n return { role, content };\n })\n .filter(Boolean);\n}\n\nfunction truncateForDeepSeekMessage(text, maxChars = 12_000) {\n const value = readText(text);\n if (value.length <= maxChars) return value;\n return `${value.slice(0, maxChars)}\\n\\n[内容已截断]`;\n}\n\nfunction truncateTailForDeepSeekMessage(text, maxChars = 2400) {\n const value = readText(text);\n if (value.length <= maxChars) return value;\n return `[前文已省略]\\n\\n${value.slice(-maxChars)}`;\n}\n\nfunction stripOpenClawPromptNoise(text) {\n const value = readText(text);\n if (!value) return '';\n\n const finalPromptMatch = value.match(/finalPromptText[\"'\\s::]+([\\s\\S]{1,4000})$/i);\n if (finalPromptMatch?.[1]) {\n return readText(finalPromptMatch[1].replace(/^[\"'`]+|[\"'`,}\\]]+$/g, ''));\n }\n\n const markers = [\n 'final prompt:',\n 'final user prompt:',\n 'user prompt:',\n '用户问题:',\n '用户消息:',\n '用户输入:',\n ];\n const lower = value.toLowerCase();\n for (const marker of markers) {\n const index = lower.lastIndexOf(marker.toLowerCase());\n if (index >= 0) {\n const tail = value.slice(index + marker.length).trim();\n if (tail) return tail;\n }\n }\n\n return value;\n}\n\nfunction compactOpenClawMessagesForDeepSeek(messages) {\n const conversationalMessages = messages\n .filter((message) => message.role === 'user' || message.role === 'assistant')\n .map((message) => ({\n role: message.role,\n content: truncateTailForDeepSeekMessage(stripOpenClawPromptNoise(message.content)),\n }))\n .filter((message) => message.content);\n\n const recent = conversationalMessages.slice(-6);\n while (recent.length > 0 && recent[0].role !== 'user') {\n recent.shift();\n }\n\n if (recent.length === 0) {\n const fallback = messages\n .slice()\n .reverse()\n .map((message) => truncateTailForDeepSeekMessage(stripOpenClawPromptNoise(message.content), 2400))\n .find(Boolean);\n if (!fallback) return messages;\n recent.push({ role: 'user', content: fallback });\n }\n\n return [\n {\n role: 'system',\n content: '你是大乘 App 的桌面 AI 助手。请直接回答用户问题,默认使用简洁、准确的中文。',\n },\n ...recent,\n ];\n}\n\nfunction isDeepSeekBlockedError(error) {\n return (\n Number(error?.statusCode) === 403 &&\n /blocked|被拦截|forbidden/i.test(String(error?.message || error || ''))\n );\n}\n\nfunction shouldRetryWithCompactedOpenClawMessages(error, messages, compactedMessages) {\n if (!isDeepSeekBlockedError(error)) return false;\n return JSON.stringify(messages) !== JSON.stringify(compactedMessages);\n}\n\nfunction openClawBlockedFallbackText(messages) {\n const lastUser = messages\n .slice()\n .reverse()\n .find((message) => message.role === 'user')?.content || '';\n const localActionIntent =\n /(file|folder|directory|project|workspace|path|文件|文件夹|目录|项目|工程|路径|本地)/i.test(lastUser) &&\n /(create|make|mkdir|write|edit|modify|rename|delete|move|copy|save|download|read|list|生成|创建|新建|写入|编辑|修改|重命名|删除|移动|复制|保存|下载|读取|列出)/i.test(lastUser);\n\n if (localActionIntent) {\n return [\n '本机工具规划被模型服务拦截了,所以这次没有实际读取、创建、修改或下载本机文件。',\n '如果只是新建文件夹,请先在底部选择项目后重新发送“新建文件夹”;桌面端会直接在已选项目里创建。',\n '如果是更复杂的本地文件操作,请在“连应用”里重启本机 AI 后重试,或把要操作的文件路径和目标说得更具体。',\n ].join('\\n');\n }\n\n return '模型服务拦截了这次回复,未能生成结果。请换一种更具体、更短的表述后重试。';\n}\nfunction usageWithFallback(usage, messages, text) {\n const promptTokens = Number(usage?.promptTokens || usage?.inputTokens || 0);\n const completionTokens = Number(usage?.completionTokens || usage?.outputTokens || 0);\n const totalTokens = Number(usage?.totalTokens || 0);\n if (totalTokens > 0) {\n return {\n promptTokens,\n completionTokens,\n totalTokens,\n };\n }\n const estimatedPrompt = promptTokens || estimateTokens(JSON.stringify(messages));\n const estimatedCompletion = completionTokens || estimateTokens(text);\n return {\n promptTokens: estimatedPrompt,\n completionTokens: estimatedCompletion,\n totalTokens: estimatedPrompt + estimatedCompletion,\n };\n}\n\nfunction openAiUsagePayload(usage, user, budget) {\n const promptTokens = Number(usage.promptTokens || 0);\n const completionTokens = Number(usage.completionTokens || 0);\n const totalTokens = Number(usage.totalTokens || promptTokens + completionTokens);\n const latestUsage = usageFor(user.userId);\n const remainingTokens = Math.max(0, budget.limit - latestUsage.used);\n return {\n prompt_tokens: promptTokens,\n completion_tokens: completionTokens,\n total_tokens: totalTokens,\n promptTokens,\n completionTokens,\n totalTokens,\n monthlyLimit: budget.limit,\n remainingTokens,\n };\n}\n\nfunction writeOpenAiSse(res, payload) {\n res.write(`data: ${JSON.stringify(payload)}\\n\\n`);\n}\n\napp.post(\n '/api/openclaw/deepseek/v1/chat/completions',\n asyncHandler(async (req, res) => {\n const user = await resolveUser(req, req.body || {});\n const incomingMessages = normalizeOpenAiMessages(req.body?.messages);\n if (incomingMessages.length === 0) {\n res.status(400).json({ error: { message: 'messages is required' } });\n return;\n }\n\n const messages = compactOpenClawMessagesForDeepSeek(incomingMessages);\n const promptEstimate = estimateTokens(JSON.stringify(messages));\n const estimated = promptEstimate + 600;\n const budget = enforceTokenBudget(user, estimated);\n const maxCompletionTokens = Math.min(\n completionBudgetFor(budget, promptEstimate),\n normalizeMaxCompletionTokens(req.body?.max_tokens),\n );\n const model = normalizeDeepSeekModelName(req.body?.model);\n const wantsStream =\n req.body?.stream === true ||\n String(req.get('accept') || '').includes('text/event-stream');\n const id = `chatcmpl_${crypto.randomUUID().replaceAll('-', '')}`;\n const created = Math.floor(Date.now() / 1000);\n const compactedMessages = messages;\n\n if (!wantsStream) {\n let result;\n let usageMessages = messages;\n try {\n result = await callDeepSeek(messages, { model, maxCompletionTokens });\n } catch (error) {\n if (!shouldRetryWithCompactedOpenClawMessages(error, messages, compactedMessages)) {\n throw error;\n }\n logger.warn(\n {\n statusCode: error?.statusCode,\n messageCount: messages.length,\n compactedMessageCount: compactedMessages.length,\n },\n 'OpenClaw DeepSeek proxy retrying with compacted messages',\n );\n usageMessages = compactedMessages;\n result = await callDeepSeek(compactedMessages, { model, maxCompletionTokens });\n }\n const usage = usageWithFallback(result.usage, usageMessages, result.message);\n recordUsage(user.userId, usage.totalTokens);\n res.json({\n id,\n object: 'chat.completion',\n created,\n model: result.model || model,\n choices: [\n {\n index: 0,\n message: { role: 'assistant', content: result.message },\n finish_reason: 'stop',\n },\n ],\n usage: openAiUsagePayload(usage, user, budget),\n });\n return;\n }\n\n const abortController = new AbortController();\n req.on('close', () => abortController.abort());\n\n res.status(200);\n res.set({\n 'Content-Type': 'text/event-stream; charset=utf-8',\n 'Cache-Control': 'no-cache, no-transform',\n Connection: 'keep-alive',\n 'X-Accel-Buffering': 'no',\n });\n res.flushHeaders?.();\n\n const baseChunk = {\n id,\n object: 'chat.completion.chunk',\n created,\n model,\n system_fingerprint: 'fabushi-openclaw-deepseek-proxy',\n };\n writeOpenAiSse(res, {\n ...baseChunk,\n choices: [{ index: 0, delta: { role: 'assistant' }, finish_reason: null }],\n });\n\n let text = '';\n let usageMessages = messages;\n try {\n let result;\n try {\n result = await callDeepSeekStream(messages, {\n model,\n maxCompletionTokens,\n signal: abortController.signal,\n onToken: (delta) => {\n text += delta;\n writeOpenAiSse(res, {\n ...baseChunk,\n choices: [{ index: 0, delta: { content: delta }, finish_reason: null }],\n });\n },\n });\n } catch (error) {\n if (!shouldRetryWithCompactedOpenClawMessages(error, messages, compactedMessages)) {\n throw error;\n }\n logger.warn(\n {\n statusCode: error?.statusCode,\n messageCount: messages.length,\n compactedMessageCount: compactedMessages.length,\n },\n 'OpenClaw DeepSeek proxy stream retrying with compacted messages',\n );\n text = '';\n usageMessages = compactedMessages;\n result = await callDeepSeekStream(compactedMessages, {\n model,\n maxCompletionTokens,\n signal: abortController.signal,\n onToken: (delta) => {\n text += delta;\n writeOpenAiSse(res, {\n ...baseChunk,\n choices: [{ index: 0, delta: { content: delta }, finish_reason: null }],\n });\n },\n });\n }\n text = result.message || text;\n const usage = usageWithFallback(result.usage, usageMessages, text);\n recordUsage(user.userId, usage.totalTokens);\n writeOpenAiSse(res, {\n ...baseChunk,\n model: result.model || model,\n choices: [{ index: 0, delta: {}, finish_reason: 'stop' }],\n });\n writeOpenAiSse(res, {\n ...baseChunk,\n model: result.model || model,\n choices: [],\n usage: openAiUsagePayload(usage, user, budget),\n });\n res.write('data: [DONE]\\n\\n');\n } catch (error) {\n if (isDeepSeekBlockedError(error)) {\n const fallbackMessage = openClawBlockedFallbackText(usageMessages);\n const usage = usageWithFallback(null, usageMessages, fallbackMessage);\n recordUsage(user.userId, usage.totalTokens);\n logger.warn(\n { statusCode: error?.statusCode, code: error?.code },\n 'OpenClaw DeepSeek proxy stream blocked; returning local fallback message',\n );\n writeOpenAiSse(res, {\n ...baseChunk,\n choices: [{ index: 0, delta: { content: fallbackMessage }, finish_reason: null }],\n });\n writeOpenAiSse(res, {\n ...baseChunk,\n choices: [{ index: 0, delta: {}, finish_reason: 'stop' }],\n });\n writeOpenAiSse(res, {\n ...baseChunk,\n choices: [],\n usage: openAiUsagePayload(usage, user, budget),\n });\n res.write('data: [DONE]\\n\\n');\n return;\n }\n logger.error(\n { error: String(error), code: error?.code, statusCode: error?.statusCode },\n 'OpenClaw DeepSeek proxy stream failed',\n );\n writeOpenAiSse(res, {\n ...baseChunk,\n choices: [],\n error: {\n message: publicAiErrorMessage(error),\n code: error?.code || 'OPENCLAW_DEEPSEEK_PROXY_ERROR',\n },\n });\n res.write('data: [DONE]\\n\\n');\n } finally {\n res.end();\n }\n }),\n);\n\napp.post(\n '/codex-deepseek/v1/responses',\n asyncHandler(async (req, res) => {\n if (!deepseekApiKey) {\n res.status(401).json({\n error: { message: 'Unauthorized Codex DeepSeek adapter request' },\n });\n return;\n }\n\n const prompt = codexResponsesPrompt(req.body);\n if (!prompt) {\n res.status(400).json({ error: { message: 'Responses input is required' } });\n return;\n }\n\n const { user, recordUsage: shouldRecordUsage } = await resolveCodexResponsesBilling(req);\n const messages = [{ role: 'user', content: prompt }];\n const promptEstimate = estimateTokens(JSON.stringify(messages));\n const budget = enforceTokenBudget(user, promptEstimate + 600);\n const maxCompletionTokens = Math.min(\n completionBudgetFor(budget, promptEstimate),\n normalizeMaxCompletionTokens(req.body?.max_output_tokens || req.body?.max_tokens),\n );\n const model = normalizeDeepSeekModelName(req.body?.model);\n const responseId = `resp_${crypto.randomUUID().replaceAll('-', '')}`;\n const itemId = `msg_${crypto.randomUUID().replaceAll('-', '')}`;\n const wantsStream =\n req.body?.stream !== false ||\n String(req.get('accept') || '').includes('text/event-stream');\n\n if (!wantsStream) {\n const result = await callDeepSeek(messages, { model, maxCompletionTokens });\n const usage = usageWithFallback(result.usage, messages, result.message);\n if (shouldRecordUsage) recordUsage(user.userId, usage.totalTokens);\n res.json(\n responsesPayload({\n responseId,\n itemId,\n status: 'completed',\n model: result.model || model,\n text: result.message,\n usage,\n }),\n );\n return;\n }\n\n res.status(200);\n res.set({\n 'Content-Type': 'text/event-stream; charset=utf-8',\n 'Cache-Control': 'no-cache, no-transform',\n Connection: 'keep-alive',\n 'X-Accel-Buffering': 'no',\n });\n res.flushHeaders?.();\n\n writeResponsesEvent(res, 'response.created', {\n type: 'response.created',\n response: responsesPayload({\n responseId,\n itemId,\n status: 'in_progress',\n model,\n }),\n });\n writeResponsesEvent(res, 'response.output_item.added', {\n type: 'response.output_item.added',\n output_index: 0,\n item: {\n id: itemId,\n type: 'message',\n role: 'assistant',\n status: 'in_progress',\n content: [],\n },\n });\n writeResponsesEvent(res, 'response.content_part.added', {\n type: 'response.content_part.added',\n item_id: itemId,\n output_index: 0,\n content_index: 0,\n part: { type: 'output_text', text: '' },\n });\n\n let text = '';\n try {\n const result = await callDeepSeekStream(messages, {\n model,\n maxCompletionTokens,\n onToken: (delta) => {\n text += delta;\n writeResponsesEvent(res, 'response.output_text.delta', {\n type: 'response.output_text.delta',\n item_id: itemId,\n output_index: 0,\n content_index: 0,\n delta,\n });\n },\n });\n text = result.message || text;\n const usage = usageWithFallback(result.usage, messages, text);\n if (shouldRecordUsage) recordUsage(user.userId, usage.totalTokens);\n writeResponsesEvent(res, 'response.output_text.done', {\n type: 'response.output_text.done',\n item_id: itemId,\n output_index: 0,\n content_index: 0,\n text,\n });\n writeResponsesEvent(res, 'response.content_part.done', {\n type: 'response.content_part.done',\n item_id: itemId,\n output_index: 0,\n content_index: 0,\n part: { type: 'output_text', text },\n });\n writeResponsesEvent(res, 'response.output_item.done', {\n type: 'response.output_item.done',\n output_index: 0,\n item: {\n id: itemId,\n type: 'message',\n role: 'assistant',\n status: 'completed',\n content: [{ type: 'output_text', text }],\n },\n });\n writeResponsesEvent(res, 'response.completed', {\n type: 'response.completed',\n response: responsesPayload({\n responseId,\n itemId,\n status: 'completed',\n model: result.model || model,\n text,\n usage,\n }),\n });\n res.end();\n } catch (error) {\n writeResponsesEvent(res, 'response.failed', {\n type: 'response.failed',\n response: responsesPayload({\n responseId,\n itemId,\n status: 'failed',\n model,\n text,\n error,\n }),\n });\n res.end();\n }\n }),\n);\n\napp.use(\n '/api/openclaw/runtime/files',\n express.static(openClawRuntimeUpdatesDir, {\n fallthrough: false,\n immutable: true,\n maxAge: '7d',\n setHeaders(res) {\n res.setHeader('X-Content-Type-Options', 'nosniff');\n },\n }),\n);\n\napp.get(\n '/api/openclaw/runtime/manifest',\n asyncHandler(async (req, res) => {\n const platform = readText(req.query.platform);\n const currentVersion = readText(req.query.currentVersion);\n const appVersion = readText(req.query.appVersion);\n\n let manifest;\n try {\n manifest = await readJsonFile(openClawRuntimeManifestPath);\n } catch (error) {\n logger.info(\n { manifestPath: openClawRuntimeManifestPath, error: String(error) },\n 'OpenClaw runtime manifest unavailable',\n );\n return jsonResponse(res, 200, {\n success: true,\n schema: 1,\n updateAvailable: false,\n reason: 'manifest-unavailable',\n platform,\n currentVersion,\n appVersion,\n });\n }\n\n const latest = manifest.latest && typeof manifest.latest === 'object'\n ? manifest.latest\n : manifest;\n const platforms = latest.platforms && typeof latest.platforms === 'object'\n ? latest.platforms\n : {};\n const platformRelease = platform ? platforms[platform] : null;\n const version = readText(latest.version || manifest.version);\n\n if (!platform || !platformRelease || typeof platformRelease !== 'object') {\n return jsonResponse(res, 200, {\n success: true,\n schema: Number(manifest.schema || 1),\n channel: readText(manifest.channel || latest.channel || 'stable'),\n updateAvailable: false,\n reason: platform ? 'platform-unavailable' : 'platform-required',\n platform,\n currentVersion,\n appVersion,\n latestVersion: version,\n });\n }\n\n if (platform.startsWith('macos-') && !openClawRuntimeEnableMacosUpdates) {\n return jsonResponse(res, 200, {\n success: true,\n schema: Number(manifest.schema || 1),\n channel: readText(manifest.channel || latest.channel || 'stable'),\n updateAvailable: false,\n reason: 'macos-runtime-updates-disabled',\n platform,\n currentVersion,\n appVersion,\n latestVersion: version,\n });\n }\n\n const downloadUrl = absolutizeOpenClawRuntimeUrl(\n req,\n platformRelease.archiveUrl || platformRelease.url || platformRelease.fileName,\n );\n const release = {\n version,\n channel: readText(manifest.channel || latest.channel || 'stable'),\n minAppVersion: readText(latest.minAppVersion || platformRelease.minAppVersion),\n notes: readText(latest.notes || platformRelease.notes),\n nodeExecutable: readText(\n platformRelease.nodeExecutable || latest.nodeExecutable || 'node/bin/node',\n ),\n cliEntrypoint: readText(\n platformRelease.cliEntrypoint || latest.cliEntrypoint || 'openclaw/openclaw.mjs',\n ),\n defaultPort: Number(latest.defaultPort || manifest.defaultPort || 18789),\n defaultModel: readText(latest.defaultModel || manifest.defaultModel || 'openclaw/default'),\n defaultModelOverride: readText(latest.defaultModelOverride || manifest.defaultModelOverride),\n gatewayArgs: Array.isArray(latest.gatewayArgs)\n ? latest.gatewayArgs.map((item) => String(item))\n : ['gateway', '--port', '{port}', '--force'],\n archiveUrl: downloadUrl,\n sha256: readText(platformRelease.sha256),\n size: Number(platformRelease.size || 0),\n };\n\n jsonResponse(res, 200, {\n success: true,\n schema: Number(manifest.schema || 1),\n updateAvailable: Boolean(version && downloadUrl && version !== currentVersion),\n platform,\n currentVersion,\n appVersion,\n latest: release,\n generatedAt: readText(manifest.generatedAt || latest.generatedAt),\n });\n }),\n);\n\napp.get('/health', (_req, res) => {\n const codexRuntime = createCodexDeepSeekRuntime();\n const libreChatRuntime = createLibreChatAgentRuntime();\n const openClawRuntime = createOpenClawRuntime();\n jsonResponse(res, 200, {\n status: 'ok',\n service: 'dacheng-ai-backend',\n provider: libreChatRuntime.enabled ? libreChatRuntime.provider : 'deepseek',\n model: libreChatRuntime.enabled ? libreChatRuntime.model : deepseekModel,\n deepseekModel,\n deepseekAvailableModels,\n libreChatAgentChatEnabled: libreChatRuntime.enabled,\n libreChatAgentProvider: libreChatRuntime.provider,\n libreChatAgentReason: libreChatRuntime.enabled ? undefined : libreChatRuntime.reason,\n codexSdkAvailable: true,\n codexSdkChatEnabled: codexRuntime.enabled,\n codexSdkProvider: codexRuntime.provider,\n openClawAgentChatEnabled: openClawRuntime.enabled,\n openClawAgentProvider: openClawRuntime.provider,\n openClawAgentReason: openClawRuntime.enabled ? undefined : openClawRuntime.reason,\n requireMemberForAi,\n memberMonthlyLimit,\n freeMonthlyLimit,\n deepseekMaxCompletionTokens,\n openClawRuntimeUpdatesDir,\n openClawRuntimeManifestConfigured: fs.existsSync(openClawRuntimeManifestPath),\n cbetaApiRoot,\n timestamp: nowIso(),\n });\n});\n\napp.get(\n '/api/miniapps/registry',\n asyncHandler(async (req, res) => {\n const user = await resolveUser(req, { username: req.query.username });\n const official = officialMiniAppRegistry(req);\n const store = await readMiniAppStore();\n const visibleSandbox = store.apps.filter((appRecord) => {\n if (appRecord.reviewStatus === 'approved' && appRecord.source === 'marketplace') {\n return true;\n }\n return appRecord.ownerId === user.userId;\n });\n const bots = [\n ...official.bots,\n ...visibleSandbox.map((record) => miniAppRecordToBot(record)),\n ];\n const miniApps = [\n ...official.miniApps,\n ...visibleSandbox.map((record) => miniAppRecordToManifest(req, record)),\n ];\n const registry = {\n schemaVersion: 1,\n hostApiVersion: miniAppHostApiVersion,\n bots,\n miniApps,\n updatedAt: nowIso(),\n };\n registry.signature = sha256(JSON.stringify({ bots, miniApps }));\n jsonResponse(res, 200, { success: true, registry });\n }),\n);\n\napp.get(\n '/api/miniapps/dev/:miniAppId/index.html',\n asyncHandler(async (req, res) => {\n const miniAppId = safeMiniAppId(req.params.miniAppId);\n const store = await readMiniAppStore();\n const record = store.apps.find((appRecord) => appRecord.miniAppId === miniAppId);\n if (!record) {\n res.status(404).type('text/html').send('<h1>Mini app not found</h1>');\n return;\n }\n res.setHeader('Content-Type', 'text/html; charset=utf-8');\n res.setHeader('Cache-Control', 'no-store');\n res.send(record.sourceHtml || generatedMiniAppHtml(record));\n }),\n);\n\napp.post(\n '/api/miniapps/dev/create',\n asyncHandler(async (req, res) => {\n const user = await resolveUser(req, req.body || {});\n const title = safeUserText(req.body?.title, '个人小程序').slice(0, 80);\n const subtitle = safeUserText(req.body?.subtitle, '个人沙箱小程序').slice(0, 120);\n const permissions = sanitizeMiniAppPermissions(req.body?.permissions);\n const prompt = safeUserText(req.body?.prompt || title);\n const idSeed = crypto.randomUUID().replaceAll('-', '').slice(0, 12);\n const miniAppId = `sandbox.${idSeed}`;\n const botId = `bot.${miniAppId}`;\n const sourceHtml = generatedMiniAppHtml({ title, prompt });\n const scan = miniAppScan(permissions, sourceHtml);\n const createdAt = nowIso();\n const record = {\n ownerId: user.userId,\n miniAppId,\n botId,\n title,\n subtitle,\n initials: title.slice(0, 1) || '小',\n icon: 'code',\n avatarColor: '#3D8BFF',\n greeting: '你好,我是这个个人沙箱小程序的机器人。',\n inputHint: '输入消息,或打开小程序面板',\n version: '0.0.1',\n permissions,\n surfaces: ['chatPanel'],\n theme: 'telegramDark',\n source: 'sandbox',\n reviewStatus: 'sandbox',\n sourceHtml,\n scan,\n createdAt,\n updatedAt: createdAt,\n };\n const store = await readMiniAppStore();\n store.apps.push(record);\n await writeMiniAppStore(store);\n jsonResponse(res, 200, {\n success: true,\n miniApp: miniAppRecordToManifest(req, record),\n bot: miniAppRecordToBot(record),\n scan,\n });\n }),\n);\n\napp.post(\n '/api/miniapps/dev/:miniAppId/version',\n asyncHandler(async (req, res) => {\n const user = await resolveUser(req, req.body || {});\n const miniAppId = safeMiniAppId(req.params.miniAppId);\n const store = await readMiniAppStore();\n const index = store.apps.findIndex((appRecord) => appRecord.miniAppId === miniAppId);\n if (index < 0) {\n return jsonResponse(res, 404, { success: false, message: 'mini app not found' });\n }\n const record = store.apps[index];\n if (record.ownerId !== user.userId) {\n return jsonResponse(res, 403, { success: false, message: 'not your mini app' });\n }\n const title = safeUserText(req.body?.title, record.title).slice(0, 80);\n const subtitle = safeUserText(req.body?.subtitle, record.subtitle).slice(0, 120);\n const sourceHtml =\n safeUserText(req.body?.sourceHtml) ||\n generatedMiniAppHtml({ title, prompt: safeUserText(req.body?.prompt || title) });\n const permissions = sanitizeMiniAppPermissions(req.body?.permissions || record.permissions);\n const scan = miniAppScan(permissions, sourceHtml);\n store.apps[index] = {\n ...record,\n title,\n subtitle,\n sourceHtml,\n permissions,\n scan,\n reviewStatus: 'sandbox',\n version: safeUserText(req.body?.version, record.version || '0.0.1'),\n updatedAt: nowIso(),\n };\n await writeMiniAppStore(store);\n jsonResponse(res, 200, {\n success: true,\n miniApp: miniAppRecordToManifest(req, store.apps[index]),\n bot: miniAppRecordToBot(store.apps[index]),\n scan,\n });\n }),\n);\n\napp.post(\n '/api/miniapps/:miniAppId/submit-review',\n asyncHandler(async (req, res) => {\n const user = await resolveUser(req, req.body || {});\n const miniAppId = safeMiniAppId(req.params.miniAppId);\n const store = await readMiniAppStore();\n const index = store.apps.findIndex((appRecord) => appRecord.miniAppId === miniAppId);\n if (index < 0) {\n return jsonResponse(res, 404, { success: false, message: 'mini app not found' });\n }\n const record = store.apps[index];\n if (record.ownerId !== user.userId) {\n return jsonResponse(res, 403, { success: false, message: 'not your mini app' });\n }\n const scan = miniAppScan(record.permissions || [], record.sourceHtml || '');\n if (!scan.passed) {\n return jsonResponse(res, 400, {\n success: false,\n message: 'security scan failed',\n scan,\n });\n }\n store.apps[index] = {\n ...record,\n scan,\n reviewStatus: 'pending_review',\n updatedAt: nowIso(),\n };\n await writeMiniAppStore(store);\n jsonResponse(res, 200, {\n success: true,\n miniApp: miniAppRecordToManifest(req, store.apps[index]),\n scan,\n });\n }),\n);\n\napp.post(\n '/api/botfather/generate-miniapp',\n asyncHandler(async (req, res) => {\n const user = await resolveUser(req, req.body || {});\n const prompt = safeUserText(req.body?.prompt);\n if (!prompt) {\n return jsonResponse(res, 400, { success: false, message: 'prompt is required' });\n }\n const title = titleFromPromptForMiniApp(prompt);\n const subtitle = '机器人之父生成的个人沙箱小程序';\n const requestedPermissions = Array.isArray(req.body?.permissions) ? req.body.permissions : [];\n const generated = await generateBotFatherMiniAppHtml({ title, prompt, user });\n let sourceHtml = generated.sourceHtml;\n let generation = generated.generation;\n const permissions = sanitizeMiniAppPermissions([\n ...requestedPermissions,\n ...inferMiniAppPermissionsFromText(prompt, sourceHtml),\n ]);\n let scan = miniAppScan(permissions, sourceHtml);\n if (!scan.passed) {\n sourceHtml = generatedMiniAppHtml({ title, prompt });\n generation = {\n provider: 'template',\n fallbackReason: 'ai_output_failed_security_scan',\n };\n scan = miniAppScan(permissions, sourceHtml);\n }\n const idSeed = crypto.randomUUID().replaceAll('-', '').slice(0, 12);\n const miniAppId = `sandbox.${idSeed}`;\n const createdAt = nowIso();\n const record = {\n ownerId: user.userId,\n miniAppId,\n botId: `bot.${miniAppId}`,\n title,\n subtitle,\n initials: title.slice(0, 1) || '小',\n icon: 'code',\n avatarColor: '#3D8BFF',\n greeting: `我是「${title}」的机器人。`,\n inputHint: '继续描述修改需求,或打开小程序面板',\n version: '0.0.1',\n permissions,\n surfaces: ['chatPanel'],\n theme: 'telegramDark',\n source: 'sandbox',\n reviewStatus: 'sandbox',\n sourceHtml,\n scan,\n generation,\n prompt,\n createdAt,\n updatedAt: createdAt,\n };\n const store = await readMiniAppStore();\n store.apps.push(record);\n await writeMiniAppStore(store);\n jsonResponse(res, 200, {\n success: true,\n miniApp: miniAppRecordToManifest(req, record),\n bot: miniAppRecordToBot(record),\n scan,\n generation,\n actions: ['openMiniApp', 'continueEditing', 'submitReview'],\n });\n }),\n);\n\napp.get('/api/ai/models', (_req, res) => {\n jsonResponse(res, 200, {\n success: true,\n provider: 'deepseek',\n defaultModel: deepseekModel,\n models: deepseekAvailableModels.map((model) => ({\n id: model,\n label: model === 'deepseek-reasoner' ? 'DeepSeek Reasoner' : 'DeepSeek Chat',\n })),\n });\n});\napp.get(\n '/api/ai/quota',\n asyncHandler(async (req, res) => {\n const user = await resolveUser(req, { username: req.query.username });\n const budget = enforceTokenBudget(user, 0);\n const usage = usageFor(user.userId);\n jsonResponse(res, 200, {\n success: true,\n isMember: user.isMember,\n monthlyLimit: budget.limit,\n usedTokens: usage.used,\n remainingTokens: Math.max(0, budget.limit - usage.used),\n month: usage.month,\n });\n }),\n);\n\napp.post(\n '/api/agent/chat',\n asyncHandler(async (req, res) => {\n const message = safeUserText(req.body?.message);\n if (!message) {\n return jsonResponse(res, 400, { success: false, message: 'message is required' });\n }\n\n const user = await resolveUser(req, req.body || {});\n const promptEstimate = estimateTokens(message);\n const estimated = promptEstimate + 600;\n const budget = enforceTokenBudget(user, estimated);\n const maxCompletionTokens = completionBudgetFor(budget, promptEstimate);\n const createdAt = nowIso();\n const conversationId = safeUserText(req.body?.conversationId) || crypto.randomUUID();\n const mode = normalizeAgentMode(req.body?.mode);\n const stream = req.body?.stream === true;\n let conversation = statements.getConversation.get(conversationId, user.userId);\n const isNewConversation = !conversation;\n\n if (isNewConversation) {\n conversation = { id: conversationId, user_id: user.userId, title: titleFromMessage(message) };\n statements.insertConversation.run({\n id: conversationId,\n userId: user.userId,\n title: conversation.title,\n provider: 'agent',\n model: openClawAgentId,\n createdAt,\n updatedAt: createdAt,\n });\n }\n\n const messageId = safeUserText(req.body?.messageId) || crypto.randomUUID();\n statements.insertMessage.run({\n id: messageId,\n conversationId,\n role: 'user',\n content: message,\n promptTokens: 0,\n completionTokens: 0,\n totalTokens: 0,\n createdAt,\n });\n\n const runId = `run_${crypto.randomUUID().replaceAll('-', '')}`;\n const sessionKey = openClawSessionKey(user, conversationId);\n const openClawRuntime = createOpenClawRuntime();\n statements.insertAgentRun.run({\n id: runId,\n userId: user.userId,\n conversationId,\n messageId,\n openClawRunId: null,\n openClawSessionKey: sessionKey,\n status: stream ? 'queued' : 'running',\n mode,\n provider: openClawRuntime.enabled ? openClawRuntime.provider : 'dacheng-ai-fallback',\n model: openClawRuntime.enabled ? openClawAgentId : deepseekModel,\n startedAt: createdAt,\n });\n\n if (stream) {\n return jsonResponse(res, 200, {\n success: true,\n runId,\n conversationId,\n streamUrl: `/api/agent/runs/${runId}/events`,\n });\n }\n\n const controller = new AbortController();\n activeAgentRuns.set(runId, controller);\n try {\n const skillResult = await runResourceFinderSkill({ message, user });\n const skillContext = resourceContextMessage(skillResult);\n const historyRows = statements.listMessages.all(conversationId).slice(-14);\n const modelMessages = [\n { role: 'system', content: systemPrompt },\n ...(skillContext ? [{ role: 'system', content: skillContext }] : []),\n ...normalizeMessages(historyRows),\n ];\n\n const aiResult = await runAgentModel({\n messages: modelMessages,\n user,\n conversationId,\n mode,\n signal: controller.signal,\n maxCompletionTokens,\n });\n const usage = {\n promptTokens: aiResult.usage?.promptTokens || estimateTokens(JSON.stringify(modelMessages)),\n completionTokens: aiResult.usage?.completionTokens || estimateTokens(aiResult.message),\n totalTokens:\n aiResult.usage?.totalTokens ||\n estimateTokens(JSON.stringify(modelMessages)) + estimateTokens(aiResult.message),\n };\n if (!aiResult.usageAlreadyRecorded) recordUsage(user.userId, usage.totalTokens);\n const answeredAt = nowIso();\n const assistantMessageId = crypto.randomUUID();\n statements.insertMessage.run({\n id: assistantMessageId,\n conversationId,\n role: 'assistant',\n content: aiResult.message,\n promptTokens: usage.promptTokens,\n completionTokens: usage.completionTokens,\n totalTokens: usage.totalTokens,\n createdAt: answeredAt,\n });\n statements.updateConversation.run({\n id: conversationId,\n userId: user.userId,\n title: isNewConversation ? titleFromMessage(message) : conversation.title,\n updatedAt: answeredAt,\n });\n updateAgentRunStatus({ id: runId, user_id: user.userId }, 'completed', {\n openClawRunId: aiResult.runId,\n inputTokens: usage.promptTokens,\n outputTokens: usage.completionTokens,\n });\n const action = determineClientAction(skillResult, message);\n return jsonResponse(res, 200, {\n success: true,\n runId,\n conversationId,\n messageId: assistantMessageId,\n provider: aiResult.provider,\n model: aiResult.model,\n message: aiResult.message,\n usage: agentUsagePayload(usage, user, budget),\n ...(action ? { clientAction: action } : {}),\n });\n } catch (error) {\n const status = error.name === 'AbortError' ? 'cancelled' : 'failed';\n updateAgentRunStatus({ id: runId, user_id: user.userId }, status, {\n errorCode: error.name || 'AGENT_RUN_FAILED',\n errorMessage: publicAiErrorMessage(error),\n });\n if (status === 'cancelled') {\n return jsonResponse(res, 499, { success: false, runId, status: 'cancelled' });\n }\n throw error;\n } finally {\n activeAgentRuns.delete(runId);\n }\n }),\n);\n\napp.get(\n '/api/agent/runs/:runId/events',\n asyncHandler(async (req, res) => {\n const user = await resolveUser(req, {});\n const run = statements.getAgentRun.get(req.params.runId, user.userId);\n if (!run) {\n return jsonResponse(res, 404, { success: false, message: 'run not found' });\n }\n if (run.status === 'completed' || run.status === 'failed' || run.status === 'cancelled') {\n return jsonResponse(res, 409, { success: false, message: `run is already ${run.status}` });\n }\n\n sseHeaders(res);\n const controller = new AbortController();\n activeAgentRuns.set(run.id, controller);\n updateAgentRunStatus(run, 'running');\n writeSse(res, 'run.started', { runId: run.id, conversationId: run.conversation_id });\n\n try {\n let streamedText = '';\n const modelMessages = buildAgentMessages(run.conversation_id);\n const lastUserMsg = [...modelMessages].reverse().find(m => m.role === 'user');\n const userMsgText = lastUserMsg ? lastUserMsg.content : '';\n const skillResult = userMsgText ? await runResourceFinderSkill({\n message: userMsgText,\n user,\n onStep: (step) => writeSse(res, 'tool.call.completed', {\n tool: 'agent.step',\n displayName: step.title || 'Agent step',\n summary: step.message || '',\n }),\n }) : null;\n const skillContext = resourceContextMessage(skillResult);\n const finalMessages = skillContext ? [\n modelMessages[0],\n { role: 'system', content: skillContext },\n ...modelMessages.slice(1)\n ] : modelMessages;\n\n const promptEstimate = estimateTokens(JSON.stringify(finalMessages));\n const estimated = promptEstimate + 600;\n const budget = enforceTokenBudget(user, estimated);\n const maxCompletionTokens = completionBudgetFor(budget, promptEstimate);\n const aiResult = await runAgentModel({\n messages: finalMessages,\n user,\n conversationId: run.conversation_id,\n mode: run.mode,\n signal: controller.signal,\n maxCompletionTokens,\n callbacks: {\n onStep: (step) => writeSse(res, 'tool.call.completed', {\n tool: 'agent.step',\n displayName: step.title || 'Agent step',\n summary: step.message || '',\n }),\n onToken: (delta) => {\n streamedText += delta;\n writeSse(res, 'assistant.delta', { text: delta });\n },\n },\n });\n const message = aiResult.message || streamedText.trim();\n const usage = {\n promptTokens: aiResult.usage?.promptTokens || estimateTokens(JSON.stringify(finalMessages)),\n completionTokens: aiResult.usage?.completionTokens || estimateTokens(message),\n totalTokens:\n aiResult.usage?.totalTokens || estimateTokens(JSON.stringify(finalMessages)) + estimateTokens(message),\n };\n if (!aiResult.usageAlreadyRecorded) recordUsage(user.userId, usage.totalTokens);\n const assistantMessageId = crypto.randomUUID();\n const answeredAt = nowIso();\n statements.insertMessage.run({\n id: assistantMessageId,\n conversationId: run.conversation_id,\n role: 'assistant',\n content: message,\n promptTokens: usage.promptTokens,\n completionTokens: usage.completionTokens,\n totalTokens: usage.totalTokens,\n createdAt: answeredAt,\n });\n statements.updateConversation.run({\n id: run.conversation_id,\n userId: user.userId,\n title: titleFromMessage(message),\n updatedAt: answeredAt,\n });\n updateAgentRunStatus(run, 'completed', {\n openClawRunId: aiResult.runId,\n inputTokens: usage.promptTokens,\n outputTokens: usage.completionTokens,\n });\n const action = determineClientAction(skillResult, userMsgText);\n writeSse(res, 'assistant.message', { messageId: assistantMessageId, content: message });\n writeSse(res, 'run.completed', {\n usage: agentUsagePayload(usage, user, budget),\n ...(action ? { clientAction: action } : {}),\n });\n res.end();\n } catch (error) {\n const status = error.name === 'AbortError' ? 'cancelled' : 'failed';\n updateAgentRunStatus(run, status, {\n errorCode: error.name || 'AGENT_RUN_FAILED',\n errorMessage: publicAiErrorMessage(error),\n });\n writeSse(res, status === 'cancelled' ? 'run.cancelled' : 'run.failed', {\n runId: run.id,\n message: publicAiErrorMessage(error),\n });\n res.end();\n } finally {\n activeAgentRuns.delete(run.id);\n }\n }),\n);\n\napp.post(\n '/api/agent/runs/:runId/cancel',\n asyncHandler(async (req, res) => {\n const user = await resolveUser(req, req.body || {});\n const run = statements.getAgentRun.get(req.params.runId, user.userId);\n if (!run) {\n return jsonResponse(res, 404, { success: false, message: 'run not found' });\n }\n const controller = activeAgentRuns.get(run.id);\n if (controller) controller.abort();\n updateAgentRunStatus(run, 'cancelled');\n return jsonResponse(res, 200, { success: true, runId: run.id, status: 'cancelled' });\n }),\n);\n\napp.post(\n '/api/agent/messages/:messageId/feedback',\n asyncHandler(async (req, res) => {\n const user = await resolveUser(req, req.body || {});\n const rating = safeUserText(req.body?.rating);\n if (!['up', 'down'].includes(rating)) {\n return jsonResponse(res, 400, { success: false, message: 'rating must be up or down' });\n }\n statements.insertAgentFeedback.run({\n id: crypto.randomUUID(),\n messageId: req.params.messageId,\n userId: user.userId,\n rating,\n reason: safeUserText(req.body?.reason),\n comment: safeUserText(req.body?.comment),\n createdAt: nowIso(),\n });\n return jsonResponse(res, 200, { success: true });\n }),\n);\n\napp.post(\n '/api/ai/chat',\n asyncHandler(async (req, res) => {\n const message = safeUserText(req.body?.message);\n if (!message) {\n return jsonResponse(res, 400, { success: false, message: 'message is required' });\n }\n\n const user = await resolveUser(req, req.body || {});\n const promptEstimate = estimateTokens(message);\n const estimated = promptEstimate + 600;\n const budget = enforceTokenBudget(user, estimated);\n const maxCompletionTokens = completionBudgetFor(budget, promptEstimate);\n const createdAt = nowIso();\n const conversationId = safeUserText(req.body?.conversationId) || crypto.randomUUID();\n const requestedModel = normalizeDeepSeekModelName(req.body?.model);\n const clientContext = clientContextMessage(req.body?.client);\n let conversation = statements.getConversation.get(conversationId, user.userId);\n const isNewConversation = !conversation;\n\n if (isNewConversation) {\n conversation = {\n id: conversationId,\n user_id: user.userId,\n title: titleFromMessage(message),\n };\n statements.insertConversation.run({\n id: conversationId,\n userId: user.userId,\n title: conversation.title,\n provider: 'deepseek',\n model: requestedModel,\n createdAt,\n updatedAt: createdAt,\n });\n }\n\n statements.insertMessage.run({\n id: crypto.randomUUID(),\n conversationId,\n role: 'user',\n content: message,\n promptTokens: 0,\n completionTokens: 0,\n totalTokens: 0,\n createdAt,\n });\n\n const libreChatRuntime = createLibreChatAgentRuntime();\n const skillResult = await runResourceFinderSkill({ message, user });\n const skillContext = resourceContextMessage(skillResult);\n const historyRows = statements.listMessages.all(conversationId).slice(-14);\n const modelMessages = [\n { role: 'system', content: systemPrompt },\n ...(clientContext ? [{ role: 'system', content: clientContext }] : []),\n ...(skillContext ? [{ role: 'system', content: skillContext }] : []),\n ...normalizeMessages(historyRows),\n ];\n\n const openClawRuntime = createOpenClawRuntime();\n const codexRuntime = createCodexDeepSeekRuntime(user);\n let provider = openClawRuntime.enabled\n ? openClawRuntime.provider\n : libreChatRuntime.enabled\n ? libreChatRuntime.provider\n : 'deepseek';\n let responseModel = openClawRuntime.enabled\n ? openClawRuntime.model\n : libreChatRuntime.enabled\n ? libreChatRuntime.model\n : requestedModel;\n let aiResult;\n try {\n if (openClawRuntime.enabled) {\n aiResult = await callOpenClawAgent({\n messages: modelMessages,\n user,\n conversationId,\n mode: normalizeAgentMode(req.body?.mode),\n });\n provider = aiResult.provider || provider;\n responseModel = aiResult.model || responseModel;\n } else if (libreChatRuntime.enabled) {\n aiResult = await callLibreChatAgent(modelMessages);\n } else if (codexRuntime.enabled) {\n provider = codexRuntime.provider;\n aiResult = await callCodexSdkDeepSeek(modelMessages, { user, maxCompletionTokens });\n } else {\n aiResult = await callDeepSeek(modelMessages, { model: requestedModel, maxCompletionTokens });\n }\n } catch (error) {\n if (libreChatRuntime.enabled) {\n logger.warn({ error: String(error) }, 'LibreChat Agent chat failed, falling back to Codex SDK or DeepSeek direct');\n provider = codexRuntime.enabled ? codexRuntime.provider : 'deepseek';\n responseModel = requestedModel;\n if (codexRuntime.enabled) {\n try {\n aiResult = await callCodexSdkDeepSeek(modelMessages, { user, maxCompletionTokens });\n } catch (codexError) {\n if (isDeepSeekQuotaError(codexError)) throw codexError;\n logger.warn({ error: String(codexError) }, 'Codex SDK chat failed, falling back to DeepSeek direct');\n provider = 'deepseek';\n aiResult = await callDeepSeek(modelMessages, { model: requestedModel, maxCompletionTokens });\n }\n } else {\n aiResult = await callDeepSeek(modelMessages, { model: requestedModel, maxCompletionTokens });\n }\n } else {\n if (!codexRuntime.enabled || isDeepSeekQuotaError(error)) throw error;\n logger.warn({ error: String(error) }, 'Codex SDK chat failed, falling back to DeepSeek direct');\n provider = 'deepseek';\n responseModel = requestedModel;\n aiResult = await callDeepSeek(modelMessages, { model: requestedModel, maxCompletionTokens });\n }\n }\n const usage = {\n promptTokens: aiResult.usage.promptTokens || estimateTokens(JSON.stringify(modelMessages)),\n completionTokens: aiResult.usage.completionTokens || estimateTokens(aiResult.message),\n totalTokens:\n aiResult.usage.totalTokens ||\n estimateTokens(JSON.stringify(modelMessages)) + estimateTokens(aiResult.message),\n };\n if (!aiResult.usageAlreadyRecorded) recordUsage(user.userId, usage.totalTokens);\n\n const answeredAt = nowIso();\n statements.insertMessage.run({\n id: crypto.randomUUID(),\n conversationId,\n role: 'assistant',\n content: aiResult.message,\n promptTokens: usage.promptTokens,\n completionTokens: usage.completionTokens,\n totalTokens: usage.totalTokens,\n createdAt: answeredAt,\n });\n statements.updateConversation.run({\n id: conversationId,\n userId: user.userId,\n title: isNewConversation ? titleFromMessage(message) : conversation.title,\n updatedAt: answeredAt,\n });\n\n const latestUsage = usageFor(user.userId);\n const monthlyLimit = budget.limit;\n const action = determineClientAction(skillResult, message);\n jsonResponse(res, 200, {\n success: true,\n conversationId,\n provider,\n model: responseModel,\n message: aiResult.message,\n usage: {\n ...usage,\n monthlyLimit,\n remainingTokens: Math.max(0, monthlyLimit - latestUsage.used),\n },\n ...(action ? { clientAction: action } : {}),\n });\n }),\n);\n\napp.post('/api/ai/chat/stream', async (req, res) => {\n sseHeaders(res);\n\n try {\n const message = safeUserText(req.body?.message);\n if (!message) {\n writeSse(res, 'error', { message: 'message is required' });\n res.end();\n return;\n }\n\n const user = await resolveUser(req, req.body || {});\n const promptEstimate = estimateTokens(message);\n const estimated = promptEstimate + 600;\n const budget = enforceTokenBudget(user, estimated);\n const maxCompletionTokens = completionBudgetFor(budget, promptEstimate);\n const createdAt = nowIso();\n const conversationId = safeUserText(req.body?.conversationId) || crypto.randomUUID();\n const requestedModel = normalizeDeepSeekModelName(req.body?.model);\n const clientContext = clientContextMessage(req.body?.client);\n let conversation = statements.getConversation.get(conversationId, user.userId);\n const isNewConversation = !conversation;\n\n if (isNewConversation) {\n conversation = {\n id: conversationId,\n user_id: user.userId,\n title: titleFromMessage(message),\n };\n statements.insertConversation.run({\n id: conversationId,\n userId: user.userId,\n title: conversation.title,\n provider: 'deepseek',\n model: requestedModel,\n createdAt,\n updatedAt: createdAt,\n });\n }\n\n statements.insertMessage.run({\n id: crypto.randomUUID(),\n conversationId,\n role: 'user',\n content: message,\n promptTokens: 0,\n completionTokens: 0,\n totalTokens: 0,\n createdAt,\n });\n\n const codexRuntime = createCodexDeepSeekRuntime(user);\n const libreChatRuntime = createLibreChatAgentRuntime();\n const initialOpenClawRuntime = createOpenClawRuntime();\n let provider = initialOpenClawRuntime.enabled\n ? initialOpenClawRuntime.provider\n : libreChatRuntime.enabled\n ? libreChatRuntime.provider\n : codexRuntime.enabled\n ? codexRuntime.provider\n : 'deepseek';\n let responseModel = initialOpenClawRuntime.enabled\n ? initialOpenClawRuntime.model\n : libreChatRuntime.enabled\n ? libreChatRuntime.model\n : requestedModel;\n writeSse(res, 'meta', {\n conversationId,\n provider,\n model: responseModel,\n });\n const skillResult = await runResourceFinderSkill({\n message,\n user,\n onStep: (step) => writeSse(res, 'step', step),\n });\n const skillContext = resourceContextMessage(skillResult);\n const historyRows = statements.listMessages.all(conversationId).slice(-14);\n const modelMessages = [\n { role: 'system', content: systemPrompt },\n ...(clientContext ? [{ role: 'system', content: clientContext }] : []),\n ...(skillContext ? [{ role: 'system', content: skillContext }] : []),\n ...normalizeMessages(historyRows),\n ];\n\n let aiResult;\n const openClawRuntime = initialOpenClawRuntime;\n try {\n if (openClawRuntime.enabled) {\n aiResult = await callOpenClawAgent({\n messages: modelMessages,\n user,\n conversationId,\n mode: normalizeAgentMode(req.body?.mode),\n });\n provider = aiResult.provider || provider;\n responseModel = aiResult.model || responseModel;\n writeSse(res, 'delta', { text: aiResult.message });\n } else if (libreChatRuntime.enabled) {\n aiResult = await callLibreChatAgentStream(modelMessages, {\n onToken: (token) => writeSse(res, 'delta', { text: token }),\n });\n } else if (codexRuntime.enabled) {\n aiResult = await callCodexSdkDeepSeek(modelMessages, {\n user,\n maxCompletionTokens,\n onToken: (token) => writeSse(res, 'delta', { text: token }),\n onStep: (step) => writeSse(res, 'step', step),\n });\n } else {\n aiResult = await callDeepSeekStream(modelMessages, {\n model: requestedModel,\n maxCompletionTokens,\n onToken: (token) => writeSse(res, 'delta', { text: token }),\n });\n }\n } catch (error) {\n if (libreChatRuntime.enabled) {\n logger.warn({ error: String(error) }, 'LibreChat Agent streaming chat failed, falling back to Codex SDK or DeepSeek direct');\n provider = codexRuntime.enabled ? codexRuntime.provider : 'deepseek';\n responseModel = requestedModel;\n writeSse(res, 'meta', {\n conversationId,\n provider,\n model: responseModel,\n });\n if (codexRuntime.enabled) {\n try {\n aiResult = await callCodexSdkDeepSeek(modelMessages, {\n user,\n maxCompletionTokens,\n onToken: (token) => writeSse(res, 'delta', { text: token }),\n onStep: (step) => writeSse(res, 'step', step),\n });\n } catch (codexError) {\n if (isDeepSeekQuotaError(codexError)) throw codexError;\n logger.warn({ error: String(codexError) }, 'Codex SDK streaming chat failed, falling back to DeepSeek direct');\n provider = 'deepseek';\n responseModel = requestedModel;\n writeSse(res, 'meta', {\n conversationId,\n provider,\n model: responseModel,\n });\n aiResult = await callDeepSeekStream(modelMessages, {\n model: requestedModel,\n maxCompletionTokens,\n onToken: (token) => writeSse(res, 'delta', { text: token }),\n });\n }\n } else {\n aiResult = await callDeepSeekStream(modelMessages, {\n model: requestedModel,\n maxCompletionTokens,\n onToken: (token) => writeSse(res, 'delta', { text: token }),\n });\n }\n } else {\n if (!codexRuntime.enabled || isDeepSeekQuotaError(error)) throw error;\n logger.warn({ error: String(error) }, 'Codex SDK streaming chat failed, falling back to DeepSeek direct');\n provider = 'deepseek';\n responseModel = requestedModel;\n writeSse(res, 'meta', {\n conversationId,\n provider,\n model: responseModel,\n });\n aiResult = await callDeepSeekStream(modelMessages, {\n model: requestedModel,\n maxCompletionTokens,\n onToken: (token) => writeSse(res, 'delta', { text: token }),\n });\n }\n }\n\n const usage = {\n promptTokens: aiResult.usage.promptTokens || estimateTokens(JSON.stringify(modelMessages)),\n completionTokens: aiResult.usage.completionTokens || estimateTokens(aiResult.message),\n totalTokens:\n aiResult.usage.totalTokens ||\n estimateTokens(JSON.stringify(modelMessages)) + estimateTokens(aiResult.message),\n };\n if (!aiResult.usageAlreadyRecorded) recordUsage(user.userId, usage.totalTokens);\n\n const answeredAt = nowIso();\n statements.insertMessage.run({\n id: crypto.randomUUID(),\n conversationId,\n role: 'assistant',\n content: aiResult.message,\n promptTokens: usage.promptTokens,\n completionTokens: usage.completionTokens,\n totalTokens: usage.totalTokens,\n createdAt: answeredAt,\n });\n statements.updateConversation.run({\n id: conversationId,\n userId: user.userId,\n title: isNewConversation ? titleFromMessage(message) : conversation.title,\n updatedAt: answeredAt,\n });\n\n const latestUsage = usageFor(user.userId);\n const monthlyLimit = budget.limit;\n const action = determineClientAction(skillResult, message);\n writeSse(res, 'done', {\n conversationId,\n provider,\n model: responseModel,\n message: aiResult.message,\n usage: {\n ...usage,\n monthlyLimit,\n remainingTokens: Math.max(0, monthlyLimit - latestUsage.used),\n },\n ...(action ? { clientAction: action } : {}),\n });\n res.end();\n } catch (error) {\n logger.error({ error: error.stack || String(error) }, 'streaming chat failed');\n writeSse(res, 'error', {\n message: publicAiErrorMessage(error),\n details: error.details,\n });\n res.end();\n }\n});\n\napp.get(\n '/api/ai/conversations',\n asyncHandler(async (req, res) => {\n const user = await resolveUser(req, { username: req.query.username });\n const items = statements.listConversations.all(user.userId).map((item) => ({\n id: item.id,\n title: item.title,\n updatedAt: item.updatedAt,\n }));\n jsonResponse(res, 200, { success: true, items });\n }),\n);\n\napp.get(\n '/api/ai/conversations/:id',\n asyncHandler(async (req, res) => {\n const user = await resolveUser(req);\n const conversation = statements.getConversation.get(req.params.id, user.userId);\n if (!conversation) {\n return jsonResponse(res, 404, { success: false, message: 'conversation not found' });\n }\n const messages = statements.listMessages.all(req.params.id).map((item) => ({\n role: item.role,\n content: item.content,\n createdAt: item.createdAt,\n }));\n jsonResponse(res, 200, {\n success: true,\n conversation: {\n id: conversation.id,\n title: conversation.title,\n updatedAt: conversation.updated_at,\n },\n messages,\n });\n }),\n);\n\nfunction buildUrl(base, endpoint, params = {}) {\n const url = new URL(endpoint, `${base}/`);\n for (const [key, value] of Object.entries(params)) {\n if (Array.isArray(value)) {\n for (const item of value) {\n if (item !== undefined && item !== null && item !== '') {\n url.searchParams.append(key, String(item));\n }\n }\n } else if (value !== undefined && value !== null && value !== '') {\n url.searchParams.set(key, String(value));\n }\n }\n return url;\n}\n\nasync function fetchJson(url) {\n const response = await fetch(url, {\n headers: { Accept: 'application/json' },\n signal: AbortSignal.timeout(15_000),\n });\n if (!response.ok) {\n throw new Error(`Request failed: ${response.status}`);\n }\n return response.json();\n}\n\nfunction stripHtml(value) {\n return String(value || '')\n .replace(/<script[\\s\\S]*?<\\/script>/gi, ' ')\n .replace(/<style[\\s\\S]*?<\\/style>/gi, ' ')\n .replace(/<[^>]+>/g, ' ')\n .replace(/ /g, ' ')\n .replace(/</g, '<')\n .replace(/>/g, '>')\n .replace(/&/g, '&')\n .replace(/&#(\\d+);/g, (_match, code) => String.fromCharCode(Number(code)))\n .replace(/\\s+/g, ' ')\n .trim();\n}\n\nfunction cleanExtractedText(value) {\n let text = String(value || '')\n .replace(/\\b[A-Z]{1,3}\\d{2}n\\d{4,5}[a-z]?_p?\\d{4}[a-z]\\d{2}\\b/g, ' ')\n .replace(/\\uFFFD/g, '')\n .replace(/\\s+([,。;:!?、」』)》】])/g, '$1')\n .replace(/([「『《(【])\\s+/g, '$1');\n for (let i = 0; i < 3; i += 1) {\n text = text.replace(/([\\u3400-\\u9fff])\\s+([\\u3400-\\u9fff])/g, '$1$2');\n }\n return text.replace(/[ \\t]{2,}/g, ' ').trim();\n}\n\nfunction compactText(value, limit = 140) {\n const text = stripHtml(value);\n return text.length > limit ? `${text.slice(0, limit)}...` : text;\n}\n\nfunction collectSearchResults(data) {\n if (!data || typeof data !== 'object') return [];\n if (Array.isArray(data.results)) return data.results;\n if (Array.isArray(data?.results?.docs)) return data.results.docs;\n if (Array.isArray(data?.response?.docs)) return data.response.docs;\n if (Array.isArray(data.docs)) return data.docs;\n return [];\n}\n\nfunction firstText(item, keys) {\n for (const key of keys) {\n const value = item?.[key];\n if (Array.isArray(value)) {\n const first = value.map((entry) => safeUserText(String(entry))).find(Boolean);\n if (first) return first;\n }\n if (typeof value === 'string' && value.trim()) return value.trim();\n if (typeof value === 'number') return String(value);\n }\n return '';\n}\n\nfunction firstNumber(item, keys, fallback = undefined) {\n for (const key of keys) {\n const value = item?.[key];\n if (typeof value === 'number') return value;\n if (typeof value === 'string' && value.trim() && Number.isFinite(Number(value))) {\n return Number(value);\n }\n }\n return fallback;\n}\n\nasync function searchCbetaTitle(query, limit) {\n const url = buildUrl(cbetaApiRoot, 'search/title', { q: query, start: 0, rows: limit });\n const data = await fetchJson(url);\n return collectSearchResults(data).flatMap((item) => {\n const work = firstText(item, ['work', 'work_id', 'workId']);\n const title = firstText(item, ['content', 'title', 'work_title', 'workTitle']);\n if (!work || !title) return [];\n const juan = firstNumber(item, ['juan', 'juan_num', 'juanNum'], 1);\n return [\n {\n id: `cbeta-title-${work}-${juan}`,\n title,\n sourceName: 'CBETA 佛典',\n url: `cbeta:${work}:${juan}`,\n snippet: firstText(item, ['byline', 'creators', 'time_dynasty']) || '可下载经文正文并用于全球法布施。',\n resourceType: 'scripture',\n work,\n juan,\n },\n ];\n });\n}\n\nasync function searchCbetaContent(query, limit) {\n const endpoints = ['search/fulltext', 'search/content', 'search/all_in_one'];\n const settled = await Promise.allSettled(\n endpoints.map((endpoint) =>\n fetchJson(buildUrl(cbetaApiRoot, endpoint, { q: query, start: 0, rows: limit, field: 'content' })),\n ),\n );\n const results = [];\n for (const response of settled) {\n if (response.status !== 'fulfilled') continue;\n for (const item of collectSearchResults(response.value)) {\n const work = firstText(item, ['work', 'work_id', 'workId', 'sutra', 'id']);\n const snippet = compactText(firstText(item, ['kwic', 'highlight', 'snippet', 'content', 'text', 'body', 'p']));\n if (!work || !snippet) continue;\n const juan = firstNumber(item, ['juan', 'juan_num', 'juanNum', '卷'], 1);\n const title = firstText(item, ['title', 'work_title', 'workTitle', 'sutra_name', 'book', 'name']) || work;\n results.push({\n id: `cbeta-content-${work}-${juan}-${sha256(snippet).slice(0, 8)}`,\n title,\n sourceName: 'CBETA 佛典',\n url: `cbeta:${work}:${juan}`,\n snippet,\n resourceType: 'scripture',\n work,\n juan,\n });\n if (results.length >= limit) return results;\n }\n }\n return results;\n}\n\nfunction directUrlResource(query) {\n const url = safeUserText(query);\n let parsed;\n try {\n parsed = new URL(url);\n } catch {\n return null;\n }\n if (!['http:', 'https:'].includes(parsed.protocol)) return null;\n return {\n id: `url-${sha256(url).slice(0, 12)}`,\n title: parsed.hostname,\n sourceName: '网页资源',\n url,\n snippet: '直接下载并提取这个链接的可读正文。',\n resourceType: 'web',\n };\n}\n\nfunction looksLikeAudioQuery(query) {\n return /音乐|音樂|音频|音訊|mp3|audio|念诵|念誦|诵经|誦經|唱诵|唱誦|读诵|讀誦/.test(\n safeUserText(query).toLowerCase(),\n );\n}\n\nfunction archiveSearchTerm(query) {\n return (\n safeUserText(query)\n .replace(/下载|查找|寻找|找一?个|资源|音频|音訊|音乐|音樂|mp3|audio|念诵|念誦|诵经|誦經|唱诵|唱誦|读诵|讀誦/gi, ' ')\n .replace(/\\s+/g, ' ')\n .trim() || safeUserText(query)\n );\n}\n\nfunction solrPhrase(value) {\n return String(value).replace(/\\\\/g, '\\\\\\\\').replace(/\"/g, '\\\\\"');\n}\n\nasync function searchInternetArchiveAudio(query, limit) {\n if (!looksLikeAudioQuery(query)) return [];\n const term = archiveSearchTerm(query);\n if (!term) return [];\n const variants = Array.from(\n new Set([\n term,\n term.replaceAll('金刚经', '金剛經'),\n term.replaceAll('金剛經', '金刚经'),\n ].filter(Boolean)),\n );\n const phraseQuery = variants\n .flatMap((item) => [`title:\"${solrPhrase(item)}\"`, `description:\"${solrPhrase(item)}\"`])\n .join(' OR ');\n const url = buildUrl('https://archive.org', 'advancedsearch.php', {\n q: `(${phraseQuery}) AND mediatype:audio`,\n rows: Math.min(Math.max(limit, 1), 10),\n output: 'json',\n 'fl[]': ['identifier', 'title', 'description'],\n });\n const data = await fetchJson(url);\n const docs = Array.isArray(data?.response?.docs) ? data.response.docs : [];\n return docs\n .map((item) => {\n const identifier = firstText(item, ['identifier']);\n const title = firstText(item, ['title']) || identifier;\n if (!identifier || !title) return null;\n return {\n id: `ia-audio-${identifier}`,\n title,\n sourceName: 'Internet Archive 音频',\n url: `ia:${identifier}`,\n snippet: compactText(firstText(item, ['description'])) || '公开可访问的音频资源,将下载第一个可用 MP3 文件。',\n resourceType: 'audio',\n identifier,\n };\n })\n .filter(Boolean);\n}\n\nconst curatedCbetaWorks = [\n {\n work: 'T0235',\n juan: 1,\n title: '金剛般若波羅蜜經',\n keywords: ['金剛般若波羅蜜經', '金刚般若波罗蜜经', '金剛經', '金刚经'],\n snippet: '姚秦 鳩摩羅什譯。常用于读诵、共修与法布施的般若经典。',\n },\n {\n work: 'T0251',\n juan: 1,\n title: '般若波羅蜜多心經',\n keywords: ['般若波羅蜜多心經', '般若波罗蜜多心经', '心經', '心经'],\n snippet: '唐 玄奘譯。篇幅短,适合加入禅室功课本与日常持诵。',\n },\n {\n work: 'T0262',\n juan: 1,\n title: '妙法蓮華經',\n keywords: ['妙法蓮華經', '妙法莲华经', '法華經', '法华经'],\n snippet: '姚秦 鳩摩羅什譯。可按卷下载正文,用于长期功课与分享。',\n },\n {\n work: 'T0279',\n juan: 1,\n title: '大方廣佛華嚴經',\n keywords: ['大方廣佛華嚴經', '大方广佛华严经', '華嚴經', '华严经'],\n snippet: '唐 實叉難陀譯。可按卷下载正文,用于系统研读与法布施。',\n },\n {\n work: 'T0360',\n juan: 1,\n title: '佛說無量壽經',\n keywords: ['佛說無量壽經', '佛说无量寿经', '無量壽經', '无量寿经'],\n snippet: '净土经典。可下载正文,用于净土功课本与全球法布施。',\n },\n {\n work: 'T0365',\n juan: 1,\n title: '佛說觀無量壽佛經',\n keywords: ['佛說觀無量壽佛經', '佛说观无量寿佛经', '觀無量壽經', '观无量寿经'],\n snippet: '净土三经之一。适合查找下载后加入净土功课。',\n },\n {\n work: 'T0366',\n juan: 1,\n title: '佛說阿彌陀經',\n keywords: ['佛說阿彌陀經', '佛说阿弥陀经', '阿彌陀經', '阿弥陀经'],\n snippet: '姚秦 鳩摩羅什譯。篇幅适中,适合共修、持诵与分享。',\n },\n {\n work: 'T0475',\n juan: 1,\n title: '維摩詰所說經',\n keywords: ['維摩詰所說經', '维摩诘所说经', '維摩詰經', '维摩诘经'],\n snippet: '姚秦 鳩摩羅什譯。大乘经典,可按卷下载用于研读。',\n },\n {\n work: 'T0676',\n juan: 1,\n title: '解深密經',\n keywords: ['解深密經', '解深密经'],\n snippet: '唯识相关大乘经典。适合研读、整理笔记与法布施。',\n },\n {\n work: 'T1666',\n juan: 1,\n title: '大乘起信論',\n keywords: ['大乘起信論', '大乘起信论', '起信論', '起信论'],\n snippet: '大乘论典。适合下载后作为研习功课资料。',\n },\n];\n\nfunction localCbetaResults(query, limit) {\n const normalizedQuery = safeUserText(query).toLowerCase();\n if (!normalizedQuery) return [];\n return curatedCbetaWorks\n .filter((item) =>\n item.keywords.some((keyword) => {\n const normalizedKeyword = keyword.toLowerCase();\n return normalizedKeyword.includes(normalizedQuery) || normalizedQuery.includes(normalizedKeyword);\n }),\n )\n .slice(0, limit)\n .map((item) => ({\n id: `cbeta-curated-${item.work}-${item.juan}`,\n title: item.title,\n sourceName: 'CBETA 佛典',\n url: `cbeta:${item.work}:${item.juan}`,\n snippet: item.snippet,\n resourceType: 'scripture',\n work: item.work,\n juan: item.juan,\n }));\n}\n\nfunction uniqueResources(items, limit) {\n const seen = new Set();\n const results = [];\n for (const item of items) {\n const key = `${item.url}:${item.title}`;\n if (seen.has(key)) continue;\n seen.add(key);\n results.push(item);\n if (results.length >= limit) break;\n }\n return results;\n}\n\nasync function findResourceCandidates(query, limit) {\n const direct = directUrlResource(query);\n const results = [];\n if (direct) results.push(direct);\n try {\n results.push(...(await searchInternetArchiveAudio(query, limit)));\n } catch (error) {\n logger.warn({ error: String(error), query }, 'Internet Archive audio search skipped');\n }\n results.push(...localCbetaResults(query, limit));\n\n const [titleResults, contentResults] = await Promise.allSettled([\n searchCbetaTitle(query, limit),\n searchCbetaContent(query, limit),\n ]);\n if (titleResults.status === 'fulfilled') results.push(...titleResults.value);\n if (contentResults.status === 'fulfilled') results.push(...contentResults.value);\n\n return uniqueResources(results, limit);\n}\n\nfunction looksLikeResourceTask(message) {\n const text = safeUserText(message).toLowerCase();\n if (!text) return false;\n return /下载|查找|寻找|找一?个|资源|經文|经文|佛经|佛經|仪轨|儀軌|音乐|音樂|mp3|audio|念诵|念誦|诵经|誦經/.test(text);\n}\n\nfunction extractResourceQuery(message) {\n const text = safeUserText(message)\n .replace(/请|請|帮我|幫我|麻烦|麻煩|可以|能不能|需要|我要|想要/g, ' ')\n .replace(/下载|下載|查找|寻找|尋找|找一个|找一個|找|资源|資源|并|並|然后|然後|用于|用於|全球法布施|加入功课本|加入功課本/g, ' ')\n .replace(/\\s+/g, ' ')\n .trim();\n return text || safeUserText(message);\n}\n\nfunction resourceContextMessage(skillResult) {\n if (!skillResult) return '';\n const lines = [\n '本轮资源检索/下载已经由大乘 App 后端完成。',\n '不要再次调用 search_dharma_resources、download_dharma_resource 或其他资源 MCP 工具;请直接基于以下结果回答用户。',\n `后端已调用 skill: ${skillResult.skillName}`,\n `执行目标: ${skillResult.query}`,\n ];\n if (skillResult.selected) {\n lines.push(`候选资源: ${skillResult.selected.title}`);\n lines.push(`来源: ${skillResult.selected.sourceName}`);\n lines.push(`资源地址: ${skillResult.selected.url}`);\n }\n if (skillResult.downloaded) {\n lines.push(`下载标题: ${skillResult.downloaded.title}`);\n lines.push(`下载文件: ${skillResult.downloaded.fileName}`);\n const text = safeUserText(skillResult.downloaded.contentText);\n if (text) {\n lines.push('下载正文摘录:');\n lines.push(text.slice(0, 8000));\n }\n }\n return lines.join('\\n');\n}\n\nfunction determineClientAction(skillResult, message) {\n if (skillResult && skillResult.downloaded) {\n const text = safeUserText(message);\n if (/法布施|全球法布施|分享/.test(text)) {\n return {\n type: 'prepare_dharma_share_text',\n title: skillResult.downloaded.title,\n text: skillResult.downloaded.contentText || '',\n };\n }\n }\n return null;\n}\n\nasync function runResourceFinderSkill({ message, user, onStep }) {\n if (!looksLikeResourceTask(message)) return null;\n\n const skillName = 'resource-finder-downloader';\n const query = extractResourceQuery(message);\n const audioTask = looksLikeAudioQuery(message) || looksLikeAudioQuery(query);\n onStep?.({\n type: 'skill.step',\n skillName,\n stage: 'start',\n title: '调用资源查找下载 skill',\n message: `识别到资源任务: ${query}`,\n });\n\n onStep?.({\n type: 'skill.step',\n skillName,\n stage: 'search',\n title: '搜索可分享资源',\n message: audioTask\n ? '优先检索公开可访问音频资源,失败时再回落到佛典正文资源。'\n : '优先检索 CBETA 佛典正文接口,搜索索引不可用时使用内置经典映射。',\n });\n const candidates = await findResourceCandidates(query, 8);\n if (candidates.length === 0) {\n onStep?.({\n type: 'skill.step',\n skillName,\n stage: 'empty',\n title: '没有找到可下载资源',\n message: '后端没有找到明确可下载且可分享的资源。',\n });\n return {\n skillName,\n query,\n candidates: [],\n selected: null,\n downloaded: null,\n };\n }\n\n const selected = candidates[0];\n onStep?.({\n type: 'skill.step',\n skillName,\n stage: 'select',\n title: '选择可信候选资源',\n message: `${selected.title} / ${selected.sourceName}`,\n });\n\n onStep?.({\n type: 'skill.step',\n skillName,\n stage: 'download',\n title: '下载并提取资源',\n message: selected.url,\n });\n const cbeta = parseCbetaResource(selected);\n const internetArchive = parseInternetArchiveResource(selected);\n const downloaded = cbeta\n ? await downloadCbetaResource({ ...selected, ...cbeta })\n : internetArchive\n ? await downloadInternetArchiveAudio({ ...selected, ...internetArchive })\n : await downloadWebResource(selected);\n const persisted = persistDownloadedResource(user, downloaded);\n\n onStep?.({\n type: 'skill.step',\n skillName,\n stage: 'done',\n title: '资源已准备完成',\n message: downloaded.fileName,\n });\n\n return {\n skillName,\n query,\n candidates,\n selected,\n downloaded: {\n ...downloaded,\n downloadId: persisted.id,\n },\n };\n}\n\napp.post(\n '/api/resources/search',\n asyncHandler(async (req, res) => {\n const query = safeUserText(req.body?.query);\n const limit = Math.min(Math.max(Number(req.body?.limit || 12), 1), 20);\n if (query.length < 2) {\n return jsonResponse(res, 400, { success: false, message: 'query is required' });\n }\n\n const items = await findResourceCandidates(query, limit);\n\n jsonResponse(res, 200, {\n success: true,\n source: 'resource-index',\n items,\n });\n }),\n);\n\nfunction parseCbetaResource(input) {\n const work = safeUserText(input.work);\n const juan = Number(input.juan || 1);\n if (work) return { work, juan: Number.isFinite(juan) ? juan : 1 };\n\n const url = safeUserText(input.url);\n const match = /^cbeta:([^:]+):(\\d+)$/i.exec(url);\n if (!match) return null;\n return {\n work: match[1].toUpperCase(),\n juan: Number(match[2] || 1),\n };\n}\n\nfunction parseInternetArchiveResource(input) {\n const url = safeUserText(input.url);\n const match = /^ia:([^:]+)$/i.exec(url);\n const identifier = match?.[1] || safeUserText(input.identifier);\n if (!identifier) return null;\n return { identifier };\n}\n\nfunction safeFileName(value) {\n const normalized = String(value || 'dharma-resource')\n .replace(/[\\\\/:*?\"<>|]+/g, '-')\n .replace(/\\s+/g, '-')\n .replace(/-+/g, '-')\n .replace(/^-|-$/g, '')\n .slice(0, 80);\n return normalized || 'dharma-resource';\n}\n\nfunction extractCbetaHtml(data) {\n const first = Array.isArray(data?.results) ? data.results[0] : null;\n if (typeof first === 'string') return first;\n if (first && typeof first === 'object' && typeof first.html === 'string') return first.html;\n if (typeof data?.html === 'string') return data.html;\n return '';\n}\n\nasync function downloadCbetaResource(resource) {\n const url = buildUrl(cbetaApiRoot, 'juans', {\n work: resource.work,\n juan: resource.juan,\n work_info: 1,\n toc: 1,\n });\n const data = await fetchJson(url);\n const workInfo = data.work_info || {};\n const title = safeUserText(workInfo.title) || safeUserText(resource.title) || resource.work;\n const html = extractCbetaHtml(data);\n const text = cleanExtractedText(stripHtml(html));\n if (!text) throw new Error('CBETA resource returned empty content');\n return {\n title,\n sourceName: 'CBETA 佛典',\n url: `cbeta:${resource.work}:${resource.juan}`,\n contentText: text.slice(0, maxResourceTextChars),\n fileName: `${resource.work}_${resource.juan}_${safeFileName(title)}.txt`,\n };\n}\n\nasync function downloadInternetArchiveAudio(resource) {\n const identifier = safeUserText(resource.identifier);\n if (!identifier) throw new Error('Internet Archive identifier is required');\n const metadata = await fetchJson(`https://archive.org/metadata/${encodeURIComponent(identifier)}`);\n const title = safeUserText(metadata?.metadata?.title) || safeUserText(resource.title) || identifier;\n const files = Array.isArray(metadata?.files) ? metadata.files : [];\n const audioFile = files.find((file) => {\n const name = safeUserText(file?.name);\n const format = safeUserText(file?.format).toLowerCase();\n return /\\.(mp3|m4a|ogg)$/i.test(name) || format.includes('mp3');\n });\n if (!audioFile?.name) {\n throw new Error('Internet Archive audio item has no downloadable audio file');\n }\n const sourceFileName = audioFile.name;\n const fileUrl = `https://archive.org/download/${encodeURIComponent(identifier)}/${sourceFileName\n .split('/')\n .map((part) => encodeURIComponent(part))\n .join('/')}`;\n const response = await fetch(fileUrl, {\n headers: {\n Accept: 'audio/mpeg, audio/*;q=0.9, */*;q=0.5',\n 'User-Agent': 'DachengResourceDownloader/1.0 (+https://ombhrum.com)',\n },\n signal: AbortSignal.timeout(60_000),\n });\n if (!response.ok) {\n throw new Error(`Internet Archive audio download failed: ${response.status}`);\n }\n const binaryContent = Buffer.from(await response.arrayBuffer());\n if (!binaryContent.length) throw new Error('Internet Archive audio returned empty content');\n const sizeMb = (binaryContent.length / 1024 / 1024).toFixed(1);\n return {\n title,\n sourceName: 'Internet Archive 音频',\n url: fileUrl,\n contentText: `已下载音频资源: ${title}\\n文件名: ${sourceFileName}\\n大小: ${sizeMb} MB\\n来源: ${fileUrl}`,\n binaryContent,\n fileName: safeFileName(sourceFileName),\n };\n}\n\nasync function downloadWebResource(input) {\n const url = safeUserText(input.url);\n const parsed = new URL(url);\n if (!['http:', 'https:'].includes(parsed.protocol)) {\n throw new Error('Only http/https resources can be downloaded');\n }\n\n const response = await fetch(parsed, {\n headers: {\n Accept: 'text/html, text/plain, application/xhtml+xml, application/xml;q=0.8, */*;q=0.5',\n 'User-Agent': 'DachengResourceDownloader/1.0 (+https://ombhrum.com)',\n },\n signal: AbortSignal.timeout(25_000),\n });\n if (!response.ok) throw new Error(`Resource download failed: ${response.status}`);\n const contentType = response.headers.get('content-type') || '';\n const body = await response.text();\n const title = safeUserText(input.title) || parsed.hostname;\n const contentText = contentType.includes('html') ? cleanExtractedText(stripHtml(body)) : body.trim();\n if (!contentText) throw new Error('Resource returned empty content');\n return {\n title,\n sourceName: safeUserText(input.sourceName) || parsed.hostname,\n url,\n contentText: contentText.slice(0, maxResourceTextChars),\n fileName: `${safeFileName(title)}.txt`,\n };\n}\n\nfunction persistDownloadedResource(user, content) {\n const id = crypto.randomUUID();\n const fileName = `${id}-${safeFileName(content.fileName)}`;\n const filePath = path.join(resourcesDir, fileName);\n if (content.binaryContent) {\n fs.writeFileSync(filePath, content.binaryContent);\n } else {\n fs.writeFileSync(filePath, content.contentText, 'utf8');\n }\n statements.insertResourceDownload.run({\n id,\n userId: user.userId,\n title: content.title,\n sourceName: content.sourceName,\n sourceUrl: content.url,\n fileName: content.fileName,\n filePath,\n createdAt: nowIso(),\n });\n return { id, fileName, filePath };\n}\n\nfunction createLibreChatMcpServer() {\n const server = new McpServer({\n name: 'dacheng-ai-tools',\n version: '0.1.0',\n });\n\n server.registerTool(\n 'search_dharma_resources',\n {\n title: 'Search dharma resources',\n description:\n 'Search for legally shareable Buddhist scripture, text, web, or audio resources that can be prepared for Fabushi sharing.',\n inputSchema: {\n query: z.string().min(2).max(200),\n limit: z.number().int().min(1).max(12).optional(),\n },\n outputSchema: {\n query: z.string(),\n items: z.array(\n z.object({\n id: z.string(),\n title: z.string(),\n sourceName: z.string(),\n url: z.string(),\n snippet: z.string(),\n resourceType: z.string(),\n work: z.string().optional(),\n juan: z.number().optional(),\n }),\n ),\n },\n annotations: {\n readOnlyHint: true,\n destructiveHint: false,\n openWorldHint: true,\n },\n },\n async ({ query, limit }) => {\n const items = await findResourceCandidates(query, limit ?? 8);\n return textToolResult(`Found ${items.length} candidate resources for \"${query}\".`, {\n query,\n items,\n });\n },\n );\n\n server.registerTool(\n 'download_dharma_resource',\n {\n title: 'Download dharma resource',\n description:\n 'Download and extract a selected resource. Use the url/work/juan returned by search_dharma_resources whenever possible.',\n inputSchema: {\n url: z.string().min(1).max(1000),\n title: z.string().max(300).optional(),\n sourceName: z.string().max(120).optional(),\n work: z.string().max(40).optional(),\n juan: z.number().int().min(1).optional(),\n identifier: z.string().max(300).optional(),\n },\n outputSchema: {\n title: z.string(),\n sourceName: z.string(),\n url: z.string(),\n fileName: z.string(),\n downloadId: z.string(),\n contentText: z.string(),\n },\n annotations: {\n readOnlyHint: false,\n destructiveHint: false,\n openWorldHint: true,\n },\n },\n async (input) => {\n const user = { userId: 'librechat:mcp' };\n const cbeta = parseCbetaResource(input);\n const internetArchive = parseInternetArchiveResource(input);\n const content = cbeta\n ? await downloadCbetaResource({ ...input, ...cbeta })\n : internetArchive\n ? await downloadInternetArchiveAudio({ ...input, ...internetArchive })\n : await downloadWebResource(input);\n const persisted = persistDownloadedResource(user, content);\n const { binaryContent: _binaryContent, ...publicContent } = content;\n return textToolResult(`Downloaded ${publicContent.title} as ${publicContent.fileName}.`, {\n ...publicContent,\n downloadId: persisted.id,\n });\n },\n );\n\n server.registerTool(\n 'prepare_dharma_share_text',\n {\n title: 'Prepare dharma sharing text',\n description:\n 'Prepare a text payload for the Fabushi app to confirm and add to global dharma sharing. This returns a clientAction; it does not start sending.',\n inputSchema: {\n title: z.string().min(1).max(120),\n text: z.string().min(1).max(20000),\n },\n outputSchema: {\n clientAction: z.object({\n type: z.literal('prepare_dharma_share_text'),\n title: z.string(),\n text: z.string(),\n }),\n },\n annotations: {\n readOnlyHint: false,\n destructiveHint: false,\n openWorldHint: false,\n },\n },\n async ({ title, text }) =>\n textToolResult(`Prepared \"${title}\" for user confirmation in the Fabushi app.`, {\n clientAction: {\n type: 'prepare_dharma_share_text',\n title: safeUserText(title).slice(0, 120),\n text: String(text || '').trim().slice(0, 20000),\n },\n }),\n );\n\n server.registerTool(\n 'prepare_practice_book_item',\n {\n title: 'Prepare practice book item',\n description:\n 'Prepare a scripture, chant, or practice item for the Fabushi app practice book. This returns a clientAction; it does not mutate the app by itself.',\n inputSchema: {\n title: z.string().min(1).max(120),\n sourceName: z.string().max(120).optional(),\n text: z.string().min(1).max(20000),\n },\n outputSchema: {\n clientAction: z.object({\n type: z.literal('prepare_practice_book_item'),\n title: z.string(),\n sourceName: z.string(),\n text: z.string(),\n }),\n },\n annotations: {\n readOnlyHint: false,\n destructiveHint: false,\n openWorldHint: false,\n },\n },\n async ({ title, sourceName, text }) =>\n textToolResult(`Prepared \"${title}\" for the Fabushi practice book confirmation flow.`, {\n clientAction: {\n type: 'prepare_practice_book_item',\n title: safeUserText(title).slice(0, 120),\n sourceName: safeUserText(sourceName || '大乘 AI').slice(0, 120),\n text: String(text || '').trim().slice(0, 20000),\n },\n }),\n );\n\n return server;\n}\n\nasync function handleMcpRequest(req, res) {\n const server = createLibreChatMcpServer();\n const transport = new StreamableHTTPServerTransport({\n sessionIdGenerator: undefined,\n enableJsonResponse: true,\n });\n\n res.on('close', () => {\n transport.close();\n server.close();\n });\n\n await server.connect(transport);\n await transport.handleRequest(req, res, req.body);\n}\n\napp.all('/mcp', async (req, res) => {\n try {\n await handleMcpRequest(req, res);\n } catch (error) {\n logger.error({ error: error.stack || String(error) }, 'MCP request failed');\n if (!res.headersSent) {\n res.status(500).json({\n error: 'MCP request failed',\n message: publicAiErrorMessage(error),\n });\n }\n }\n});\n\napp.post(\n '/api/resources/download',\n asyncHandler(async (req, res) => {\n const user = await resolveUser(req, req.body || {});\n const cbeta = parseCbetaResource(req.body || {});\n const internetArchive = parseInternetArchiveResource(req.body || {});\n const content = cbeta\n ? await downloadCbetaResource({ ...req.body, ...cbeta })\n : internetArchive\n ? await downloadInternetArchiveAudio({ ...req.body, ...internetArchive })\n : await downloadWebResource(req.body || {});\n const persisted = persistDownloadedResource(user, content);\n const { binaryContent: _binaryContent, ...publicContent } = content;\n jsonResponse(res, 200, {\n success: true,\n downloadId: persisted.id,\n ...publicContent,\n });\n }),\n);\n\napp.post(\n '/api/codex/resource-task',\n asyncHandler(async (req, res) => {\n if (process.env.ENABLE_CODEX_SDK !== 'true') {\n return jsonResponse(res, 403, {\n success: false,\n message: 'Codex SDK task runner is disabled. Set ENABLE_CODEX_SDK=true on the server to enable it.',\n });\n }\n const prompt = safeUserText(req.body?.prompt);\n if (!prompt) {\n return jsonResponse(res, 400, { success: false, message: 'prompt is required' });\n }\n const user = await resolveUser(req, req.body || {});\n const { Codex } = await import('@openai/codex-sdk');\n const codexRuntime = createCodexDeepSeekRuntime(user);\n const codex = new Codex(codexRuntime.enabled ? codexRuntime.options : {});\n const thread = codex.startThread(\n codexRuntime.enabled\n ? codexRuntime.threadOptions\n : { skipGitRepoCheck: true, networkAccessEnabled: true },\n );\n const result = await thread.run(prompt);\n const usage = {\n promptTokens: Number(result.usage?.input_tokens || 0),\n completionTokens: Number(result.usage?.output_tokens || 0),\n totalTokens: Number((result.usage?.input_tokens || 0) + (result.usage?.output_tokens || 0)),\n };\n if (!codexRuntime.enabled) {\n recordUsage(user.userId, usage.totalTokens || estimateTokens(JSON.stringify(result)));\n }\n jsonResponse(res, 200, {\n success: true,\n threadId: thread.id,\n provider: codexRuntime.provider,\n model: deepseekModel,\n usage,\n result,\n });\n }),\n);\n\nregisterPlatformApi({ app, db, resolveUser, asyncHandler });\n\napp.use((error, _req, res, _next) => {\n const status = Number(error.statusCode || error.status || 500);\n const safeStatus = status >= 400 && status < 600 ? status : 500;\n logger.error({ error: error.stack || String(error) }, 'request failed');\n jsonResponse(res, safeStatus, {\n success: false,\n message: error.message || 'Internal Server Error',\n details: error.details,\n });\n});\n\nconst port = Number(process.env.PORT || 8788);\napp.listen(port, '0.0.0.0', () => {\n logger.info(\n {\n port,\n dbPath,\n dataDir,\n deepseekModel,\n cbetaApiRoot,\n },\n 'Dacheng AI backend started',\n );\n});\n"}, "6350892067731a13abd2a62d2fcebe7fc6a97fc4": {"t": 1785757957.511994, "n": 151280, "h": "15a1c5d921721132", "c": "import crypto from 'node:crypto';\nimport fs from 'node:fs';\nimport path from 'node:path';\nimport process from 'node:process';\nimport { fileURLToPath } from 'node:url';\n\nimport { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';\nimport { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';\nimport Database from 'better-sqlite3';\nimport cors from 'cors';\nimport dotenv from 'dotenv';\nimport express from 'express';\nimport rateLimit from 'express-rate-limit';\nimport helmet from 'helmet';\nimport pino from 'pino';\nimport { z } from 'zod';\n\nimport { registerPlatformApi } from './platform_api.js';\n\ndotenv.config();\n\nconst __dirname = path.dirname(fileURLToPath(import.meta.url));\nconst rootDir = path.resolve(__dirname, '..');\nconst dataDir = process.env.DATA_DIR || path.join(rootDir, 'data');\nconst resourcesDir = path.join(dataDir, 'resources');\nconst miniAppsDir = path.join(dataDir, 'miniapps');\nconst miniAppsStorePath = path.join(miniAppsDir, 'sandbox-miniapps.json');\nconst openClawRuntimeUpdatesDir =\n process.env.OPENCLAW_RUNTIME_UPDATES_DIR || path.join(dataDir, 'openclaw-runtime');\nconst openClawRuntimeEnableMacosUpdates =\n process.env.OPENCLAW_RUNTIME_ENABLE_MACOS_UPDATES === 'true';\nconst codexHomeDir = process.env.CODEX_HOME || path.join(dataDir, 'codex-home');\nconst codexTempDir = process.env.CODEX_TMPDIR || path.join(dataDir, 'codex-tmp');\nconst codexRuntimeDir = process.env.XDG_RUNTIME_DIR || path.join(dataDir, 'codex-runtime');\nconst dbPath = process.env.SQLITE_PATH || path.join(dataDir, 'dacheng-ai.sqlite');\n\nfs.mkdirSync(resourcesDir, { recursive: true });\nfs.mkdirSync(miniAppsDir, { recursive: true });\nfs.mkdirSync(openClawRuntimeUpdatesDir, { recursive: true });\nfs.mkdirSync(codexHomeDir, { recursive: true });\nfs.mkdirSync(codexTempDir, { recursive: true });\nfs.mkdirSync(codexRuntimeDir, { recursive: true });\n\nconst logger = pino({\n level: process.env.LOG_LEVEL || 'info',\n});\n\nconst app = express();\napp.disable('x-powered-by');\napp.set('trust proxy', Number(process.env.TRUST_PROXY_HOPS || 1));\napp.use(helmet());\nconst corsOrigin =\n !process.env.CORS_ORIGIN || process.env.CORS_ORIGIN.trim() === '*'\n ? true\n : process.env.CORS_ORIGIN.split(',').map((item) => item.trim()).filter(Boolean);\napp.use(cors({ origin: corsOrigin }));\napp.use(express.json({ limit: '1mb' }));\napp.use(\n rateLimit({\n windowMs: 60_000,\n limit: Number(process.env.RATE_LIMIT_PER_MINUTE || 90),\n standardHeaders: true,\n legacyHeaders: false,\n }),\n);\n\nconst db = new Database(dbPath);\ndb.pragma('journal_mode = WAL');\ndb.pragma('foreign_keys = ON');\n\ndb.exec(`\n CREATE TABLE IF NOT EXISTS conversations (\n id TEXT PRIMARY KEY,\n user_id TEXT NOT NULL,\n title TEXT NOT NULL,\n provider TEXT NOT NULL,\n model TEXT NOT NULL,\n created_at TEXT NOT NULL,\n updated_at TEXT NOT NULL\n );\n\n CREATE INDEX IF NOT EXISTS idx_conversations_user_updated\n ON conversations (user_id, updated_at DESC);\n\n CREATE TABLE IF NOT EXISTS messages (\n id TEXT PRIMARY KEY,\n conversation_id TEXT NOT NULL,\n role TEXT NOT NULL,\n content TEXT NOT NULL,\n prompt_tokens INTEGER NOT NULL DEFAULT 0,\n completion_tokens INTEGER NOT NULL DEFAULT 0,\n total_tokens INTEGER NOT NULL DEFAULT 0,\n created_at TEXT NOT NULL,\n FOREIGN KEY (conversation_id) REFERENCES conversations(id) ON DELETE CASCADE\n );\n\n CREATE INDEX IF NOT EXISTS idx_messages_conversation_created\n ON messages (conversation_id, created_at ASC);\n\n CREATE TABLE IF NOT EXISTS usage_months (\n user_id TEXT NOT NULL,\n month TEXT NOT NULL,\n total_tokens INTEGER NOT NULL DEFAULT 0,\n updated_at TEXT NOT NULL,\n PRIMARY KEY (user_id, month)\n );\n\n CREATE TABLE IF NOT EXISTS resource_downloads (\n id TEXT PRIMARY KEY,\n user_id TEXT NOT NULL,\n title TEXT NOT NULL,\n source_name TEXT NOT NULL,\n source_url TEXT NOT NULL,\n file_name TEXT NOT NULL,\n file_path TEXT NOT NULL,\n created_at TEXT NOT NULL\n );\n\n CREATE TABLE IF NOT EXISTS agent_runs (\n id TEXT PRIMARY KEY,\n user_id TEXT NOT NULL,\n conversation_id TEXT NOT NULL,\n message_id TEXT NOT NULL,\n openclaw_run_id TEXT,\n openclaw_session_key TEXT NOT NULL,\n status TEXT NOT NULL,\n mode TEXT NOT NULL,\n provider TEXT NOT NULL,\n model TEXT,\n input_tokens INTEGER NOT NULL DEFAULT 0,\n output_tokens INTEGER NOT NULL DEFAULT 0,\n tool_call_count INTEGER NOT NULL DEFAULT 0,\n cost_microusd INTEGER NOT NULL DEFAULT 0,\n started_at TEXT NOT NULL,\n completed_at TEXT,\n failed_at TEXT,\n error_code TEXT,\n error_message TEXT\n );\n\n CREATE INDEX IF NOT EXISTS idx_agent_runs_user_started\n ON agent_runs (user_id, started_at DESC);\n\n CREATE INDEX IF NOT EXISTS idx_agent_runs_status_started\n ON agent_runs (status, started_at DESC);\n\n CREATE TABLE IF NOT EXISTS agent_message_feedback (\n id TEXT PRIMARY KEY,\n message_id TEXT NOT NULL,\n user_id TEXT NOT NULL,\n rating TEXT NOT NULL,\n reason TEXT,\n comment TEXT,\n created_at TEXT NOT NULL\n );\n`);\n\nconst statements = {\n insertConversation: db.prepare(`\n INSERT INTO conversations (id, user_id, title, provider, model, created_at, updated_at)\n VALUES (@id, @userId, @title, @provider, @model, @createdAt, @updatedAt)\n `),\n updateConversation: db.prepare(`\n UPDATE conversations\n SET title = @title, updated_at = @updatedAt\n WHERE id = @id AND user_id = @userId\n `),\n getConversation: db.prepare(`\n SELECT * FROM conversations WHERE id = ? AND user_id = ? LIMIT 1\n `),\n listConversations: db.prepare(`\n SELECT id, title, updated_at AS updatedAt\n FROM conversations\n WHERE user_id = ?\n ORDER BY updated_at DESC\n LIMIT 30\n `),\n insertMessage: db.prepare(`\n INSERT INTO messages (\n id, conversation_id, role, content,\n prompt_tokens, completion_tokens, total_tokens, created_at\n )\n VALUES (\n @id, @conversationId, @role, @content,\n @promptTokens, @completionTokens, @totalTokens, @createdAt\n )\n `),\n listMessages: db.prepare(`\n SELECT role, content, prompt_tokens AS promptTokens,\n completion_tokens AS completionTokens, total_tokens AS totalTokens,\n created_at AS createdAt\n FROM messages\n WHERE conversation_id = ?\n ORDER BY created_at ASC\n `),\n getUsage: db.prepare(`\n SELECT total_tokens AS totalTokens FROM usage_months WHERE user_id = ? AND month = ?\n `),\n upsertUsage: db.prepare(`\n INSERT INTO usage_months (user_id, month, total_tokens, updated_at)\n VALUES (@userId, @month, @totalTokens, @updatedAt)\n ON CONFLICT(user_id, month)\n DO UPDATE SET total_tokens = usage_months.total_tokens + excluded.total_tokens,\n updated_at = excluded.updated_at\n `),\n insertResourceDownload: db.prepare(`\n INSERT INTO resource_downloads (\n id, user_id, title, source_name, source_url, file_name, file_path, created_at\n )\n VALUES (\n @id, @userId, @title, @sourceName, @sourceUrl, @fileName, @filePath, @createdAt\n )\n `),\n insertAgentRun: db.prepare(`\n INSERT INTO agent_runs (\n id, user_id, conversation_id, message_id, openclaw_run_id, openclaw_session_key,\n status, mode, provider, model, started_at\n )\n VALUES (\n @id, @userId, @conversationId, @messageId, @openClawRunId, @openClawSessionKey,\n @status, @mode, @provider, @model, @startedAt\n )\n `),\n getAgentRun: db.prepare(`\n SELECT * FROM agent_runs WHERE id = ? AND user_id = ? LIMIT 1\n `),\n updateAgentRunStatus: db.prepare(`\n UPDATE agent_runs\n SET status = @status,\n openclaw_run_id = COALESCE(@openClawRunId, openclaw_run_id),\n input_tokens = COALESCE(@inputTokens, input_tokens),\n output_tokens = COALESCE(@outputTokens, output_tokens),\n tool_call_count = COALESCE(@toolCallCount, tool_call_count),\n completed_at = @completedAt,\n failed_at = @failedAt,\n error_code = @errorCode,\n error_message = @errorMessage\n WHERE id = @id AND user_id = @userId\n `),\n insertAgentFeedback: db.prepare(`\n INSERT INTO agent_message_feedback (id, message_id, user_id, rating, reason, comment, created_at)\n VALUES (@id, @messageId, @userId, @rating, @reason, @comment, @createdAt)\n `),\n};\n\nconst deepseekApiKey = process.env.DEEPSEEK_API_KEY || '';\nconst deepseekBaseUrl = (process.env.DEEPSEEK_BASE_URL || 'https://api.deepseek.com').replace(/\\/+$/, '');\nconst deepseekModel = process.env.DEEPSEEK_MODEL || 'deepseek-chat';\nconst deepseekAvailableModels = Array.from(\n new Set(\n (process.env.DEEPSEEK_MODELS || `${deepseekModel},deepseek-chat,deepseek-reasoner`)\n .split(',')\n .map((item) => normalizeDeepSeekModelName(item))\n .filter(Boolean),\n ),\n);\nconst cbetaApiRoot = (process.env.CBETA_API_ROOT || 'https://api.ombhrum.com/api/cbeta').replace(/\\/+$/, '');\nconst fabushiApiBaseUrl = (process.env.FABUSHI_API_BASE_URL || 'https://api.ombhrum.com').replace(/\\/+$/, '');\nconst memberMonthlyLimit = Number(process.env.MEMBER_MONTHLY_TOKEN_LIMIT || 1_000_000);\nconst freeMonthlyLimit = Number(process.env.FREE_MONTHLY_TOKEN_LIMIT || 1_000);\nconst requireMemberForAi = process.env.REQUIRE_MEMBER_FOR_AI === 'true';\nconst deepseekMaxCompletionTokens = Math.max(1, Number(process.env.DEEPSEEK_MAX_COMPLETION_TOKENS || 1400));\nconst maxResourceTextChars = Number(process.env.MAX_RESOURCE_TEXT_CHARS || 80_000);\nconst openClawRuntimeManifestPath =\n process.env.OPENCLAW_RUNTIME_MANIFEST_PATH || path.join(openClawRuntimeUpdatesDir, 'manifest.json');\nconst openClawRuntimePublicBaseUrl = (process.env.OPENCLAW_RUNTIME_PUBLIC_BASE_URL || '').replace(/\\/+$/, '');\nconst enableLibreChatAgentChat = process.env.ENABLE_LIBRECHAT_AGENT_CHAT === 'true';\nconst libreChatAgentsBaseUrl = (\n process.env.LIBRECHAT_AGENTS_BASE_URL || 'http://127.0.0.1:3080/api/agents/v1'\n).replace(/\\/+$/, '');\nconst libreChatAgentApiKey = process.env.LIBRECHAT_AGENT_API_KEY || '';\nconst libreChatAgentId = process.env.LIBRECHAT_AGENT_ID || '';\nconst enableCodexSdkChat = process.env.ENABLE_CODEX_SDK_CHAT === 'true';\nconst enableOpenClawAgentChat = process.env.ENABLE_OPENCLAW_AGENT_CHAT === 'true';\nconst openClawGatewayUrl = (process.env.OPENCLAW_GATEWAY_URL || 'http://127.0.0.1:18789').replace(/\\/+$/, '');\nconst openClawGatewayToken = process.env.OPENCLAW_GATEWAY_TOKEN || '';\nconst openClawAgentId = process.env.OPENCLAW_AGENT_ID || 'fabushi-public-agent';\nconst openClawRunsEndpoint = process.env.OPENCLAW_RUNS_ENDPOINT || '';\nconst codexDeepSeekProviderId = 'dacheng-deepseek-proxy';\nconst codexResponsesBaseUrl = (\n process.env.CODEX_DEEPSEEK_RESPONSES_BASE_URL ||\n `http://127.0.0.1:${process.env.PORT || 8788}/codex-deepseek/v1`\n).replace(/\\/+$/, '');\nconst codexAdapterTokenPrefix = 'dacheng-codex-proxy.';\nconst codexAdapterTokenTtlMs = Math.max(\n 60_000,\n Number(process.env.CODEX_DEEPSEEK_ADAPTER_TOKEN_TTL_MS || 15 * 60_000),\n);\nconst codexAdapterSecret =\n process.env.CODEX_DEEPSEEK_ADAPTER_SECRET ||\n deepseekApiKey ||\n openClawGatewayToken ||\n 'dacheng-codex-local-dev-secret';\n\nconst systemPrompt = [\n '你是“大乘”App 的 AI 助手。',\n '你的核心任务是帮助用户查找、整理、理解并全球法布施合法可分享的佛法资源。',\n '回答要庄重、简洁、可执行;涉及经典、仪轨或资源时,提醒用户尊重版权、来源和当地法规。',\n '如果用户想找资源、下载经文、寻找音频或准备可分享资料,你必须先在后端自动执行搜索、验证和下载步骤;不要要求用户去前端 + 菜单手动查找。',\n '你可以把执行进度总结给用户,但不要输出隐藏推理原文。',\n].join('\\n');\n\nfunction nowIso() {\n return new Date().toISOString();\n}\n\nfunction monthKey(date = new Date()) {\n return `${date.getUTCFullYear()}-${String(date.getUTCMonth() + 1).padStart(2, '0')}`;\n}\n\nfunction jsonResponse(res, status, payload) {\n res.status(status).json(payload);\n}\n\nasync function readJsonFile(filePath) {\n const raw = await fs.promises.readFile(filePath, 'utf8');\n return JSON.parse(raw);\n}\n\nfunction requestOrigin(req) {\n if (openClawRuntimePublicBaseUrl) return openClawRuntimePublicBaseUrl;\n const proto = req.get('X-Forwarded-Proto') || req.protocol || 'https';\n const host = req.get('X-Forwarded-Host') || req.get('Host');\n return host ? `${proto}://${host}` : '';\n}\n\nfunction absolutizeOpenClawRuntimeUrl(req, value) {\n const text = readText(value);\n if (!text) return '';\n if (/^https?:\\/\\//i.test(text)) return text;\n const normalized = text.startsWith('/')\n ? text\n : `/api/openclaw/runtime/files/${text.replace(/^\\/+/, '')}`;\n const origin = requestOrigin(req);\n return origin ? new URL(normalized, `${origin}/`).toString() : normalized;\n}\n\nfunction textToolResult(text, structuredContent = {}) {\n return {\n structuredContent,\n content: [{ type: 'text', text }],\n };\n}\n\nfunction sseHeaders(res) {\n res.status(200);\n res.setHeader('Content-Type', 'text/event-stream; charset=utf-8');\n res.setHeader('Cache-Control', 'no-cache, no-transform');\n res.setHeader('Connection', 'keep-alive');\n res.setHeader('X-Accel-Buffering', 'no');\n if (typeof res.flushHeaders === 'function') res.flushHeaders();\n}\n\nfunction writeSse(res, event, payload) {\n res.write(`event: ${event}\\n`);\n res.write(`data: ${JSON.stringify(payload)}\\n\\n`);\n}\n\nfunction asyncHandler(fn) {\n return (req, res, next) => {\n Promise.resolve(fn(req, res, next)).catch(next);\n };\n}\n\nfunction bearerToken(req) {\n const delegated = readText(req.get('x-dacheng-auth-token'));\n if (\n delegated &&\n delegated !== 'DACHENG_AUTH_TOKEN' &&\n !delegated.startsWith('secretref-env:') &&\n delegated !== 'dacheng-openclaw-proxy'\n ) {\n return delegated;\n }\n const header = req.get('Authorization') || '';\n if (!header.startsWith('Bearer ')) return '';\n const token = header.slice('Bearer '.length).trim();\n return token === 'dacheng-openclaw-proxy' ? '' : token;\n}\n\nfunction sha256(value) {\n return crypto.createHash('sha256').update(value).digest('hex');\n}\n\nfunction safeUserText(value, fallback = '') {\n if (typeof value !== 'string') return fallback;\n return value.replace(/\\u0000/g, '').trim();\n}\n\nfunction estimateTokens(text) {\n const normalized = String(text || '').trim();\n if (!normalized) return 0;\n return Math.max(1, Math.ceil(normalized.length / 3));\n}\n\nfunction readText(value) {\n if (value === undefined || value === null) return '';\n return String(value).replace(/\\u0000/g, '').trim();\n}\n\nconst miniAppHostApiVersion = '2.0';\nconst miniAppHighRiskPermissions = new Set([\n 'auth.token',\n 'payments.alipay',\n 'payments.fudeGold',\n 'desktop.control',\n 'network.http',\n 'network.udp',\n 'network.interfaces',\n 'system.keepAwake',\n 'hotspot.settings',\n 'local.loopback',\n 'fs.readWrite',\n 'runtime.process',\n 'shell.execute',\n 'browser.external',\n 'files.write',\n 'projects.write',\n 'openclaw.restart',\n]);\n\nfunction officialMiniAppRegistry(req) {\n const webBase = process.env.MINIAPP_WEB_BASE_URL || 'https://fabushi.ombhrum.com';\n const official = [\n {\n botId: 'bot.global-dharma',\n miniAppId: 'official.global-dharma',\n title: '全球法布施',\n subtitle: '地区、循环、场能都在对话框上方设置',\n initials: '法',\n icon: 'public',\n avatarColor: '#4CAF7A',\n kind: 'global_dharma',\n entryUrl: process.env.GLOBAL_DHARMA_MINIAPP_URL,\n greeting: '把要分享的文字、链接或素材发给我,我会按上方选择的地区与模式启动全球法布施。',\n inputHint: '输入要法布施的文字/链接,或点 + 添加素材',\n permissions: [\n 'app.context',\n 'bot.chat',\n 'auth.session',\n 'wallet.balance',\n 'payments.entitlement',\n 'payments.fudeGold',\n 'payments.alipay',\n 'files.pick',\n 'network.http',\n 'network.udp',\n 'network.interfaces',\n 'system.keepAwake',\n 'hotspot.settings',\n 'local.loopback',\n 'openclaw.status',\n 'openclaw.chat',\n 'desktop.control',\n 'fs.readWrite',\n 'runtime.process',\n ],\n },\n {\n botId: 'bot.flashcards',\n miniAppId: 'official.flashcards',\n title: '背诵闪卡制作',\n subtitle: '随机挖空 / AI 制卡从顶部模式按钮选择',\n initials: '卡',\n icon: 'flashcards',\n avatarColor: '#7E57C2',\n kind: 'flashcards',\n entryUrl: process.env.FLASHCARDS_MINIAPP_URL,\n greeting: '粘贴经文、文章正文或链接即可制作背诵闪卡。制卡模式请在上方按钮切换。',\n inputHint: '粘贴链接或正文,发送后制作闪卡',\n permissions: ['app.context', 'bot.chat', 'flashcards.create', 'openclaw.status', 'openclaw.chat'],\n },\n {\n botId: 'bot.platform-publish',\n miniAppId: 'official.platform-publish',\n title: '法布施到平台',\n subtitle: '选择平台后生成发布草稿并打开入口',\n initials: '发',\n icon: 'publish',\n avatarColor: '#FF9F43',\n kind: 'platform_publish',\n entryUrl: process.env.PLATFORM_PUBLISH_MINIAPP_URL,\n greeting: '把要发布的正文或链接发给我,上方选择平台后,我会生成发布草稿并打开对应平台入口。',\n inputHint: '输入要发布到平台的正文/链接',\n permissions: [\n 'app.context',\n 'bot.chat',\n 'platform.publish',\n 'files.pick',\n 'fs.readWrite',\n 'runtime.process',\n 'shell.execute',\n 'browser.external',\n ],\n },\n {\n botId: 'bot.father',\n miniAppId: 'official.bot-father',\n title: '机器人之父',\n subtitle: '用对话生成个人沙箱小程序',\n initials: '父',\n icon: 'bot_father',\n avatarColor: '#3D8BFF',\n kind: 'bot_father',\n entryUrl: process.env.BOT_FATHER_MINIAPP_URL,\n greeting: '告诉我你想要的小程序,我会生成 manifest、界面和权限声明,并放进你的个人沙箱。',\n inputHint: '描述你想创建的小程序',\n permissions: ['app.context', 'bot.chat', 'miniapps.dev'],\n },\n ];\n return {\n bots: official.map((bot, index) => ({\n ...bot,\n destinationIndex: index,\n source: 'official',\n })),\n miniApps: official.map((bot) => ({\n miniAppId: bot.miniAppId,\n botId: bot.botId,\n title: bot.title,\n subtitle: bot.subtitle,\n entryUrl:\n bot.entryUrl ||\n `${webBase.replace(/\\/+$/, '')}/miniapps/${encodeURIComponent(bot.miniAppId)}`,\n version: '2.0.0',\n permissions: bot.permissions,\n surfaces: ['homePinned', 'chatPanel'],\n theme: 'telegramDark',\n signature: 'official',\n reviewStatus: 'trusted',\n source: 'official',\n })),\n };\n}\n\nasync function readMiniAppStore() {\n try {\n const raw = await fs.promises.readFile(miniAppsStorePath, 'utf8');\n const decoded = JSON.parse(raw);\n return {\n apps: Array.isArray(decoded.apps) ? decoded.apps : [],\n };\n } catch (error) {\n if (error?.code === 'ENOENT') return { apps: [] };\n throw error;\n }\n}\n\nasync function writeMiniAppStore(store) {\n await fs.promises.mkdir(miniAppsDir, { recursive: true });\n await fs.promises.writeFile(\n miniAppsStorePath,\n JSON.stringify({ apps: store.apps || [] }, null, 2),\n );\n}\n\nfunction miniAppEntryUrl(req, miniAppId) {\n const origin = requestOrigin(req) || '';\n const pathName = `/api/miniapps/dev/${encodeURIComponent(miniAppId)}/index.html`;\n return origin ? new URL(pathName, `${origin}/`).toString() : pathName;\n}\n\nfunction sanitizeMiniAppPermissions(value, { allowHighRisk = false } = {}) {\n const raw = Array.isArray(value) ? value : [];\n const normalized = raw\n .map((item) => readText(item).toLowerCase())\n .filter((item) => /^[a-z][a-z0-9_.:-]{0,64}$/.test(item));\n const deduped = [...new Set(['app.context', 'bot.chat', ...normalized])];\n return deduped.filter((permission) => allowHighRisk || !miniAppHighRiskPermissions.has(permission));\n}\n\nfunction miniAppScan(permissions, sourceHtml = '') {\n const highRisk = permissions.filter((permission) => miniAppHighRiskPermissions.has(permission));\n const blockedPatterns = [\n /authorization\\s*:\\s*['\"`]bearer/i,\n /desktop_control_bridge_token/i,\n /openclaw_gateway_token/i,\n /eval\\s*\\(/i,\n ];\n const blocked = blockedPatterns\n .filter((pattern) => pattern.test(sourceHtml))\n .map((pattern) => pattern.toString());\n return {\n passed: blocked.length === 0,\n highRiskPermissions: highRisk,\n blockedPatterns: blocked,\n };\n}\n\nfunction inferMiniAppPermissionsFromText(...values) {\n const text = values.map((value) => readText(value)).join('\\n').toLowerCase();\n const rules = [\n ['auth.session', [/auth\\.(getsession|requirelogin)/, /登录|用户信息|宿主账号/]],\n ['auth.token', [/auth\\.getaccesstoken/, /access token|访问 token/]],\n ['payments.alipay', [/payments\\.alipay/, /支付宝|支付|购买|订单/]],\n ['payments.entitlement', [/payments\\.checkentitlement|权益|解锁状态|购买状态/]],\n ['payments.fudeGold', [/payments\\.requestpayment|福德金|代币|钱包|余额/]],\n ['wallet.balance', [/wallet\\.getbalance|福德金|代币|钱包|余额/]],\n ['network.http', [/network\\.http|http\\.fetch|fetch\\s*\\(/, /链接正文|抓取网页|http 请求|网页内容/]],\n ['network.udp', [/network\\.udp|udp|datagram/, /全球法布施|法布施发送|数据报/]],\n ['network.interfaces', [/network\\.interfaces|网卡|网络接口|广播地址/]],\n ['system.keepAwake', [/system\\.keepawake|保持唤醒|后台任务|keep awake/]],\n ['hotspot.settings', [/hotspot\\.opensettings|热点|本地场能/]],\n ['local.loopback', [/localloopback\\./, /localhost|127\\.0\\.0\\.1|本地回环|本地转经轮/]],\n ['flashcards.create', [/flashcards\\./, /闪卡|背诵卡|挖空/]],\n ['platform.publish', [/platformpublish\\./, /发布草稿|发布到平台|公众号|小红书|知乎/]],\n ['files.pick', [/files\\.pick/, /选择文件|上传文件|本地素材/]],\n ['fs.readWrite', [/fs\\.(writefile|readfile)/, /本地文件|写文件|读文件|文件处理/]],\n ['runtime.process', [/runtime\\.process|process\\.execute/, /本地进程|执行程序|命令行|cli|playwright/]],\n ['shell.execute', [/shell\\.execute/, /shell/]],\n ['browser.external', [/browser\\.open/, /浏览器|打开网页|打开平台/]],\n ['desktop.control', [/desktopcontrol\\./, /桌面控制|点击|键盘|鼠标/]],\n ['openclaw.chat', [/openclaw\\.chat/, /openclaw/]],\n ];\n return rules\n .filter(([, patterns]) => patterns.some((pattern) => pattern.test(text)))\n .map(([permission]) => permission);\n}\n\nfunction miniAppRecordToBot(record) {\n return {\n botId: record.botId,\n miniAppId: record.miniAppId,\n title: record.title,\n subtitle: record.subtitle,\n initials: record.initials || '小',\n icon: record.icon || 'code',\n avatarColor: record.avatarColor || '#3D8BFF',\n kind: 'third_party',\n greeting: record.greeting || '你好,我是这个小程序的机器人。',\n inputHint: record.inputHint || '输入消息,或打开小程序面板',\n permissions: record.permissions || ['app.context', 'bot.chat'],\n source: record.source || 'sandbox',\n };\n}\n\nfunction miniAppRecordToManifest(req, record) {\n return {\n miniAppId: record.miniAppId,\n botId: record.botId,\n title: record.title,\n subtitle: record.subtitle,\n entryUrl: miniAppEntryUrl(req, record.miniAppId),\n version: record.version || '0.0.1',\n permissions: record.permissions || ['app.context', 'bot.chat'],\n surfaces: record.surfaces || ['chatPanel'],\n theme: record.theme || 'telegramDark',\n signature: record.signature || `sandbox:${record.ownerId}`,\n reviewStatus: record.reviewStatus || 'sandbox',\n source: record.source || 'sandbox',\n };\n}\n\nfunction safeMiniAppId(value) {\n return readText(value).replace(/[^a-zA-Z0-9_.-]/g, '-').slice(0, 80);\n}\n\nfunction titleFromPromptForMiniApp(prompt) {\n const text = readText(prompt).replace(/\\s+/g, ' ');\n if (!text) return '个人小程序';\n return text.length <= 18 ? text : text.slice(0, 18);\n}\n\nfunction escapeHtml(value) {\n return String(value || '')\n .replace(/&/g, '&')\n .replace(/</g, '<')\n .replace(/>/g, '>')\n .replace(/\"/g, '"')\n .replace(/'/g, ''');\n}\n\nfunction generatedMiniAppHtml({ title, prompt }) {\n const safeTitle = escapeHtml(title);\n const safePrompt = escapeHtml(prompt);\n const promptJson = JSON.stringify(readText(prompt));\n return `<!doctype html>\n<html lang=\"zh-Hans\">\n<head>\n <meta charset=\"utf-8\" />\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n <title>${safeTitle}</title>\n <style>\n body { margin:0; font-family:-apple-system,BlinkMacSystemFont,\"Segoe UI\",sans-serif; background:#0f1722; color:#fff; }\n main { padding:20px; display:grid; gap:14px; }\n .card { border:1px solid #263445; background:#17212b; border-radius:16px; padding:16px; }\n button { border:0; border-radius:999px; padding:10px 14px; background:#3390ec; color:white; font-weight:800; }\n pre { white-space:pre-wrap; color:#9ec7ff; }\n </style>\n</head>\n<body>\n <main>\n <section class=\"card\">\n <h1>${safeTitle}</h1>\n <p>这是机器人之父根据你的描述生成的个人沙箱小程序。</p>\n <pre>${safePrompt}</pre>\n </section>\n <section class=\"card\">\n <button id=\"context\">读取宿主上下文</button>\n <button id=\"spec\">读取宿主能力</button>\n <button id=\"chat\">让机器人解释这个小程序</button>\n <pre id=\"output\">等待操作...</pre>\n </section>\n </main>\n <script>\n const out = document.getElementById('output');\n async function invoke(method, params) {\n if (!window.FabushiMiniApp) throw new Error('FabushiMiniApp host SDK not ready');\n const result = await window.FabushiMiniApp.invoke(method, params || {});\n out.textContent = JSON.stringify(result, null, 2);\n }\n document.getElementById('context').onclick = () => invoke('app.getContext');\n document.getElementById('spec').onclick = () => invoke('app.getHostApiSpec');\n const prompt = ${promptJson};\n document.getElementById('chat').onclick = () => invoke('bot.sendMessage', {\n message: '请简要说明这个小程序能做什么:' + prompt\n });\n const seenCommands = new Set();\n function deliverMiniAppCommand(detail) {\n if (!detail || typeof detail !== 'object') return;\n const commandKey = detail.commandId || detail.id || [detail.createdAt, detail.command, detail.rawText || detail.text].filter(Boolean).join(':');\n if (commandKey) {\n if (seenCommands.has(commandKey)) return;\n seenCommands.add(commandKey);\n }\n handleMiniAppCommand(detail);\n }\n async function handleMiniAppCommand(detail) {\n out.textContent = '收到后台命令:' + JSON.stringify(detail, null, 2);\n try {\n await invoke('bot.postMessage', {\n commandId: detail.commandId || detail.id,\n message: '「${safeTitle}」已收到后台命令:' + (detail.args || detail.text || '')\n });\n await invoke('bot.reportCommandResult', {\n commandId: detail.commandId || detail.id,\n status: 'completed',\n message: '「${safeTitle}」后台命令已处理。'\n });\n } catch (error) {\n out.textContent = String(error && error.message || error);\n }\n }\n window.addEventListener('fabushi-miniapp-command', (event) => {\n deliverMiniAppCommand(event.detail || {});\n });\n if (window.__fabushiLastMiniAppCommand) {\n (window.queueMicrotask || ((fn) => setTimeout(fn, 0)))(() => {\n deliverMiniAppCommand(window.__fabushiLastMiniAppCommand);\n });\n }\n </script>\n</body>\n</html>`;\n}\n\nfunction extractMiniAppHtml(value) {\n const text = readText(value);\n if (!text) return '';\n const fenced = text.match(/```(?:html)?\\s*([\\s\\S]*?)```/i);\n const candidate = (fenced ? fenced[1] : text).trim();\n if (!/<html[\\s>]/i.test(candidate) || !/<script[\\s>]/i.test(candidate)) return '';\n return candidate.startsWith('<!doctype') || candidate.startsWith('<html')\n ? candidate\n : `<!doctype html>\\n${candidate}`;\n}\n\nasync function generateBotFatherMiniAppHtml({ title, prompt, user }) {\n const fallback = generatedMiniAppHtml({ title, prompt });\n try {\n const result = await runAgentModel({\n user,\n mode: 'miniapp_generation',\n maxCompletionTokens: deepseekMaxCompletionTokens,\n messages: [\n {\n role: 'system',\n content: [\n '你是 Fabushi App 的小程序生成器。',\n '只输出一个完整静态 HTML 文件,不要 Markdown,不要解释。',\n '小程序只能通过 window.FabushiMiniApp.invoke(method, params) 调用宿主能力。',\n '先调用 app.getHostApiSpec 或 app.getCapabilities 判断宿主版本和已授权权限,再显示对应 UI。',\n '聊天后台调用协议:优先使用 window.FabushiMiniApp.bot.onCommand(\"/start\", handler) 或 bot.onAnyCommand(handler);如果手写监听 window 的 fabushi-miniapp-command 事件,注册后也要读取 window.__fabushiLastMiniAppCommand 并按 commandId 去重补处理。detail.command 默认为 /start,detail.args 是用户文本;处理完成后调用 bot.postMessage 或 bot.reportCommandResult 把进度/结果写回聊天框。',\n '标准能力:auth.getSession/auth.requireLogin 复用宿主登录;payments.requestPayment 使用福德金支付并由宿主登记权益;payments.checkEntitlement 查询商品是否已解锁;payments.alipay.pay/createOrder 仅作为宿主官方现金支付兜底;network.http.fetch/network.udp/network.interfaces 提供 HTTP、UDP 和网卡原语;system.keepAwake/hotspot.openSettings 提供系统级辅助;flashcards.createDeck 接入背诵闪卡;platformPublish.createDraft 接入发布草稿;fs.writeFile/fs.readFile、runtime.process.execute、browser.open 用于本地文件、进程和浏览器。',\n '高危权限包括 auth.token、payments.alipay、payments.fudeGold、network.http、network.udp、network.interfaces、system.keepAwake、hotspot.settings、local.loopback、fs.readWrite、runtime.process、shell.execute、browser.external、desktop.control;只有用户需求明确需要时才使用。',\n '禁止外链脚本、eval、Authorization token、桌面桥 token、OpenClaw token。',\n '不要调用 auth.getAccessToken,除非用户明确要求对接需要 token 的受信服务。',\n '优先实现一个可点击、可立即使用的小面板,包含 app.getContext 或 bot.sendMessage 示例。',\n ].join('\\n'),\n },\n {\n role: 'user',\n content: [\n `小程序标题:${title}`,\n `用户需求:${prompt}`,\n '请生成 HTML/CSS/JS 单文件。',\n ].join('\\n'),\n },\n ],\n });\n const sourceHtml = extractMiniAppHtml(result.message);\n if (!sourceHtml) {\n return {\n sourceHtml: fallback,\n generation: { provider: 'template', fallbackReason: 'ai_output_not_html' },\n };\n }\n return {\n sourceHtml,\n generation: {\n provider: result.provider || 'ai',\n model: result.model || '',\n },\n };\n } catch (error) {\n return {\n sourceHtml: fallback,\n generation: {\n provider: 'template',\n fallbackReason: readText(error?.message || error),\n },\n };\n }\n}\n\nfunction clientContextMessage(client) {\n if (!client || typeof client !== 'object') return '';\n const selectedModel = readText(client.selectedModel || client.model);\n const project = client.project && typeof client.project === 'object' ? client.project : {};\n const projectName = readText(project.name);\n const projectPath = readText(project.path);\n const lines = ['桌面端上下文:'];\n if (selectedModel) lines.push(`用户选择的 DeepSeek 模型:${normalizeDeepSeekModelName(selectedModel)}`);\n if (projectPath) {\n lines.push(`当前项目名称:${projectName || projectPath}`);\n lines.push(`当前项目路径:${projectPath}`);\n lines.push('如果本轮任务涉及读取、修改、运行、分析或整理项目,请优先围绕当前项目路径工作;不要擅自操作其他目录。');\n }\n return lines.length > 1 ? lines.join('\\n') : '';\n}\nfunction unwrapPayload(payload) {\n if (!payload || typeof payload !== 'object') return {};\n return payload.data && typeof payload.data === 'object' ? payload.data : payload;\n}\n\nfunction readFirstText(values) {\n for (const value of values) {\n const text = readText(value);\n if (text) return text;\n }\n return '';\n}\n\nfunction activeFlag(value) {\n if (value === true) return true;\n if (value === 1) return true;\n if (typeof value !== 'string') return false;\n return ['true', '1', 'yes', 'active'].includes(value.trim().toLowerCase());\n}\n\nfunction futureDate(value) {\n const text = readText(value);\n if (!text) return false;\n const timestamp = Date.parse(text);\n return Number.isFinite(timestamp) && timestamp > Date.now();\n}\n\nfunction normalizeMembershipPayload(payload) {\n if (!payload || typeof payload !== 'object') return false;\n const data = unwrapPayload(payload);\n const membership = data.membership && typeof data.membership === 'object' ? data.membership : data;\n const type = readText(membership.type || membership.membershipType || data.membershipType).toLowerCase();\n const status = readText(membership.status || data.status).toLowerCase();\n const expiry = membership.expiresAt ||\n membership.expires_at ||\n membership.membershipExpiry ||\n membership.membership_expires_at ||\n data.membershipExpiry ||\n data.membership_expires_at;\n const hasExplicitActiveFlag =\n membership.isActive !== undefined ||\n membership.active !== undefined ||\n membership.hasPremiumAccess !== undefined ||\n membership.hasPremiumMembership !== undefined ||\n membership.isPremium !== undefined;\n\n if (hasExplicitActiveFlag) {\n return Boolean(\n activeFlag(membership.isActive) ||\n activeFlag(membership.active) ||\n activeFlag(membership.hasPremiumAccess) ||\n activeFlag(membership.hasPremiumMembership) ||\n activeFlag(membership.isPremium),\n );\n }\n\n return Boolean(\n status === 'active' ||\n type === 'paid' ||\n type === 'premium' ||\n type === 'trial' ||\n futureDate(expiry),\n );\n}\n\nfunction extractRemoteAccount(payload) {\n const data = unwrapPayload(payload);\n const nestedUser = data.user && typeof data.user === 'object' ? data.user : {};\n const id = readFirstText([\n data.userId,\n data.user_id,\n data.userNo,\n data.user_no,\n data.id,\n nestedUser.userId,\n nestedUser.user_id,\n nestedUser.userNo,\n nestedUser.user_no,\n nestedUser.id,\n ]);\n const username = readFirstText([data.username, data.name, nestedUser.username, nestedUser.name]);\n const email = readFirstText([data.email, nestedUser.email]);\n const membership = data.membership && typeof data.membership === 'object' ? data.membership : null;\n return {\n authenticated: Boolean(id || username || email || membership || data.success === true),\n id,\n username,\n email,\n isMember: normalizeMembershipPayload(payload),\n membership,\n };\n}\n\nasync function resolveRemoteAccount(token) {\n const fallback = {\n authenticated: false,\n id: '',\n username: '',\n email: '',\n isMember: false,\n membership: null,\n };\n if (!token || !fabushiApiBaseUrl) return fallback;\n\n const endpoints = [\n '/api/stripe/membership-status',\n '/api/alipay/check-membership',\n '/api/auth/user-info',\n ];\n\n for (const endpoint of endpoints) {\n try {\n const response = await fetch(`${fabushiApiBaseUrl}${endpoint}`, {\n headers: {\n Accept: 'application/json',\n Authorization: `Bearer ${token}`,\n },\n signal: AbortSignal.timeout(8_000),\n });\n if (response.status === 401 || response.status === 403) return fallback;\n if (!response.ok) continue;\n const payload = await response.json();\n const account = extractRemoteAccount(payload);\n if (account.authenticated || account.isMember) {\n return {\n ...fallback,\n ...account,\n authenticated: true,\n };\n }\n } catch (error) {\n logger.debug({ endpoint, error: String(error) }, 'membership lookup skipped');\n }\n }\n\n return fallback;\n}\n\nasync function resolveUser(req, body = {}) {\n const token = bearerToken(req);\n const usernameHint = safeUserText(body.username || req.query.username);\n\n const testToken = process.env.TEST_ACCOUNT_TOKEN || 'dacheng-test-token-bypass-limits';\n if (token && token === testToken) {\n return {\n userId: 'user:test_account',\n username: 'TestAccount',\n tokenHash: sha256(token).slice(0, 24),\n isAuthenticated: true,\n isMember: true,\n isTestAccount: true,\n membership: { type: 'lifetime', active: true },\n };\n }\n\n const tokenHash = token ? sha256(token).slice(0, 24) : '';\n const anonymousHash = sha256(`${req.ip}|${req.get('User-Agent') || ''}`).slice(0, 24);\n const remoteAccount = await resolveRemoteAccount(token);\n const remoteIdentity = remoteAccount.id ||\n remoteAccount.username ||\n remoteAccount.email ||\n (remoteAccount.authenticated ? tokenHash : '');\n const userId = remoteAccount.authenticated\n ? `user:${remoteIdentity}`\n : tokenHash\n ? `token:${tokenHash}`\n : `anon:${anonymousHash}`;\n return {\n userId,\n username: remoteAccount.username || usernameHint,\n tokenHash,\n isAuthenticated: remoteAccount.authenticated,\n isMember: remoteAccount.isMember,\n membership: remoteAccount.membership,\n };\n}\n\nfunction base64UrlJson(value) {\n return Buffer.from(JSON.stringify(value), 'utf8').toString('base64url');\n}\n\nfunction timingSafeEqualText(left, right) {\n const leftBuffer = Buffer.from(String(left || ''));\n const rightBuffer = Buffer.from(String(right || ''));\n if (leftBuffer.length !== rightBuffer.length) return false;\n return crypto.timingSafeEqual(leftBuffer, rightBuffer);\n}\n\nfunction signCodexAdapterPayload(encodedPayload) {\n return crypto\n .createHmac('sha256', codexAdapterSecret)\n .update(encodedPayload)\n .digest('base64url');\n}\n\nfunction createCodexAdapterToken(user, options = {}) {\n const payload = {\n sub: user.userId,\n username: user.username || '',\n tokenHash: user.tokenHash || '',\n isAuthenticated: Boolean(user.isAuthenticated),\n isMember: Boolean(user.isMember),\n isTestAccount: Boolean(user.isTestAccount),\n membership: user.membership || {},\n recordUsage: options.recordUsage !== false,\n exp: Date.now() + codexAdapterTokenTtlMs,\n };\n const encodedPayload = base64UrlJson(payload);\n return `${codexAdapterTokenPrefix}${encodedPayload}.${signCodexAdapterPayload(encodedPayload)}`;\n}\n\nfunction resolveCodexAdapterToken(token) {\n const raw = readText(token);\n if (!raw.startsWith(codexAdapterTokenPrefix)) return null;\n\n const signed = raw.slice(codexAdapterTokenPrefix.length);\n const separator = signed.lastIndexOf('.');\n if (separator < 1) return null;\n\n const encodedPayload = signed.slice(0, separator);\n const signature = signed.slice(separator + 1);\n const expectedSignature = signCodexAdapterPayload(encodedPayload);\n if (!timingSafeEqualText(signature, expectedSignature)) return null;\n\n let payload;\n try {\n payload = JSON.parse(Buffer.from(encodedPayload, 'base64url').toString('utf8'));\n } catch {\n return null;\n }\n\n if (!payload || typeof payload !== 'object') return null;\n if (Number(payload.exp || 0) < Date.now()) return null;\n\n const userId = safeUserText(payload.sub);\n if (!userId) return null;\n\n return {\n user: {\n userId,\n username: safeUserText(payload.username),\n tokenHash: safeUserText(payload.tokenHash),\n isAuthenticated: Boolean(payload.isAuthenticated),\n isMember: Boolean(payload.isMember),\n isTestAccount: Boolean(payload.isTestAccount),\n membership:\n payload.membership && typeof payload.membership === 'object'\n ? payload.membership\n : {},\n },\n recordUsage: payload.recordUsage !== false,\n };\n}\n\nasync function resolveCodexResponsesBilling(req) {\n const token = bearerToken(req);\n const adapter = resolveCodexAdapterToken(token);\n if (adapter) return adapter;\n\n if (deepseekApiKey && token === deepseekApiKey) {\n const error = new Error('Codex DeepSeek adapter now requires first-party user billing');\n error.statusCode = 401;\n throw error;\n }\n\n return {\n user: await resolveUser(req, req.body || {}),\n recordUsage: true,\n };\n}\n\nfunction usageFor(userId) {\n const month = monthKey();\n const row = statements.getUsage.get(userId, month);\n return {\n month,\n used: Number(row?.totalTokens || 0),\n };\n}\n\nfunction enforceTokenBudget(user, estimatedTokensForRequest) {\n if (user && user.isTestAccount) {\n return { limit: 999999999, usage: { month: monthKey(), used: 0 } };\n }\n if (requireMemberForAi && !user.isMember) {\n const error = new Error('大乘 AI 需要开通会员后使用');\n error.statusCode = 403;\n error.code = 'AI_MEMBERSHIP_REQUIRED';\n error.details = {\n requireMemberForAi,\n monthlyLimit: memberMonthlyLimit,\n remainingTokens: 0,\n };\n throw error;\n }\n\n const limit = user.isMember ? memberMonthlyLimit : freeMonthlyLimit;\n const usage = usageFor(user.userId);\n if (usage.used + estimatedTokensForRequest > limit) {\n const error = new Error('本月 AI token 额度已不足');\n error.statusCode = 429;\n error.code = 'USER_TOKEN_QUOTA_EXCEEDED';\n error.details = {\n monthlyLimit: limit,\n usedTokens: usage.used,\n remainingTokens: Math.max(0, limit - usage.used),\n };\n throw error;\n }\n return { limit, usage };\n}\n\nfunction completionBudgetFor(budget, estimatedPromptTokens) {\n const remainingAfterPrompt = budget.limit - budget.usage.used - estimatedPromptTokens;\n return Math.max(1, Math.min(deepseekMaxCompletionTokens, Math.floor(remainingAfterPrompt)));\n}\n\nfunction recordUsage(userId, totalTokens) {\n if (!totalTokens || totalTokens < 1) return;\n statements.upsertUsage.run({\n userId,\n month: monthKey(),\n totalTokens,\n updatedAt: nowIso(),\n });\n}\n\nfunction titleFromMessage(message) {\n const compact = message.replace(/\\s+/g, ' ').trim();\n if (!compact) return '新对话';\n return compact.length > 22 ? `${compact.slice(0, 22)}...` : compact;\n}\n\nfunction normalizeMessages(rows) {\n return rows.map((row) => ({\n role: row.role,\n content: row.content,\n }));\n}\n\nfunction normalizeMaxCompletionTokens(value) {\n const numeric = Number(value || deepseekMaxCompletionTokens);\n if (!Number.isFinite(numeric) || numeric < 1) return deepseekMaxCompletionTokens;\n return Math.max(1, Math.min(deepseekMaxCompletionTokens, Math.floor(numeric)));\n}\n\nfunction normalizeDeepSeekModelName(value) {\n const raw = readText(value || deepseekModel);\n if (!raw) return deepseekModel;\n const modelId = raw.includes('/') ? raw.split('/').pop() : raw;\n return modelId || deepseekModel;\n}\n\nasync function callDeepSeek(messages, options = {}) {\n if (!deepseekApiKey) {\n const error = new Error('DeepSeek API key is not configured');\n error.statusCode = 503;\n throw error;\n }\n\n const model = normalizeDeepSeekModelName(options.model);\n const response = await fetch(`${deepseekBaseUrl}/chat/completions`, {\n method: 'POST',\n headers: {\n Accept: 'application/json',\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${deepseekApiKey}`,\n },\n body: JSON.stringify({\n model,\n messages,\n temperature: 0.4,\n max_tokens: normalizeMaxCompletionTokens(options.maxCompletionTokens),\n stream: false,\n }),\n signal: AbortSignal.timeout(60_000),\n });\n\n const bodyText = await response.text();\n let payload;\n try {\n payload = bodyText ? JSON.parse(bodyText) : {};\n } catch {\n payload = { error: bodyText };\n }\n\n if (!response.ok) {\n const upstreamMessage = payload?.error?.message || payload?.message || '';\n const friendlyMessage =\n response.status === 402 || /insufficient\\s+balance/i.test(upstreamMessage)\n ? '大乘 AI 服务额度暂不可用,请联系管理员充值 DeepSeek 账户后重试。'\n : upstreamMessage || `DeepSeek request failed: ${response.status}`;\n const error = new Error(friendlyMessage);\n error.statusCode = response.status;\n if (response.status === 402 || /insufficient\\s+balance/i.test(upstreamMessage)) {\n error.code = 'DEEPSEEK_PROVIDER_QUOTA';\n }\n throw error;\n }\n\n const message = payload?.choices?.[0]?.message?.content?.trim();\n if (!message) {\n const error = new Error('DeepSeek returned an empty response');\n error.statusCode = 502;\n throw error;\n }\n\n return {\n message,\n model,\n usage: {\n promptTokens: Number(payload?.usage?.prompt_tokens || 0),\n completionTokens: Number(payload?.usage?.completion_tokens || 0),\n totalTokens: Number(payload?.usage?.total_tokens || 0),\n },\n };\n}\n\nasync function callDeepSeekStream(messages, callbacks = {}) {\n if (!deepseekApiKey) {\n const error = new Error('DeepSeek API key is not configured');\n error.statusCode = 503;\n throw error;\n }\n\n const model = normalizeDeepSeekModelName(callbacks.model);\n const response = await fetch(`${deepseekBaseUrl}/chat/completions`, {\n method: 'POST',\n headers: {\n Accept: 'text/event-stream',\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${deepseekApiKey}`,\n },\n body: JSON.stringify({\n model,\n messages,\n temperature: 0.4,\n max_tokens: normalizeMaxCompletionTokens(callbacks.maxCompletionTokens),\n stream: true,\n stream_options: { include_usage: true },\n }),\n signal: callbacks.signal || AbortSignal.timeout(90_000),\n });\n\n if (!response.ok) {\n const bodyText = await response.text();\n let payload;\n try {\n payload = bodyText ? JSON.parse(bodyText) : {};\n } catch {\n payload = { error: bodyText };\n }\n const upstreamMessage = payload?.error?.message || payload?.message || '';\n const friendlyMessage =\n response.status === 402 || /insufficient\\s+balance/i.test(upstreamMessage)\n ? '大乘 AI 服务额度暂不可用,请联系管理员充值 DeepSeek 账户后重试。'\n : upstreamMessage || `DeepSeek request failed: ${response.status}`;\n const error = new Error(friendlyMessage);\n error.statusCode = response.status;\n if (response.status === 402 || /insufficient\\s+balance/i.test(upstreamMessage)) {\n error.code = 'DEEPSEEK_PROVIDER_QUOTA';\n }\n throw error;\n }\n\n let buffer = '';\n let message = '';\n let usage = {\n promptTokens: 0,\n completionTokens: 0,\n totalTokens: 0,\n };\n const decoder = new TextDecoder();\n\n for await (const chunk of response.body) {\n buffer += decoder.decode(chunk, { stream: true });\n const lines = buffer.split(/\\r?\\n/);\n buffer = lines.pop() || '';\n\n for (const rawLine of lines) {\n const line = rawLine.trim();\n if (!line.startsWith('data:')) continue;\n const data = line.slice('data:'.length).trim();\n if (!data || data === '[DONE]') continue;\n\n let payload;\n try {\n payload = JSON.parse(data);\n } catch {\n continue;\n }\n\n const delta = payload?.choices?.[0]?.delta?.content || '';\n if (delta) {\n message += delta;\n callbacks.onToken?.(delta);\n }\n\n if (payload?.usage) {\n usage = {\n promptTokens: Number(payload.usage.prompt_tokens || usage.promptTokens || 0),\n completionTokens: Number(payload.usage.completion_tokens || usage.completionTokens || 0),\n totalTokens: Number(payload.usage.total_tokens || usage.totalTokens || 0),\n };\n }\n }\n }\n\n message = message.trim();\n if (!message) {\n const error = new Error('DeepSeek returned an empty response');\n error.statusCode = 502;\n throw error;\n }\n\n return { message, model, usage };\n}\n\nfunction createCodexDeepSeekRuntime(user = null) {\n if (!enableCodexSdkChat || !deepseekApiKey) {\n return {\n enabled: false,\n provider: codexDeepSeekProviderId,\n reason: enableCodexSdkChat ? 'DeepSeek API key is not configured' : 'ENABLE_CODEX_SDK_CHAT is not true',\n };\n }\n\n const adapterToken = user ? createCodexAdapterToken(user) : '';\n return {\n enabled: true,\n provider: codexDeepSeekProviderId,\n options: {\n apiKey: adapterToken,\n baseUrl: codexResponsesBaseUrl,\n env: {\n ...process.env,\n DEEPSEEK_API_KEY: adapterToken,\n HOME: codexHomeDir,\n CODEX_HOME: codexHomeDir,\n TMPDIR: codexTempDir,\n XDG_RUNTIME_DIR: codexRuntimeDir,\n XDG_CACHE_HOME: path.join(codexHomeDir, '.cache'),\n XDG_CONFIG_HOME: path.join(codexHomeDir, '.config'),\n XDG_DATA_HOME: path.join(codexHomeDir, '.local', 'share'),\n PATH: process.env.PATH || '/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin',\n },\n config: {\n model_provider: codexDeepSeekProviderId,\n model_providers: {\n [codexDeepSeekProviderId]: {\n name: 'Dacheng DeepSeek Proxy',\n base_url: codexResponsesBaseUrl,\n env_key: 'DEEPSEEK_API_KEY',\n wire_api: 'responses',\n query_params: {},\n },\n },\n },\n },\n threadOptions: {\n model: deepseekModel,\n approvalPolicy: 'never',\n sandboxMode: 'read-only',\n skipGitRepoCheck: true,\n networkAccessEnabled: true,\n },\n };\n}\n\nfunction codexPromptFromMessages(messages) {\n return messages\n .map((message) => {\n const role = String(message.role || 'user').toUpperCase();\n return `${role}:\\n${message.content}`;\n })\n .join('\\n\\n');\n}\n\nfunction responseContentText(content) {\n if (typeof content === 'string') return content;\n if (!Array.isArray(content)) return '';\n return content\n .map((part) => {\n if (typeof part === 'string') return part;\n if (!part || typeof part !== 'object') return '';\n return String(part.text || part.input_text || part.output_text || '');\n })\n .filter(Boolean)\n .join('\\n');\n}\n\nfunction codexResponsesPrompt(body) {\n const input = Array.isArray(body?.input) ? body.input : [];\n const userItems = input.filter((item) => item?.role === 'user');\n const source = userItems[userItems.length - 1] || input[input.length - 1];\n const prompt = responseContentText(source?.content).trim();\n if (prompt) return prompt;\n if (typeof body?.input === 'string') return body.input.trim();\n return '';\n}\n\nfunction writeResponsesEvent(res, event, data) {\n res.write(`event: ${event}\\n`);\n res.write(`data: ${JSON.stringify(data)}\\n\\n`);\n}\n\nfunction responseUsage(usage = {}) {\n return {\n input_tokens: Number(usage.promptTokens || usage.input_tokens || 0),\n cached_input_tokens: 0,\n output_tokens: Number(usage.completionTokens || usage.output_tokens || 0),\n reasoning_output_tokens: 0,\n total_tokens: Number(usage.totalTokens || usage.total_tokens || 0),\n };\n}\n\nfunction createLibreChatAgentRuntime() {\n if (!enableLibreChatAgentChat || !libreChatAgentApiKey || !libreChatAgentId) {\n return {\n enabled: false,\n provider: 'librechat-agent',\n reason: !enableLibreChatAgentChat\n ? 'ENABLE_LIBRECHAT_AGENT_CHAT is not true'\n : !libreChatAgentApiKey\n ? 'LIBRECHAT_AGENT_API_KEY is not configured'\n : 'LIBRECHAT_AGENT_ID is not configured',\n };\n }\n\n return {\n enabled: true,\n provider: 'librechat-agent',\n model: libreChatAgentId,\n chatCompletionsUrl: `${libreChatAgentsBaseUrl}/chat/completions`,\n };\n}\n\nfunction libreChatUsage(usage = {}) {\n return {\n promptTokens: Number(usage.prompt_tokens || usage.input_tokens || usage.promptTokens || 0),\n completionTokens: Number(usage.completion_tokens || usage.output_tokens || usage.completionTokens || 0),\n totalTokens: Number(usage.total_tokens || usage.totalTokens || 0),\n };\n}\n\nasync function callLibreChatAgent(messages) {\n const runtime = createLibreChatAgentRuntime();\n if (!runtime.enabled) {\n const error = new Error(runtime.reason || 'LibreChat Agent chat is not enabled');\n error.statusCode = 503;\n throw error;\n }\n\n const response = await fetch(runtime.chatCompletionsUrl, {\n method: 'POST',\n headers: {\n Accept: 'application/json',\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${libreChatAgentApiKey}`,\n },\n body: JSON.stringify({\n model: runtime.model,\n messages,\n temperature: 0.4,\n stream: false,\n }),\n signal: AbortSignal.timeout(120_000),\n });\n\n const bodyText = await response.text();\n let payload;\n try {\n payload = bodyText ? JSON.parse(bodyText) : {};\n } catch {\n payload = { error: bodyText };\n }\n\n if (!response.ok) {\n const upstreamMessage = payload?.error?.message || payload?.message || bodyText || '';\n const error = new Error(upstreamMessage || `LibreChat Agent request failed: ${response.status}`);\n error.statusCode = response.status;\n throw error;\n }\n\n const message = payload?.choices?.[0]?.message?.content?.trim();\n if (!message) {\n const error = new Error('LibreChat Agent returned an empty response');\n error.statusCode = 502;\n throw error;\n }\n\n return {\n message,\n usage: libreChatUsage(payload?.usage),\n };\n}\n\nasync function callLibreChatAgentStream(messages, callbacks = {}) {\n const runtime = createLibreChatAgentRuntime();\n if (!runtime.enabled) {\n const error = new Error(runtime.reason || 'LibreChat Agent chat is not enabled');\n error.statusCode = 503;\n throw error;\n }\n\n const response = await fetch(runtime.chatCompletionsUrl, {\n method: 'POST',\n headers: {\n Accept: 'text/event-stream',\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${libreChatAgentApiKey}`,\n },\n body: JSON.stringify({\n model: runtime.model,\n messages,\n temperature: 0.4,\n stream: true,\n }),\n signal: callbacks.signal || AbortSignal.timeout(180_000),\n });\n\n if (!response.ok) {\n const bodyText = await response.text();\n let payload;\n try {\n payload = bodyText ? JSON.parse(bodyText) : {};\n } catch {\n payload = { error: bodyText };\n }\n const upstreamMessage = payload?.error?.message || payload?.message || bodyText || '';\n const error = new Error(upstreamMessage || `LibreChat Agent request failed: ${response.status}`);\n error.statusCode = response.status;\n throw error;\n }\n\n let buffer = '';\n let message = '';\n let usage = { promptTokens: 0, completionTokens: 0, totalTokens: 0 };\n const decoder = new TextDecoder();\n\n for await (const chunk of response.body) {\n buffer += decoder.decode(chunk, { stream: true });\n const lines = buffer.split(/\\r?\\n/);\n buffer = lines.pop() || '';\n\n for (const rawLine of lines) {\n const line = rawLine.trim();\n if (!line.startsWith('data:')) continue;\n const data = line.slice('data:'.length).trim();\n if (!data || data === '[DONE]') continue;\n\n let payload;\n try {\n payload = JSON.parse(data);\n } catch {\n continue;\n }\n\n const delta = payload?.choices?.[0]?.delta?.content || '';\n if (delta) {\n message += delta;\n callbacks.onToken?.(delta);\n }\n\n if (payload?.usage) {\n usage = libreChatUsage(payload.usage);\n }\n }\n }\n\n message = message.trim();\n if (!message) {\n const error = new Error('LibreChat Agent returned an empty response');\n error.statusCode = 502;\n throw error;\n }\n\n return { message, usage };\n}\n\nfunction isDeepSeekQuotaError(error) {\n if (error?.code === 'DEEPSEEK_PROVIDER_QUOTA') return true;\n if (error?.code === 'USER_TOKEN_QUOTA_EXCEEDED' || error?.code === 'AI_MEMBERSHIP_REQUIRED') return false;\n const message = String(error?.message || error || '');\n return /insufficient\\s+balance|balance/i.test(message);\n}\n\nfunction publicAiErrorMessage(error) {\n if (isDeepSeekQuotaError(error)) {\n return '大乘 AI 服务额度暂不可用,请联系管理员充值 DeepSeek 账户后重试。';\n }\n return error?.message || 'Internal Server Error';\n}\n\nfunction responsesPayload({\n responseId,\n itemId,\n status,\n model,\n text = '',\n usage = null,\n error = null,\n}) {\n return {\n id: responseId,\n object: 'response',\n status,\n model,\n output:\n status === 'completed'\n ? [\n {\n id: itemId,\n type: 'message',\n role: 'assistant',\n status: 'completed',\n content: [{ type: 'output_text', text }],\n },\n ]\n : [],\n ...(usage ? { usage: responseUsage(usage) } : {}),\n ...(error\n ? {\n error: {\n message: error.message || String(error),\n type: 'server_error',\n },\n }\n : {}),\n };\n}\n\nasync function callCodexSdkDeepSeek(messages, callbacks = {}) {\n const codexRuntime = createCodexDeepSeekRuntime(callbacks.user);\n if (!codexRuntime.enabled) {\n const error = new Error(codexRuntime.reason || 'Codex SDK chat is not enabled');\n error.statusCode = 503;\n throw error;\n }\n if (!callbacks.user) {\n const error = new Error('Codex SDK DeepSeek proxy requires a resolved user for billing');\n error.statusCode = 401;\n throw error;\n }\n\n const { Codex } = await import('@openai/codex-sdk');\n const codex = new Codex(codexRuntime.options);\n const thread = codex.startThread(codexRuntime.threadOptions);\n const prompt = codexPromptFromMessages(messages);\n\n if (callbacks.onToken || callbacks.onStep) {\n const { events } = await thread.runStreamed(prompt, { signal: callbacks.signal });\n let finalResponse = '';\n let usage = null;\n\n for await (const event of events) {\n if (event.type === 'item.completed') {\n const item = event.item;\n if (item.type === 'agent_message') {\n finalResponse = item.text || finalResponse;\n callbacks.onToken?.(item.text || '');\n } else if (item.type === 'todo_list') {\n callbacks.onStep?.({\n title: 'Codex SDK 执行计划',\n message: item.items.map((todo) => `${todo.completed ? '✓' : '•'} ${todo.text}`).join('\\n'),\n });\n } else if (item.type === 'command_execution') {\n callbacks.onStep?.({\n title: 'Codex SDK 执行命令',\n message: item.command,\n });\n } else if (item.type === 'mcp_tool_call') {\n callbacks.onStep?.({\n title: 'Codex SDK 调用工具',\n message: `${item.server}.${item.tool}`,\n });\n }\n } else if (event.type === 'turn.completed') {\n usage = event.usage;\n } else if (event.type === 'turn.failed') {\n throw new Error(event.error?.message || 'Codex SDK turn failed');\n } else if (event.type === 'error') {\n throw new Error(event.message || 'Codex SDK stream failed');\n }\n }\n\n if (!finalResponse.trim()) {\n const error = new Error('Codex SDK returned an empty response');\n error.statusCode = 502;\n throw error;\n }\n\n return {\n message: finalResponse.trim(),\n usage: {\n promptTokens: Number(usage?.input_tokens || 0),\n completionTokens: Number(usage?.output_tokens || 0),\n totalTokens: Number((usage?.input_tokens || 0) + (usage?.output_tokens || 0)),\n },\n usageAlreadyRecorded: true,\n };\n }\n\n const turn = await thread.run(prompt);\n if (!turn.finalResponse?.trim()) {\n const error = new Error('Codex SDK returned an empty response');\n error.statusCode = 502;\n throw error;\n }\n\n return {\n message: turn.finalResponse.trim(),\n usage: {\n promptTokens: Number(turn.usage?.input_tokens || 0),\n completionTokens: Number(turn.usage?.output_tokens || 0),\n totalTokens: Number((turn.usage?.input_tokens || 0) + (turn.usage?.output_tokens || 0)),\n },\n usageAlreadyRecorded: true,\n };\n}\n\nconst activeAgentRuns = new Map();\n\nfunction normalizeAgentMode(value) {\n const mode = safeUserText(value || 'dharma_guide');\n return /^[a-z][a-z0-9_:-]{0,48}$/i.test(mode) ? mode : 'dharma_guide';\n}\n\nfunction openClawSessionKey(user, conversationId) {\n const env = process.env.NODE_ENV === 'production' ? 'prod' : 'dev';\n return `fabushi:env:${env}:user:${user.userId}:conversation:${conversationId}`;\n}\n\nfunction createOpenClawRuntime() {\n if (!enableOpenClawAgentChat || !openClawGatewayToken || !openClawGatewayUrl) {\n return {\n enabled: false,\n provider: 'openclaw-gateway',\n reason: !enableOpenClawAgentChat\n ? 'ENABLE_OPENCLAW_AGENT_CHAT is not true'\n : !openClawGatewayToken\n ? 'OPENCLAW_GATEWAY_TOKEN is not configured'\n : 'OPENCLAW_GATEWAY_URL is not configured',\n };\n }\n\n return {\n enabled: true,\n provider: 'openclaw-gateway',\n model: openClawAgentId,\n runsUrl: openClawRunsEndpoint || `${openClawGatewayUrl}/v1/agents/${encodeURIComponent(openClawAgentId)}/runs`,\n };\n}\n\nfunction extractOpenClawText(payload) {\n if (!payload || typeof payload !== 'object') return '';\n const candidates = [\n payload.message,\n payload.content,\n payload.output_text,\n payload.finalResponse,\n payload.final_response,\n payload.result?.message,\n payload.result?.content,\n payload.output?.message,\n payload.output?.content,\n payload.choices?.[0]?.message?.content,\n ];\n for (const candidate of candidates) {\n if (typeof candidate === 'string' && candidate.trim()) return candidate.trim();\n }\n return '';\n}\n\nfunction extractOpenClawUsage(payload) {\n const usage = payload?.usage || payload?.result?.usage || {};\n return {\n promptTokens: Number(usage.input_tokens || usage.prompt_tokens || usage.promptTokens || 0),\n completionTokens: Number(usage.output_tokens || usage.completion_tokens || usage.completionTokens || 0),\n totalTokens: Number(usage.total_tokens || usage.totalTokens || 0),\n };\n}\n\nasync function callOpenClawAgent({ messages, user, conversationId, mode, signal }) {\n const runtime = createOpenClawRuntime();\n if (!runtime.enabled) {\n const error = new Error(runtime.reason || 'OpenClaw Agent chat is not enabled');\n error.statusCode = 503;\n throw error;\n }\n\n const sessionKey = openClawSessionKey(user, conversationId);\n const response = await fetch(runtime.runsUrl, {\n method: 'POST',\n headers: {\n Accept: 'application/json',\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${openClawGatewayToken}`,\n },\n body: JSON.stringify({\n agentId: openClawAgentId,\n sessionKey,\n mode,\n stream: false,\n input: { messages },\n messages,\n metadata: {\n app: 'fabushi',\n userId: user.userId,\n conversationId,\n toolPolicy: 'public-user-read-mostly',\n },\n }),\n signal: signal || AbortSignal.timeout(180_000),\n });\n\n const bodyText = await response.text();\n let payload;\n try {\n payload = bodyText ? JSON.parse(bodyText) : {};\n } catch {\n payload = { message: bodyText };\n }\n\n if (!response.ok) {\n const error = new Error(payload?.error?.message || payload?.message || `OpenClaw request failed: ${response.status}`);\n error.statusCode = response.status;\n throw error;\n }\n\n const message = extractOpenClawText(payload);\n if (!message) {\n const error = new Error('OpenClaw returned an empty response');\n error.statusCode = 502;\n throw error;\n }\n\n return {\n message,\n runId: payload.runId || payload.id || payload.result?.runId || null,\n provider: runtime.provider,\n model: runtime.model,\n usage: extractOpenClawUsage(payload),\n };\n}\n\nasync function runAgentModel({ messages, user, conversationId, mode, callbacks = {}, signal, maxCompletionTokens }) {\n const openClawRuntime = createOpenClawRuntime();\n if (openClawRuntime.enabled && !callbacks.onToken) {\n return await callOpenClawAgent({ messages, user, conversationId, mode, signal });\n }\n\n const codexRuntime = createCodexDeepSeekRuntime(user);\n const libreChatRuntime = createLibreChatAgentRuntime();\n let provider = libreChatRuntime.enabled\n ? libreChatRuntime.provider\n : codexRuntime.enabled\n ? codexRuntime.provider\n : 'deepseek';\n let model = libreChatRuntime.enabled ? libreChatRuntime.model : deepseekModel;\n\n try {\n if (libreChatRuntime.enabled) {\n const result = callbacks.onToken\n ? await callLibreChatAgentStream(messages, { ...callbacks, signal })\n : await callLibreChatAgent(messages);\n return { ...result, provider, model };\n }\n if (codexRuntime.enabled) {\n const result = await callCodexSdkDeepSeek(messages, { ...callbacks, user, signal });\n return { ...result, provider, model };\n }\n const result = callbacks.onToken\n ? await callDeepSeekStream(messages, { ...callbacks, signal, maxCompletionTokens })\n : await callDeepSeek(messages, { maxCompletionTokens });\n return { ...result, provider, model };\n } catch (error) {\n if (!codexRuntime.enabled || isDeepSeekQuotaError(error)) throw error;\n logger.warn({ error: String(error) }, 'Agent model failed, falling back to DeepSeek direct');\n const result = callbacks.onToken\n ? await callDeepSeekStream(messages, { ...callbacks, signal, maxCompletionTokens })\n : await callDeepSeek(messages, { maxCompletionTokens });\n return { ...result, provider: 'deepseek', model: deepseekModel };\n }\n}\n\nfunction updateAgentRunStatus(run, status, fields = {}) {\n statements.updateAgentRunStatus.run({\n id: run.id,\n userId: run.user_id || run.userId,\n status,\n openClawRunId: fields.openClawRunId ?? null,\n inputTokens: fields.inputTokens ?? null,\n outputTokens: fields.outputTokens ?? null,\n toolCallCount: fields.toolCallCount ?? null,\n completedAt: status === 'completed' || status === 'cancelled' ? nowIso() : null,\n failedAt: status === 'failed' ? nowIso() : null,\n errorCode: fields.errorCode ?? null,\n errorMessage: fields.errorMessage ?? null,\n });\n}\n\nfunction buildAgentMessages(conversationId) {\n const historyRows = statements.listMessages.all(conversationId).slice(-14);\n return [{ role: 'system', content: systemPrompt }, ...normalizeMessages(historyRows)];\n}\n\nfunction agentUsagePayload(usage, user, budget) {\n const promptTokens = usage.promptTokens || usage.inputTokens || 0;\n const completionTokens = usage.completionTokens || usage.outputTokens || 0;\n const totalTokens = usage.totalTokens || promptTokens + completionTokens;\n const latestUsage = usageFor(user.userId);\n return {\n inputTokens: promptTokens,\n outputTokens: completionTokens,\n totalTokens,\n monthlyLimit: budget.limit,\n remainingTokens: Math.max(0, budget.limit - latestUsage.used),\n };\n}\n\nfunction openAiContentToText(content) {\n if (typeof content === 'string') return content;\n if (Array.isArray(content)) {\n return content\n .map((part) => {\n if (typeof part === 'string') return part;\n if (!part || typeof part !== 'object') return '';\n return readText(part.text || part.content || part.value);\n })\n .filter(Boolean)\n .join('\\n');\n }\n return readText(content);\n}\n\nfunction normalizeOpenAiMessages(value) {\n if (!Array.isArray(value)) return [];\n const allowedRoles = new Set(['system', 'user', 'assistant', 'tool']);\n return value\n .map((item) => {\n if (!item || typeof item !== 'object') return null;\n const role = allowedRoles.has(item.role) ? item.role : 'user';\n const content = openAiContentToText(item.content);\n if (!content) return null;\n return { role, content };\n })\n .filter(Boolean);\n}\n\nfunction truncateForDeepSeekMessage(text, maxChars = 12_000) {\n const value = readText(text);\n if (value.length <= maxChars) return value;\n return `${value.slice(0, maxChars)}\\n\\n[内容已截断]`;\n}\n\nfunction truncateTailForDeepSeekMessage(text, maxChars = 2400) {\n const value = readText(text);\n if (value.length <= maxChars) return value;\n return `[前文已省略]\\n\\n${value.slice(-maxChars)}`;\n}\n\nfunction stripOpenClawPromptNoise(text) {\n const value = readText(text);\n if (!value) return '';\n\n const finalPromptMatch = value.match(/finalPromptText[\"'\\s::]+([\\s\\S]{1,4000})$/i);\n if (finalPromptMatch?.[1]) {\n return readText(finalPromptMatch[1].replace(/^[\"'`]+|[\"'`,}\\]]+$/g, ''));\n }\n\n const markers = [\n 'final prompt:',\n 'final user prompt:',\n 'user prompt:',\n '用户问题:',\n '用户消息:',\n '用户输入:',\n ];\n const lower = value.toLowerCase();\n for (const marker of markers) {\n const index = lower.lastIndexOf(marker.toLowerCase());\n if (index >= 0) {\n const tail = value.slice(index + marker.length).trim();\n if (tail) return tail;\n }\n }\n\n return value;\n}\n\nfunction compactOpenClawMessagesForDeepSeek(messages) {\n const conversationalMessages = messages\n .filter((message) => message.role === 'user' || message.role === 'assistant')\n .map((message) => ({\n role: message.role,\n content: truncateTailForDeepSeekMessage(stripOpenClawPromptNoise(message.content)),\n }))\n .filter((message) => message.content);\n\n const recent = conversationalMessages.slice(-6);\n while (recent.length > 0 && recent[0].role !== 'user') {\n recent.shift();\n }\n\n if (recent.length === 0) {\n const fallback = messages\n .slice()\n .reverse()\n .map((message) => truncateTailForDeepSeekMessage(stripOpenClawPromptNoise(message.content), 2400))\n .find(Boolean);\n if (!fallback) return messages;\n recent.push({ role: 'user', content: fallback });\n }\n\n return [\n {\n role: 'system',\n content: '你是大乘 App 的桌面 AI 助手。请直接回答用户问题,默认使用简洁、准确的中文。',\n },\n ...recent,\n ];\n}\n\nfunction isDeepSeekBlockedError(error) {\n return (\n Number(error?.statusCode) === 403 &&\n /blocked|被拦截|forbidden/i.test(String(error?.message || error || ''))\n );\n}\n\nfunction shouldRetryWithCompactedOpenClawMessages(error, messages, compactedMessages) {\n if (!isDeepSeekBlockedError(error)) return false;\n return JSON.stringify(messages) !== JSON.stringify(compactedMessages);\n}\n\nfunction openClawBlockedFallbackText(messages) {\n const lastUser = messages\n .slice()\n .reverse()\n .find((message) => message.role === 'user')?.content || '';\n const localActionIntent =\n /(file|folder|directory|project|workspace|path|文件|文件夹|目录|项目|工程|路径|本地)/i.test(lastUser) &&\n /(create|make|mkdir|write|edit|modify|rename|delete|move|copy|save|download|read|list|生成|创建|新建|写入|编辑|修改|重命名|删除|移动|复制|保存|下载|读取|列出)/i.test(lastUser);\n\n if (localActionIntent) {\n return [\n '本机工具规划被模型服务拦截了,所以这次没有实际读取、创建、修改或下载本机文件。',\n '如果只是新建文件夹,请先在底部选择项目后重新发送“新建文件夹”;桌面端会直接在已选项目里创建。',\n '如果是更复杂的本地文件操作,请在“连应用”里重启本机 AI 后重试,或把要操作的文件路径和目标说得更具体。',\n ].join('\\n');\n }\n\n return '模型服务拦截了这次回复,未能生成结果。请换一种更具体、更短的表述后重试。';\n}\nfunction usageWithFallback(usage, messages, text) {\n const promptTokens = Number(usage?.promptTokens || usage?.inputTokens || 0);\n const completionTokens = Number(usage?.completionTokens || usage?.outputTokens || 0);\n const totalTokens = Number(usage?.totalTokens || 0);\n if (totalTokens > 0) {\n return {\n promptTokens,\n completionTokens,\n totalTokens,\n };\n }\n const estimatedPrompt = promptTokens || estimateTokens(JSON.stringify(messages));\n const estimatedCompletion = completionTokens || estimateTokens(text);\n return {\n promptTokens: estimatedPrompt,\n completionTokens: estimatedCompletion,\n totalTokens: estimatedPrompt + estimatedCompletion,\n };\n}\n\nfunction openAiUsagePayload(usage, user, budget) {\n const promptTokens = Number(usage.promptTokens || 0);\n const completionTokens = Number(usage.completionTokens || 0);\n const totalTokens = Number(usage.totalTokens || promptTokens + completionTokens);\n const latestUsage = usageFor(user.userId);\n const remainingTokens = Math.max(0, budget.limit - latestUsage.used);\n return {\n prompt_tokens: promptTokens,\n completion_tokens: completionTokens,\n total_tokens: totalTokens,\n promptTokens,\n completionTokens,\n totalTokens,\n monthlyLimit: budget.limit,\n remainingTokens,\n };\n}\n\nfunction writeOpenAiSse(res, payload) {\n res.write(`data: ${JSON.stringify(payload)}\\n\\n`);\n}\n\napp.post(\n '/api/openclaw/deepseek/v1/chat/completions',\n asyncHandler(async (req, res) => {\n const user = await resolveUser(req, req.body || {});\n const incomingMessages = normalizeOpenAiMessages(req.body?.messages);\n if (incomingMessages.length === 0) {\n res.status(400).json({ error: { message: 'messages is required' } });\n return;\n }\n\n const messages = compactOpenClawMessagesForDeepSeek(incomingMessages);\n const promptEstimate = estimateTokens(JSON.stringify(messages));\n const estimated = promptEstimate + 600;\n const budget = enforceTokenBudget(user, estimated);\n const maxCompletionTokens = Math.min(\n completionBudgetFor(budget, promptEstimate),\n normalizeMaxCompletionTokens(req.body?.max_tokens),\n );\n const model = normalizeDeepSeekModelName(req.body?.model);\n const wantsStream =\n req.body?.stream === true ||\n String(req.get('accept') || '').includes('text/event-stream');\n const id = `chatcmpl_${crypto.randomUUID().replaceAll('-', '')}`;\n const created = Math.floor(Date.now() / 1000);\n const compactedMessages = messages;\n\n if (!wantsStream) {\n let result;\n let usageMessages = messages;\n try {\n result = await callDeepSeek(messages, { model, maxCompletionTokens });\n } catch (error) {\n if (!shouldRetryWithCompactedOpenClawMessages(error, messages, compactedMessages)) {\n throw error;\n }\n logger.warn(\n {\n statusCode: error?.statusCode,\n messageCount: messages.length,\n compactedMessageCount: compactedMessages.length,\n },\n 'OpenClaw DeepSeek proxy retrying with compacted messages',\n );\n usageMessages = compactedMessages;\n result = await callDeepSeek(compactedMessages, { model, maxCompletionTokens });\n }\n const usage = usageWithFallback(result.usage, usageMessages, result.message);\n recordUsage(user.userId, usage.totalTokens);\n res.json({\n id,\n object: 'chat.completion',\n created,\n model: result.model || model,\n choices: [\n {\n index: 0,\n message: { role: 'assistant', content: result.message },\n finish_reason: 'stop',\n },\n ],\n usage: openAiUsagePayload(usage, user, budget),\n });\n return;\n }\n\n const abortController = new AbortController();\n req.on('close', () => abortController.abort());\n\n res.status(200);\n res.set({\n 'Content-Type': 'text/event-stream; charset=utf-8',\n 'Cache-Control': 'no-cache, no-transform',\n Connection: 'keep-alive',\n 'X-Accel-Buffering': 'no',\n });\n res.flushHeaders?.();\n\n const baseChunk = {\n id,\n object: 'chat.completion.chunk',\n created,\n model,\n system_fingerprint: 'fabushi-openclaw-deepseek-proxy',\n };\n writeOpenAiSse(res, {\n ...baseChunk,\n choices: [{ index: 0, delta: { role: 'assistant' }, finish_reason: null }],\n });\n\n let text = '';\n let usageMessages = messages;\n try {\n let result;\n try {\n result = await callDeepSeekStream(messages, {\n model,\n maxCompletionTokens,\n signal: abortController.signal,\n onToken: (delta) => {\n text += delta;\n writeOpenAiSse(res, {\n ...baseChunk,\n choices: [{ index: 0, delta: { content: delta }, finish_reason: null }],\n });\n },\n });\n } catch (error) {\n if (!shouldRetryWithCompactedOpenClawMessages(error, messages, compactedMessages)) {\n throw error;\n }\n logger.warn(\n {\n statusCode: error?.statusCode,\n messageCount: messages.length,\n compactedMessageCount: compactedMessages.length,\n },\n 'OpenClaw DeepSeek proxy stream retrying with compacted messages',\n );\n text = '';\n usageMessages = compactedMessages;\n result = await callDeepSeekStream(compactedMessages, {\n model,\n maxCompletionTokens,\n signal: abortController.signal,\n onToken: (delta) => {\n text += delta;\n writeOpenAiSse(res, {\n ...baseChunk,\n choices: [{ index: 0, delta: { content: delta }, finish_reason: null }],\n });\n },\n });\n }\n text = result.message || text;\n const usage = usageWithFallback(result.usage, usageMessages, text);\n recordUsage(user.userId, usage.totalTokens);\n writeOpenAiSse(res, {\n ...baseChunk,\n model: result.model || model,\n choices: [{ index: 0, delta: {}, finish_reason: 'stop' }],\n });\n writeOpenAiSse(res, {\n ...baseChunk,\n model: result.model || model,\n choices: [],\n usage: openAiUsagePayload(usage, user, budget),\n });\n res.write('data: [DONE]\\n\\n');\n } catch (error) {\n if (isDeepSeekBlockedError(error)) {\n const fallbackMessage = openClawBlockedFallbackText(usageMessages);\n const usage = usageWithFallback(null, usageMessages, fallbackMessage);\n recordUsage(user.userId, usage.totalTokens);\n logger.warn(\n { statusCode: error?.statusCode, code: error?.code },\n 'OpenClaw DeepSeek proxy stream blocked; returning local fallback message',\n );\n writeOpenAiSse(res, {\n ...baseChunk,\n choices: [{ index: 0, delta: { content: fallbackMessage }, finish_reason: null }],\n });\n writeOpenAiSse(res, {\n ...baseChunk,\n choices: [{ index: 0, delta: {}, finish_reason: 'stop' }],\n });\n writeOpenAiSse(res, {\n ...baseChunk,\n choices: [],\n usage: openAiUsagePayload(usage, user, budget),\n });\n res.write('data: [DONE]\\n\\n');\n return;\n }\n logger.error(\n { error: String(error), code: error?.code, statusCode: error?.statusCode },\n 'OpenClaw DeepSeek proxy stream failed',\n );\n writeOpenAiSse(res, {\n ...baseChunk,\n choices: [],\n error: {\n message: publicAiErrorMessage(error),\n code: error?.code || 'OPENCLAW_DEEPSEEK_PROXY_ERROR',\n },\n });\n res.write('data: [DONE]\\n\\n');\n } finally {\n res.end();\n }\n }),\n);\n\napp.post(\n '/codex-deepseek/v1/responses',\n asyncHandler(async (req, res) => {\n if (!deepseekApiKey) {\n res.status(401).json({\n error: { message: 'Unauthorized Codex DeepSeek adapter request' },\n });\n return;\n }\n\n const prompt = codexResponsesPrompt(req.body);\n if (!prompt) {\n res.status(400).json({ error: { message: 'Responses input is required' } });\n return;\n }\n\n const { user, recordUsage: shouldRecordUsage } = await resolveCodexResponsesBilling(req);\n const messages = [{ role: 'user', content: prompt }];\n const promptEstimate = estimateTokens(JSON.stringify(messages));\n const budget = enforceTokenBudget(user, promptEstimate + 600);\n const maxCompletionTokens = Math.min(\n completionBudgetFor(budget, promptEstimate),\n normalizeMaxCompletionTokens(req.body?.max_output_tokens || req.body?.max_tokens),\n );\n const model = normalizeDeepSeekModelName(req.body?.model);\n const responseId = `resp_${crypto.randomUUID().replaceAll('-', '')}`;\n const itemId = `msg_${crypto.randomUUID().replaceAll('-', '')}`;\n const wantsStream =\n req.body?.stream !== false ||\n String(req.get('accept') || '').includes('text/event-stream');\n\n if (!wantsStream) {\n const result = await callDeepSeek(messages, { model, maxCompletionTokens });\n const usage = usageWithFallback(result.usage, messages, result.message);\n if (shouldRecordUsage) recordUsage(user.userId, usage.totalTokens);\n res.json(\n responsesPayload({\n responseId,\n itemId,\n status: 'completed',\n model: result.model || model,\n text: result.message,\n usage,\n }),\n );\n return;\n }\n\n res.status(200);\n res.set({\n 'Content-Type': 'text/event-stream; charset=utf-8',\n 'Cache-Control': 'no-cache, no-transform',\n Connection: 'keep-alive',\n 'X-Accel-Buffering': 'no',\n });\n res.flushHeaders?.();\n\n writeResponsesEvent(res, 'response.created', {\n type: 'response.created',\n response: responsesPayload({\n responseId,\n itemId,\n status: 'in_progress',\n model,\n }),\n });\n writeResponsesEvent(res, 'response.output_item.added', {\n type: 'response.output_item.added',\n output_index: 0,\n item: {\n id: itemId,\n type: 'message',\n role: 'assistant',\n status: 'in_progress',\n content: [],\n },\n });\n writeResponsesEvent(res, 'response.content_part.added', {\n type: 'response.content_part.added',\n item_id: itemId,\n output_index: 0,\n content_index: 0,\n part: { type: 'output_text', text: '' },\n });\n\n let text = '';\n try {\n const result = await callDeepSeekStream(messages, {\n model,\n maxCompletionTokens,\n onToken: (delta) => {\n text += delta;\n writeResponsesEvent(res, 'response.output_text.delta', {\n type: 'response.output_text.delta',\n item_id: itemId,\n output_index: 0,\n content_index: 0,\n delta,\n });\n },\n });\n text = result.message || text;\n const usage = usageWithFallback(result.usage, messages, text);\n if (shouldRecordUsage) recordUsage(user.userId, usage.totalTokens);\n writeResponsesEvent(res, 'response.output_text.done', {\n type: 'response.output_text.done',\n item_id: itemId,\n output_index: 0,\n content_index: 0,\n text,\n });\n writeResponsesEvent(res, 'response.content_part.done', {\n type: 'response.content_part.done',\n item_id: itemId,\n output_index: 0,\n content_index: 0,\n part: { type: 'output_text', text },\n });\n writeResponsesEvent(res, 'response.output_item.done', {\n type: 'response.output_item.done',\n output_index: 0,\n item: {\n id: itemId,\n type: 'message',\n role: 'assistant',\n status: 'completed',\n content: [{ type: 'output_text', text }],\n },\n });\n writeResponsesEvent(res, 'response.completed', {\n type: 'response.completed',\n response: responsesPayload({\n responseId,\n itemId,\n status: 'completed',\n model: result.model || model,\n text,\n usage,\n }),\n });\n res.end();\n } catch (error) {\n writeResponsesEvent(res, 'response.failed', {\n type: 'response.failed',\n response: responsesPayload({\n responseId,\n itemId,\n status: 'failed',\n model,\n text,\n error,\n }),\n });\n res.end();\n }\n }),\n);\n\napp.use(\n '/api/openclaw/runtime/files',\n express.static(openClawRuntimeUpdatesDir, {\n fallthrough: false,\n immutable: true,\n maxAge: '7d',\n setHeaders(res) {\n res.setHeader('X-Content-Type-Options', 'nosniff');\n },\n }),\n);\n\napp.get(\n '/api/openclaw/runtime/manifest',\n asyncHandler(async (req, res) => {\n const platform = readText(req.query.platform);\n const currentVersion = readText(req.query.currentVersion);\n const appVersion = readText(req.query.appVersion);\n\n let manifest;\n try {\n manifest = await readJsonFile(openClawRuntimeManifestPath);\n } catch (error) {\n logger.info(\n { manifestPath: openClawRuntimeManifestPath, error: String(error) },\n 'OpenClaw runtime manifest unavailable',\n );\n return jsonResponse(res, 200, {\n success: true,\n schema: 1,\n updateAvailable: false,\n reason: 'manifest-unavailable',\n platform,\n currentVersion,\n appVersion,\n });\n }\n\n const latest = manifest.latest && typeof manifest.latest === 'object'\n ? manifest.latest\n : manifest;\n const platforms = latest.platforms && typeof latest.platforms === 'object'\n ? latest.platforms\n : {};\n const platformRelease = platform ? platforms[platform] : null;\n const version = readText(latest.version || manifest.version);\n\n if (!platform || !platformRelease || typeof platformRelease !== 'object') {\n return jsonResponse(res, 200, {\n success: true,\n schema: Number(manifest.schema || 1),\n channel: readText(manifest.channel || latest.channel || 'stable'),\n updateAvailable: false,\n reason: platform ? 'platform-unavailable' : 'platform-required',\n platform,\n currentVersion,\n appVersion,\n latestVersion: version,\n });\n }\n\n if (platform.startsWith('macos-') && !openClawRuntimeEnableMacosUpdates) {\n return jsonResponse(res, 200, {\n success: true,\n schema: Number(manifest.schema || 1),\n channel: readText(manifest.channel || latest.channel || 'stable'),\n updateAvailable: false,\n reason: 'macos-runtime-updates-disabled',\n platform,\n currentVersion,\n appVersion,\n latestVersion: version,\n });\n }\n\n const downloadUrl = absolutizeOpenClawRuntimeUrl(\n req,\n platformRelease.archiveUrl || platformRelease.url || platformRelease.fileName,\n );\n const release = {\n version,\n channel: readText(manifest.channel || latest.channel || 'stable'),\n minAppVersion: readText(latest.minAppVersion || platformRelease.minAppVersion),\n notes: readText(latest.notes || platformRelease.notes),\n nodeExecutable: readText(\n platformRelease.nodeExecutable || latest.nodeExecutable || 'node/bin/node',\n ),\n cliEntrypoint: readText(\n platformRelease.cliEntrypoint || latest.cliEntrypoint || 'openclaw/openclaw.mjs',\n ),\n defaultPort: Number(latest.defaultPort || manifest.defaultPort || 18789),\n defaultModel: readText(latest.defaultModel || manifest.defaultModel || 'openclaw/default'),\n defaultModelOverride: readText(latest.defaultModelOverride || manifest.defaultModelOverride),\n gatewayArgs: Array.isArray(latest.gatewayArgs)\n ? latest.gatewayArgs.map((item) => String(item))\n : ['gateway', '--port', '{port}', '--force'],\n archiveUrl: downloadUrl,\n sha256: readText(platformRelease.sha256),\n size: Number(platformRelease.size || 0),\n };\n\n jsonResponse(res, 200, {\n success: true,\n schema: Number(manifest.schema || 1),\n updateAvailable: Boolean(version && downloadUrl && version !== currentVersion),\n platform,\n currentVersion,\n appVersion,\n latest: release,\n generatedAt: readText(manifest.generatedAt || latest.generatedAt),\n });\n }),\n);\n\napp.get('/health', (_req, res) => {\n const codexRuntime = createCodexDeepSeekRuntime();\n const libreChatRuntime = createLibreChatAgentRuntime();\n const openClawRuntime = createOpenClawRuntime();\n jsonResponse(res, 200, {\n status: 'ok',\n service: 'dacheng-ai-backend',\n provider: libreChatRuntime.enabled ? libreChatRuntime.provider : 'deepseek',\n model: libreChatRuntime.enabled ? libreChatRuntime.model : deepseekModel,\n deepseekModel,\n deepseekAvailableModels,\n libreChatAgentChatEnabled: libreChatRuntime.enabled,\n libreChatAgentProvider: libreChatRuntime.provider,\n libreChatAgentReason: libreChatRuntime.enabled ? undefined : libreChatRuntime.reason,\n codexSdkAvailable: true,\n codexSdkChatEnabled: codexRuntime.enabled,\n codexSdkProvider: codexRuntime.provider,\n openClawAgentChatEnabled: openClawRuntime.enabled,\n openClawAgentProvider: openClawRuntime.provider,\n openClawAgentReason: openClawRuntime.enabled ? undefined : openClawRuntime.reason,\n requireMemberForAi,\n memberMonthlyLimit,\n freeMonthlyLimit,\n deepseekMaxCompletionTokens,\n openClawRuntimeUpdatesDir,\n openClawRuntimeManifestConfigured: fs.existsSync(openClawRuntimeManifestPath),\n cbetaApiRoot,\n timestamp: nowIso(),\n });\n});\n\napp.get(\n '/api/miniapps/registry',\n asyncHandler(async (req, res) => {\n const user = await resolveUser(req, { username: req.query.username });\n const official = officialMiniAppRegistry(req);\n const store = await readMiniAppStore();\n const visibleSandbox = store.apps.filter((appRecord) => {\n if (appRecord.reviewStatus === 'approved' && appRecord.source === 'marketplace') {\n return true;\n }\n return appRecord.ownerId === user.userId;\n });\n const bots = [\n ...official.bots,\n ...visibleSandbox.map((record) => miniAppRecordToBot(record)),\n ];\n const miniApps = [\n ...official.miniApps,\n ...visibleSandbox.map((record) => miniAppRecordToManifest(req, record)),\n ];\n const registry = {\n schemaVersion: 1,\n hostApiVersion: miniAppHostApiVersion,\n bots,\n miniApps,\n updatedAt: nowIso(),\n };\n registry.signature = sha256(JSON.stringify({ bots, miniApps }));\n jsonResponse(res, 200, { success: true, registry });\n }),\n);\n\napp.get(\n '/api/miniapps/dev/:miniAppId/index.html',\n asyncHandler(async (req, res) => {\n const miniAppId = safeMiniAppId(req.params.miniAppId);\n const store = await readMiniAppStore();\n const record = store.apps.find((appRecord) => appRecord.miniAppId === miniAppId);\n if (!record) {\n res.status(404).type('text/html').send('<h1>Mini app not found</h1>');\n return;\n }\n res.setHeader('Content-Type', 'text/html; charset=utf-8');\n res.setHeader('Cache-Control', 'no-store');\n res.send(record.sourceHtml || generatedMiniAppHtml(record));\n }),\n);\n\napp.post(\n '/api/miniapps/dev/create',\n asyncHandler(async (req, res) => {\n const user = await resolveUser(req, req.body || {});\n const title = safeUserText(req.body?.title, '个人小程序').slice(0, 80);\n const subtitle = safeUserText(req.body?.subtitle, '个人沙箱小程序').slice(0, 120);\n const permissions = sanitizeMiniAppPermissions(req.body?.permissions);\n const prompt = safeUserText(req.body?.prompt || title);\n const idSeed = crypto.randomUUID().replaceAll('-', '').slice(0, 12);\n const miniAppId = `sandbox.${idSeed}`;\n const botId = `bot.${miniAppId}`;\n const sourceHtml = generatedMiniAppHtml({ title, prompt });\n const scan = miniAppScan(permissions, sourceHtml);\n const createdAt = nowIso();\n const record = {\n ownerId: user.userId,\n miniAppId,\n botId,\n title,\n subtitle,\n initials: title.slice(0, 1) || '小',\n icon: 'code',\n avatarColor: '#3D8BFF',\n greeting: '你好,我是这个个人沙箱小程序的机器人。',\n inputHint: '输入消息,或打开小程序面板',\n version: '0.0.1',\n permissions,\n surfaces: ['chatPanel'],\n theme: 'telegramDark',\n source: 'sandbox',\n reviewStatus: 'sandbox',\n sourceHtml,\n scan,\n createdAt,\n updatedAt: createdAt,\n };\n const store = await readMiniAppStore();\n store.apps.push(record);\n await writeMiniAppStore(store);\n jsonResponse(res, 200, {\n success: true,\n miniApp: miniAppRecordToManifest(req, record),\n bot: miniAppRecordToBot(record),\n scan,\n });\n }),\n);\n\napp.post(\n '/api/miniapps/dev/:miniAppId/version',\n asyncHandler(async (req, res) => {\n const user = await resolveUser(req, req.body || {});\n const miniAppId = safeMiniAppId(req.params.miniAppId);\n const store = await readMiniAppStore();\n const index = store.apps.findIndex((appRecord) => appRecord.miniAppId === miniAppId);\n if (index < 0) {\n return jsonResponse(res, 404, { success: false, message: 'mini app not found' });\n }\n const record = store.apps[index];\n if (record.ownerId !== user.userId) {\n return jsonResponse(res, 403, { success: false, message: 'not your mini app' });\n }\n const title = safeUserText(req.body?.title, record.title).slice(0, 80);\n const subtitle = safeUserText(req.body?.subtitle, record.subtitle).slice(0, 120);\n const sourceHtml =\n safeUserText(req.body?.sourceHtml) ||\n generatedMiniAppHtml({ title, prompt: safeUserText(req.body?.prompt || title) });\n const permissions = sanitizeMiniAppPermissions(req.body?.permissions || record.permissions);\n const scan = miniAppScan(permissions, sourceHtml);\n store.apps[index] = {\n ...record,\n title,\n subtitle,\n sourceHtml,\n permissions,\n scan,\n reviewStatus: 'sandbox',\n version: safeUserText(req.body?.version, record.version || '0.0.1'),\n updatedAt: nowIso(),\n };\n await writeMiniAppStore(store);\n jsonResponse(res, 200, {\n success: true,\n miniApp: miniAppRecordToManifest(req, store.apps[index]),\n bot: miniAppRecordToBot(store.apps[index]),\n scan,\n });\n }),\n);\n\napp.post(\n '/api/miniapps/:miniAppId/submit-review',\n asyncHandler(async (req, res) => {\n const user = await resolveUser(req, req.body || {});\n const miniAppId = safeMiniAppId(req.params.miniAppId);\n const store = await readMiniAppStore();\n const index = store.apps.findIndex((appRecord) => appRecord.miniAppId === miniAppId);\n if (index < 0) {\n return jsonResponse(res, 404, { success: false, message: 'mini app not found' });\n }\n const record = store.apps[index];\n if (record.ownerId !== user.userId) {\n return jsonResponse(res, 403, { success: false, message: 'not your mini app' });\n }\n const scan = miniAppScan(record.permissions || [], record.sourceHtml || '');\n if (!scan.passed) {\n return jsonResponse(res, 400, {\n success: false,\n message: 'security scan failed',\n scan,\n });\n }\n store.apps[index] = {\n ...record,\n scan,\n reviewStatus: 'pending_review',\n updatedAt: nowIso(),\n };\n await writeMiniAppStore(store);\n jsonResponse(res, 200, {\n success: true,\n miniApp: miniAppRecordToManifest(req, store.apps[index]),\n scan,\n });\n }),\n);\n\napp.post(\n '/api/botfather/generate-miniapp',\n asyncHandler(async (req, res) => {\n const user = await resolveUser(req, req.body || {});\n const prompt = safeUserText(req.body?.prompt);\n if (!prompt) {\n return jsonResponse(res, 400, { success: false, message: 'prompt is required' });\n }\n const title = titleFromPromptForMiniApp(prompt);\n const subtitle = '机器人之父生成的个人沙箱小程序';\n const requestedPermissions = Array.isArray(req.body?.permissions) ? req.body.permissions : [];\n const generated = await generateBotFatherMiniAppHtml({ title, prompt, user });\n let sourceHtml = generated.sourceHtml;\n let generation = generated.generation;\n const permissions = sanitizeMiniAppPermissions([\n ...requestedPermissions,\n ...inferMiniAppPermissionsFromText(prompt, sourceHtml),\n ]);\n let scan = miniAppScan(permissions, sourceHtml);\n if (!scan.passed) {\n sourceHtml = generatedMiniAppHtml({ title, prompt });\n generation = {\n provider: 'template',\n fallbackReason: 'ai_output_failed_security_scan',\n };\n scan = miniAppScan(permissions, sourceHtml);\n }\n const idSeed = crypto.randomUUID().replaceAll('-', '').slice(0, 12);\n const miniAppId = `sandbox.${idSeed}`;\n const createdAt = nowIso();\n const record = {\n ownerId: user.userId,\n miniAppId,\n botId: `bot.${miniAppId}`,\n title,\n subtitle,\n initials: title.slice(0, 1) || '小',\n icon: 'code',\n avatarColor: '#3D8BFF',\n greeting: `我是「${title}」的机器人。`,\n inputHint: '继续描述修改需求,或打开小程序面板',\n version: '0.0.1',\n permissions,\n surfaces: ['chatPanel'],\n theme: 'telegramDark',\n source: 'sandbox',\n reviewStatus: 'sandbox',\n sourceHtml,\n scan,\n generation,\n prompt,\n createdAt,\n updatedAt: createdAt,\n };\n const store = await readMiniAppStore();\n store.apps.push(record);\n await writeMiniAppStore(store);\n jsonResponse(res, 200, {\n success: true,\n miniApp: miniAppRecordToManifest(req, record),\n bot: miniAppRecordToBot(record),\n scan,\n generation,\n actions: ['openMiniApp', 'continueEditing', 'submitReview'],\n });\n }),\n);\n\napp.get('/api/ai/models', (_req, res) => {\n jsonResponse(res, 200, {\n success: true,\n provider: 'deepseek',\n defaultModel: deepseekModel,\n models: deepseekAvailableModels.map((model) => ({\n id: model,\n label: model === 'deepseek-reasoner' ? 'DeepSeek Reasoner' : 'DeepSeek Chat',\n })),\n });\n});\napp.get(\n '/api/ai/quota',\n asyncHandler(async (req, res) => {\n const user = await resolveUser(req, { username: req.query.username });\n const budget = enforceTokenBudget(user, 0);\n const usage = usageFor(user.userId);\n jsonResponse(res, 200, {\n success: true,\n isMember: user.isMember,\n monthlyLimit: budget.limit,\n usedTokens: usage.used,\n remainingTokens: Math.max(0, budget.limit - usage.used),\n month: usage.month,\n });\n }),\n);\n\napp.post(\n '/api/agent/chat',\n asyncHandler(async (req, res) => {\n const message = safeUserText(req.body?.message);\n if (!message) {\n return jsonResponse(res, 400, { success: false, message: 'message is required' });\n }\n\n const user = await resolveUser(req, req.body || {});\n const promptEstimate = estimateTokens(message);\n const estimated = promptEstimate + 600;\n const budget = enforceTokenBudget(user, estimated);\n const maxCompletionTokens = completionBudgetFor(budget, promptEstimate);\n const createdAt = nowIso();\n const conversationId = safeUserText(req.body?.conversationId) || crypto.randomUUID();\n const mode = normalizeAgentMode(req.body?.mode);\n const stream = req.body?.stream === true;\n let conversation = statements.getConversation.get(conversationId, user.userId);\n const isNewConversation = !conversation;\n\n if (isNewConversation) {\n conversation = { id: conversationId, user_id: user.userId, title: titleFromMessage(message) };\n statements.insertConversation.run({\n id: conversationId,\n userId: user.userId,\n title: conversation.title,\n provider: 'agent',\n model: openClawAgentId,\n createdAt,\n updatedAt: createdAt,\n });\n }\n\n const messageId = safeUserText(req.body?.messageId) || crypto.randomUUID();\n statements.insertMessage.run({\n id: messageId,\n conversationId,\n role: 'user',\n content: message,\n promptTokens: 0,\n completionTokens: 0,\n totalTokens: 0,\n createdAt,\n });\n\n const runId = `run_${crypto.randomUUID().replaceAll('-', '')}`;\n const sessionKey = openClawSessionKey(user, conversationId);\n const openClawRuntime = createOpenClawRuntime();\n statements.insertAgentRun.run({\n id: runId,\n userId: user.userId,\n conversationId,\n messageId,\n openClawRunId: null,\n openClawSessionKey: sessionKey,\n status: stream ? 'queued' : 'running',\n mode,\n provider: openClawRuntime.enabled ? openClawRuntime.provider : 'dacheng-ai-fallback',\n model: openClawRuntime.enabled ? openClawAgentId : deepseekModel,\n startedAt: createdAt,\n });\n\n if (stream) {\n return jsonResponse(res, 200, {\n success: true,\n runId,\n conversationId,\n streamUrl: `/api/agent/runs/${runId}/events`,\n });\n }\n\n const controller = new AbortController();\n activeAgentRuns.set(runId, controller);\n try {\n const skillResult = await runResourceFinderSkill({ message, user });\n const skillContext = resourceContextMessage(skillResult);\n const historyRows = statements.listMessages.all(conversationId).slice(-14);\n const modelMessages = [\n { role: 'system', content: systemPrompt },\n ...(skillContext ? [{ role: 'system', content: skillContext }] : []),\n ...normalizeMessages(historyRows),\n ];\n\n const aiResult = await runAgentModel({\n messages: modelMessages,\n user,\n conversationId,\n mode,\n signal: controller.signal,\n maxCompletionTokens,\n });\n const usage = {\n promptTokens: aiResult.usage?.promptTokens || estimateTokens(JSON.stringify(modelMessages)),\n completionTokens: aiResult.usage?.completionTokens || estimateTokens(aiResult.message),\n totalTokens:\n aiResult.usage?.totalTokens ||\n estimateTokens(JSON.stringify(modelMessages)) + estimateTokens(aiResult.message),\n };\n if (!aiResult.usageAlreadyRecorded) recordUsage(user.userId, usage.totalTokens);\n const answeredAt = nowIso();\n const assistantMessageId = crypto.randomUUID();\n statements.insertMessage.run({\n id: assistantMessageId,\n conversationId,\n role: 'assistant',\n content: aiResult.message,\n promptTokens: usage.promptTokens,\n completionTokens: usage.completionTokens,\n totalTokens: usage.totalTokens,\n createdAt: answeredAt,\n });\n statements.updateConversation.run({\n id: conversationId,\n userId: user.userId,\n title: isNewConversation ? titleFromMessage(message) : conversation.title,\n updatedAt: answeredAt,\n });\n updateAgentRunStatus({ id: runId, user_id: user.userId }, 'completed', {\n openClawRunId: aiResult.runId,\n inputTokens: usage.promptTokens,\n outputTokens: usage.completionTokens,\n });\n const action = determineClientAction(skillResult, message);\n return jsonResponse(res, 200, {\n success: true,\n runId,\n conversationId,\n messageId: assistantMessageId,\n provider: aiResult.provider,\n model: aiResult.model,\n message: aiResult.message,\n usage: agentUsagePayload(usage, user, budget),\n ...(action ? { clientAction: action } : {}),\n });\n } catch (error) {\n const status = error.name === 'AbortError' ? 'cancelled' : 'failed';\n updateAgentRunStatus({ id: runId, user_id: user.userId }, status, {\n errorCode: error.name || 'AGENT_RUN_FAILED',\n errorMessage: publicAiErrorMessage(error),\n });\n if (status === 'cancelled') {\n return jsonResponse(res, 499, { success: false, runId, status: 'cancelled' });\n }\n throw error;\n } finally {\n activeAgentRuns.delete(runId);\n }\n }),\n);\n\napp.get(\n '/api/agent/runs/:runId/events',\n asyncHandler(async (req, res) => {\n const user = await resolveUser(req, {});\n const run = statements.getAgentRun.get(req.params.runId, user.userId);\n if (!run) {\n return jsonResponse(res, 404, { success: false, message: 'run not found' });\n }\n if (run.status === 'completed' || run.status === 'failed' || run.status === 'cancelled') {\n return jsonResponse(res, 409, { success: false, message: `run is already ${run.status}` });\n }\n\n sseHeaders(res);\n const controller = new AbortController();\n activeAgentRuns.set(run.id, controller);\n updateAgentRunStatus(run, 'running');\n writeSse(res, 'run.started', { runId: run.id, conversationId: run.conversation_id });\n\n try {\n let streamedText = '';\n const modelMessages = buildAgentMessages(run.conversation_id);\n const lastUserMsg = [...modelMessages].reverse().find(m => m.role === 'user');\n const userMsgText = lastUserMsg ? lastUserMsg.content : '';\n const skillResult = userMsgText ? await runResourceFinderSkill({\n message: userMsgText,\n user,\n onStep: (step) => writeSse(res, 'tool.call.completed', {\n tool: 'agent.step',\n displayName: step.title || 'Agent step',\n summary: step.message || '',\n }),\n }) : null;\n const skillContext = resourceContextMessage(skillResult);\n const finalMessages = skillContext ? [\n modelMessages[0],\n { role: 'system', content: skillContext },\n ...modelMessages.slice(1)\n ] : modelMessages;\n\n const promptEstimate = estimateTokens(JSON.stringify(finalMessages));\n const estimated = promptEstimate + 600;\n const budget = enforceTokenBudget(user, estimated);\n const maxCompletionTokens = completionBudgetFor(budget, promptEstimate);\n const aiResult = await runAgentModel({\n messages: finalMessages,\n user,\n conversationId: run.conversation_id,\n mode: run.mode,\n signal: controller.signal,\n maxCompletionTokens,\n callbacks: {\n onStep: (step) => writeSse(res, 'tool.call.completed', {\n tool: 'agent.step',\n displayName: step.title || 'Agent step',\n summary: step.message || '',\n }),\n onToken: (delta) => {\n streamedText += delta;\n writeSse(res, 'assistant.delta', { text: delta });\n },\n },\n });\n const message = aiResult.message || streamedText.trim();\n const usage = {\n promptTokens: aiResult.usage?.promptTokens || estimateTokens(JSON.stringify(finalMessages)),\n completionTokens: aiResult.usage?.completionTokens || estimateTokens(message),\n totalTokens:\n aiResult.usage?.totalTokens || estimateTokens(JSON.stringify(finalMessages)) + estimateTokens(message),\n };\n if (!aiResult.usageAlreadyRecorded) recordUsage(user.userId, usage.totalTokens);\n const assistantMessageId = crypto.randomUUID();\n const answeredAt = nowIso();\n statements.insertMessage.run({\n id: assistantMessageId,\n conversationId: run.conversation_id,\n role: 'assistant',\n content: message,\n promptTokens: usage.promptTokens,\n completionTokens: usage.completionTokens,\n totalTokens: usage.totalTokens,\n createdAt: answeredAt,\n });\n statements.updateConversation.run({\n id: run.conversation_id,\n userId: user.userId,\n title: titleFromMessage(message),\n updatedAt: answeredAt,\n });\n updateAgentRunStatus(run, 'completed', {\n openClawRunId: aiResult.runId,\n inputTokens: usage.promptTokens,\n outputTokens: usage.completionTokens,\n });\n const action = determineClientAction(skillResult, userMsgText);\n writeSse(res, 'assistant.message', { messageId: assistantMessageId, content: message });\n writeSse(res, 'run.completed', {\n usage: agentUsagePayload(usage, user, budget),\n ...(action ? { clientAction: action } : {}),\n });\n res.end();\n } catch (error) {\n const status = error.name === 'AbortError' ? 'cancelled' : 'failed';\n updateAgentRunStatus(run, status, {\n errorCode: error.name || 'AGENT_RUN_FAILED',\n errorMessage: publicAiErrorMessage(error),\n });\n writeSse(res, status === 'cancelled' ? 'run.cancelled' : 'run.failed', {\n runId: run.id,\n message: publicAiErrorMessage(error),\n });\n res.end();\n } finally {\n activeAgentRuns.delete(run.id);\n }\n }),\n);\n\napp.post(\n '/api/agent/runs/:runId/cancel',\n asyncHandler(async (req, res) => {\n const user = await resolveUser(req, req.body || {});\n const run = statements.getAgentRun.get(req.params.runId, user.userId);\n if (!run) {\n return jsonResponse(res, 404, { success: false, message: 'run not found' });\n }\n const controller = activeAgentRuns.get(run.id);\n if (controller) controller.abort();\n updateAgentRunStatus(run, 'cancelled');\n return jsonResponse(res, 200, { success: true, runId: run.id, status: 'cancelled' });\n }),\n);\n\napp.post(\n '/api/agent/messages/:messageId/feedback',\n asyncHandler(async (req, res) => {\n const user = await resolveUser(req, req.body || {});\n const rating = safeUserText(req.body?.rating);\n if (!['up', 'down'].includes(rating)) {\n return jsonResponse(res, 400, { success: false, message: 'rating must be up or down' });\n }\n statements.insertAgentFeedback.run({\n id: crypto.randomUUID(),\n messageId: req.params.messageId,\n userId: user.userId,\n rating,\n reason: safeUserText(req.body?.reason),\n comment: safeUserText(req.body?.comment),\n createdAt: nowIso(),\n });\n return jsonResponse(res, 200, { success: true });\n }),\n);\n\napp.post(\n '/api/ai/chat',\n asyncHandler(async (req, res) => {\n const message = safeUserText(req.body?.message);\n if (!message) {\n return jsonResponse(res, 400, { success: false, message: 'message is required' });\n }\n\n const user = await resolveUser(req, req.body || {});\n const promptEstimate = estimateTokens(message);\n const estimated = promptEstimate + 600;\n const budget = enforceTokenBudget(user, estimated);\n const maxCompletionTokens = completionBudgetFor(budget, promptEstimate);\n const createdAt = nowIso();\n const conversationId = safeUserText(req.body?.conversationId) || crypto.randomUUID();\n const requestedModel = normalizeDeepSeekModelName(req.body?.model);\n const clientContext = clientContextMessage(req.body?.client);\n let conversation = statements.getConversation.get(conversationId, user.userId);\n const isNewConversation = !conversation;\n\n if (isNewConversation) {\n conversation = {\n id: conversationId,\n user_id: user.userId,\n title: titleFromMessage(message),\n };\n statements.insertConversation.run({\n id: conversationId,\n userId: user.userId,\n title: conversation.title,\n provider: 'deepseek',\n model: requestedModel,\n createdAt,\n updatedAt: createdAt,\n });\n }\n\n statements.insertMessage.run({\n id: crypto.randomUUID(),\n conversationId,\n role: 'user',\n content: message,\n promptTokens: 0,\n completionTokens: 0,\n totalTokens: 0,\n createdAt,\n });\n\n const libreChatRuntime = createLibreChatAgentRuntime();\n const skillResult = await runResourceFinderSkill({ message, user });\n const skillContext = resourceContextMessage(skillResult);\n const historyRows = statements.listMessages.all(conversationId).slice(-14);\n const modelMessages = [\n { role: 'system', content: systemPrompt },\n ...(clientContext ? [{ role: 'system', content: clientContext }] : []),\n ...(skillContext ? [{ role: 'system', content: skillContext }] : []),\n ...normalizeMessages(historyRows),\n ];\n\n const openClawRuntime = createOpenClawRuntime();\n const codexRuntime = createCodexDeepSeekRuntime(user);\n let provider = openClawRuntime.enabled\n ? openClawRuntime.provider\n : libreChatRuntime.enabled\n ? libreChatRuntime.provider\n : 'deepseek';\n let responseModel = openClawRuntime.enabled\n ? openClawRuntime.model\n : libreChatRuntime.enabled\n ? libreChatRuntime.model\n : requestedModel;\n let aiResult;\n try {\n if (openClawRuntime.enabled) {\n aiResult = await callOpenClawAgent({\n messages: modelMessages,\n user,\n conversationId,\n mode: normalizeAgentMode(req.body?.mode),\n });\n provider = aiResult.provider || provider;\n responseModel = aiResult.model || responseModel;\n } else if (libreChatRuntime.enabled) {\n aiResult = await callLibreChatAgent(modelMessages);\n } else if (codexRuntime.enabled) {\n provider = codexRuntime.provider;\n aiResult = await callCodexSdkDeepSeek(modelMessages, { user, maxCompletionTokens });\n } else {\n aiResult = await callDeepSeek(modelMessages, { model: requestedModel, maxCompletionTokens });\n }\n } catch (error) {\n if (libreChatRuntime.enabled) {\n logger.warn({ error: String(error) }, 'LibreChat Agent chat failed, falling back to Codex SDK or DeepSeek direct');\n provider = codexRuntime.enabled ? codexRuntime.provider : 'deepseek';\n responseModel = requestedModel;\n if (codexRuntime.enabled) {\n try {\n aiResult = await callCodexSdkDeepSeek(modelMessages, { user, maxCompletionTokens });\n } catch (codexError) {\n if (isDeepSeekQuotaError(codexError)) throw codexError;\n logger.warn({ error: String(codexError) }, 'Codex SDK chat failed, falling back to DeepSeek direct');\n provider = 'deepseek';\n aiResult = await callDeepSeek(modelMessages, { model: requestedModel, maxCompletionTokens });\n }\n } else {\n aiResult = await callDeepSeek(modelMessages, { model: requestedModel, maxCompletionTokens });\n }\n } else {\n if (!codexRuntime.enabled || isDeepSeekQuotaError(error)) throw error;\n logger.warn({ error: String(error) }, 'Codex SDK chat failed, falling back to DeepSeek direct');\n provider = 'deepseek';\n responseModel = requestedModel;\n aiResult = await callDeepSeek(modelMessages, { model: requestedModel, maxCompletionTokens });\n }\n }\n const usage = {\n promptTokens: aiResult.usage.promptTokens || estimateTokens(JSON.stringify(modelMessages)),\n completionTokens: aiResult.usage.completionTokens || estimateTokens(aiResult.message),\n totalTokens:\n aiResult.usage.totalTokens ||\n estimateTokens(JSON.stringify(modelMessages)) + estimateTokens(aiResult.message),\n };\n if (!aiResult.usageAlreadyRecorded) recordUsage(user.userId, usage.totalTokens);\n\n const answeredAt = nowIso();\n statements.insertMessage.run({\n id: crypto.randomUUID(),\n conversationId,\n role: 'assistant',\n content: aiResult.message,\n promptTokens: usage.promptTokens,\n completionTokens: usage.completionTokens,\n totalTokens: usage.totalTokens,\n createdAt: answeredAt,\n });\n statements.updateConversation.run({\n id: conversationId,\n userId: user.userId,\n title: isNewConversation ? titleFromMessage(message) : conversation.title,\n updatedAt: answeredAt,\n });\n\n const latestUsage = usageFor(user.userId);\n const monthlyLimit = budget.limit;\n const action = determineClientAction(skillResult, message);\n jsonResponse(res, 200, {\n success: true,\n conversationId,\n provider,\n model: responseModel,\n message: aiResult.message,\n usage: {\n ...usage,\n monthlyLimit,\n remainingTokens: Math.max(0, monthlyLimit - latestUsage.used),\n },\n ...(action ? { clientAction: action } : {}),\n });\n }),\n);\n\napp.post('/api/ai/chat/stream', async (req, res) => {\n sseHeaders(res);\n\n try {\n const message = safeUserText(req.body?.message);\n if (!message) {\n writeSse(res, 'error', { message: 'message is required' });\n res.end();\n return;\n }\n\n const user = await resolveUser(req, req.body || {});\n const promptEstimate = estimateTokens(message);\n const estimated = promptEstimate + 600;\n const budget = enforceTokenBudget(user, estimated);\n const maxCompletionTokens = completionBudgetFor(budget, promptEstimate);\n const createdAt = nowIso();\n const conversationId = safeUserText(req.body?.conversationId) || crypto.randomUUID();\n const requestedModel = normalizeDeepSeekModelName(req.body?.model);\n const clientContext = clientContextMessage(req.body?.client);\n let conversation = statements.getConversation.get(conversationId, user.userId);\n const isNewConversation = !conversation;\n\n if (isNewConversation) {\n conversation = {\n id: conversationId,\n user_id: user.userId,\n title: titleFromMessage(message),\n };\n statements.insertConversation.run({\n id: conversationId,\n userId: user.userId,\n title: conversation.title,\n provider: 'deepseek',\n model: requestedModel,\n createdAt,\n updatedAt: createdAt,\n });\n }\n\n statements.insertMessage.run({\n id: crypto.randomUUID(),\n conversationId,\n role: 'user',\n content: message,\n promptTokens: 0,\n completionTokens: 0,\n totalTokens: 0,\n createdAt,\n });\n\n const codexRuntime = createCodexDeepSeekRuntime(user);\n const libreChatRuntime = createLibreChatAgentRuntime();\n const initialOpenClawRuntime = createOpenClawRuntime();\n let provider = initialOpenClawRuntime.enabled\n ? initialOpenClawRuntime.provider\n : libreChatRuntime.enabled\n ? libreChatRuntime.provider\n : codexRuntime.enabled\n ? codexRuntime.provider\n : 'deepseek';\n let responseModel = initialOpenClawRuntime.enabled\n ? initialOpenClawRuntime.model\n : libreChatRuntime.enabled\n ? libreChatRuntime.model\n : requestedModel;\n writeSse(res, 'meta', {\n conversationId,\n provider,\n model: responseModel,\n });\n const skillResult = await runResourceFinderSkill({\n message,\n user,\n onStep: (step) => writeSse(res, 'step', step),\n });\n const skillContext = resourceContextMessage(skillResult);\n const historyRows = statements.listMessages.all(conversationId).slice(-14);\n const modelMessages = [\n { role: 'system', content: systemPrompt },\n ...(clientContext ? [{ role: 'system', content: clientContext }] : []),\n ...(skillContext ? [{ role: 'system', content: skillContext }] : []),\n ...normalizeMessages(historyRows),\n ];\n\n let aiResult;\n const openClawRuntime = initialOpenClawRuntime;\n try {\n if (openClawRuntime.enabled) {\n aiResult = await callOpenClawAgent({\n messages: modelMessages,\n user,\n conversationId,\n mode: normalizeAgentMode(req.body?.mode),\n });\n provider = aiResult.provider || provider;\n responseModel = aiResult.model || responseModel;\n writeSse(res, 'delta', { text: aiResult.message });\n } else if (libreChatRuntime.enabled) {\n aiResult = await callLibreChatAgentStream(modelMessages, {\n onToken: (token) => writeSse(res, 'delta', { text: token }),\n });\n } else if (codexRuntime.enabled) {\n aiResult = await callCodexSdkDeepSeek(modelMessages, {\n user,\n maxCompletionTokens,\n onToken: (token) => writeSse(res, 'delta', { text: token }),\n onStep: (step) => writeSse(res, 'step', step),\n });\n } else {\n aiResult = await callDeepSeekStream(modelMessages, {\n model: requestedModel,\n maxCompletionTokens,\n onToken: (token) => writeSse(res, 'delta', { text: token }),\n });\n }\n } catch (error) {\n if (libreChatRuntime.enabled) {\n logger.warn({ error: String(error) }, 'LibreChat Agent streaming chat failed, falling back to Codex SDK or DeepSeek direct');\n provider = codexRuntime.enabled ? codexRuntime.provider : 'deepseek';\n responseModel = requestedModel;\n writeSse(res, 'meta', {\n conversationId,\n provider,\n model: responseModel,\n });\n if (codexRuntime.enabled) {\n try {\n aiResult = await callCodexSdkDeepSeek(modelMessages, {\n user,\n maxCompletionTokens,\n onToken: (token) => writeSse(res, 'delta', { text: token }),\n onStep: (step) => writeSse(res, 'step', step),\n });\n } catch (codexError) {\n if (isDeepSeekQuotaError(codexError)) throw codexError;\n logger.warn({ error: String(codexError) }, 'Codex SDK streaming chat failed, falling back to DeepSeek direct');\n provider = 'deepseek';\n responseModel = requestedModel;\n writeSse(res, 'meta', {\n conversationId,\n provider,\n model: responseModel,\n });\n aiResult = await callDeepSeekStream(modelMessages, {\n model: requestedModel,\n maxCompletionTokens,\n onToken: (token) => writeSse(res, 'delta', { text: token }),\n });\n }\n } else {\n aiResult = await callDeepSeekStream(modelMessages, {\n model: requestedModel,\n maxCompletionTokens,\n onToken: (token) => writeSse(res, 'delta', { text: token }),\n });\n }\n } else {\n if (!codexRuntime.enabled || isDeepSeekQuotaError(error)) throw error;\n logger.warn({ error: String(error) }, 'Codex SDK streaming chat failed, falling back to DeepSeek direct');\n provider = 'deepseek';\n responseModel = requestedModel;\n writeSse(res, 'meta', {\n conversationId,\n provider,\n model: responseModel,\n });\n aiResult = await callDeepSeekStream(modelMessages, {\n model: requestedModel,\n maxCompletionTokens,\n onToken: (token) => writeSse(res, 'delta', { text: token }),\n });\n }\n }\n\n const usage = {\n promptTokens: aiResult.usage.promptTokens || estimateTokens(JSON.stringify(modelMessages)),\n completionTokens: aiResult.usage.completionTokens || estimateTokens(aiResult.message),\n totalTokens:\n aiResult.usage.totalTokens ||\n estimateTokens(JSON.stringify(modelMessages)) + estimateTokens(aiResult.message),\n };\n if (!aiResult.usageAlreadyRecorded) recordUsage(user.userId, usage.totalTokens);\n\n const answeredAt = nowIso();\n statements.insertMessage.run({\n id: crypto.randomUUID(),\n conversationId,\n role: 'assistant',\n content: aiResult.message,\n promptTokens: usage.promptTokens,\n completionTokens: usage.completionTokens,\n totalTokens: usage.totalTokens,\n createdAt: answeredAt,\n });\n statements.updateConversation.run({\n id: conversationId,\n userId: user.userId,\n title: isNewConversation ? titleFromMessage(message) : conversation.title,\n updatedAt: answeredAt,\n });\n\n const latestUsage = usageFor(user.userId);\n const monthlyLimit = budget.limit;\n const action = determineClientAction(skillResult, message);\n writeSse(res, 'done', {\n conversationId,\n provider,\n model: responseModel,\n message: aiResult.message,\n usage: {\n ...usage,\n monthlyLimit,\n remainingTokens: Math.max(0, monthlyLimit - latestUsage.used),\n },\n ...(action ? { clientAction: action } : {}),\n });\n res.end();\n } catch (error) {\n logger.error({ error: error.stack || String(error) }, 'streaming chat failed');\n writeSse(res, 'error', {\n message: publicAiErrorMessage(error),\n details: error.details,\n });\n res.end();\n }\n});\n\napp.get(\n '/api/ai/conversations',\n asyncHandler(async (req, res) => {\n const user = await resolveUser(req, { username: req.query.username });\n const items = statements.listConversations.all(user.userId).map((item) => ({\n id: item.id,\n title: item.title,\n updatedAt: item.updatedAt,\n }));\n jsonResponse(res, 200, { success: true, items });\n }),\n);\n\napp.get(\n '/api/ai/conversations/:id',\n asyncHandler(async (req, res) => {\n const user = await resolveUser(req);\n const conversation = statements.getConversation.get(req.params.id, user.userId);\n if (!conversation) {\n return jsonResponse(res, 404, { success: false, message: 'conversation not found' });\n }\n const messages = statements.listMessages.all(req.params.id).map((item) => ({\n role: item.role,\n content: item.content,\n createdAt: item.createdAt,\n }));\n jsonResponse(res, 200, {\n success: true,\n conversation: {\n id: conversation.id,\n title: conversation.title,\n updatedAt: conversation.updated_at,\n },\n messages,\n });\n }),\n);\n\nfunction buildUrl(base, endpoint, params = {}) {\n const url = new URL(endpoint, `${base}/`);\n for (const [key, value] of Object.entries(params)) {\n if (Array.isArray(value)) {\n for (const item of value) {\n if (item !== undefined && item !== null && item !== '') {\n url.searchParams.append(key, String(item));\n }\n }\n } else if (value !== undefined && value !== null && value !== '') {\n url.searchParams.set(key, String(value));\n }\n }\n return url;\n}\n\nasync function fetchJson(url) {\n const response = await fetch(url, {\n headers: { Accept: 'application/json' },\n signal: AbortSignal.timeout(15_000),\n });\n if (!response.ok) {\n throw new Error(`Request failed: ${response.status}`);\n }\n return response.json();\n}\n\nfunction stripHtml(value) {\n return String(value || '')\n .replace(/<script[\\s\\S]*?<\\/script>/gi, ' ')\n .replace(/<style[\\s\\S]*?<\\/style>/gi, ' ')\n .replace(/<[^>]+>/g, ' ')\n .replace(/ /g, ' ')\n .replace(/</g, '<')\n .replace(/>/g, '>')\n .replace(/&/g, '&')\n .replace(/&#(\\d+);/g, (_match, code) => String.fromCharCode(Number(code)))\n .replace(/\\s+/g, ' ')\n .trim();\n}\n\nfunction cleanExtractedText(value) {\n let text = String(value || '')\n .replace(/\\b[A-Z]{1,3}\\d{2}n\\d{4,5}[a-z]?_p?\\d{4}[a-z]\\d{2}\\b/g, ' ')\n .replace(/\\uFFFD/g, '')\n .replace(/\\s+([,。;:!?、」』)》】])/g, '$1')\n .replace(/([「『《(【])\\s+/g, '$1');\n for (let i = 0; i < 3; i += 1) {\n text = text.replace(/([\\u3400-\\u9fff])\\s+([\\u3400-\\u9fff])/g, '$1$2');\n }\n return text.replace(/[ \\t]{2,}/g, ' ').trim();\n}\n\nfunction compactText(value, limit = 140) {\n const text = stripHtml(value);\n return text.length > limit ? `${text.slice(0, limit)}...` : text;\n}\n\nfunction collectSearchResults(data) {\n if (!data || typeof data !== 'object') return [];\n if (Array.isArray(data.results)) return data.results;\n if (Array.isArray(data?.results?.docs)) return data.results.docs;\n if (Array.isArray(data?.response?.docs)) return data.response.docs;\n if (Array.isArray(data.docs)) return data.docs;\n return [];\n}\n\nfunction firstText(item, keys) {\n for (const key of keys) {\n const value = item?.[key];\n if (Array.isArray(value)) {\n const first = value.map((entry) => safeUserText(String(entry))).find(Boolean);\n if (first) return first;\n }\n if (typeof value === 'string' && value.trim()) return value.trim();\n if (typeof value === 'number') return String(value);\n }\n return '';\n}\n\nfunction firstNumber(item, keys, fallback = undefined) {\n for (const key of keys) {\n const value = item?.[key];\n if (typeof value === 'number') return value;\n if (typeof value === 'string' && value.trim() && Number.isFinite(Number(value))) {\n return Number(value);\n }\n }\n return fallback;\n}\n\nasync function searchCbetaTitle(query, limit) {\n const url = buildUrl(cbetaApiRoot, 'search/title', { q: query, start: 0, rows: limit });\n const data = await fetchJson(url);\n return collectSearchResults(data).flatMap((item) => {\n const work = firstText(item, ['work', 'work_id', 'workId']);\n const title = firstText(item, ['content', 'title', 'work_title', 'workTitle']);\n if (!work || !title) return [];\n const juan = firstNumber(item, ['juan', 'juan_num', 'juanNum'], 1);\n return [\n {\n id: `cbeta-title-${work}-${juan}`,\n title,\n sourceName: 'CBETA 佛典',\n url: `cbeta:${work}:${juan}`,\n snippet: firstText(item, ['byline', 'creators', 'time_dynasty']) || '可下载经文正文并用于全球法布施。',\n resourceType: 'scripture',\n work,\n juan,\n },\n ];\n });\n}\n\nasync function searchCbetaContent(query, limit) {\n const endpoints = ['search/fulltext', 'search/content', 'search/all_in_one'];\n const settled = await Promise.allSettled(\n endpoints.map((endpoint) =>\n fetchJson(buildUrl(cbetaApiRoot, endpoint, { q: query, start: 0, rows: limit, field: 'content' })),\n ),\n );\n const results = [];\n for (const response of settled) {\n if (response.status !== 'fulfilled') continue;\n for (const item of collectSearchResults(response.value)) {\n const work = firstText(item, ['work', 'work_id', 'workId', 'sutra', 'id']);\n const snippet = compactText(firstText(item, ['kwic', 'highlight', 'snippet', 'content', 'text', 'body', 'p']));\n if (!work || !snippet) continue;\n const juan = firstNumber(item, ['juan', 'juan_num', 'juanNum', '卷'], 1);\n const title = firstText(item, ['title', 'work_title', 'workTitle', 'sutra_name', 'book', 'name']) || work;\n results.push({\n id: `cbeta-content-${work}-${juan}-${sha256(snippet).slice(0, 8)}`,\n title,\n sourceName: 'CBETA 佛典',\n url: `cbeta:${work}:${juan}`,\n snippet,\n resourceType: 'scripture',\n work,\n juan,\n });\n if (results.length >= limit) return results;\n }\n }\n return results;\n}\n\nfunction directUrlResource(query) {\n const url = safeUserText(query);\n let parsed;\n try {\n parsed = new URL(url);\n } catch {\n return null;\n }\n if (!['http:', 'https:'].includes(parsed.protocol)) return null;\n return {\n id: `url-${sha256(url).slice(0, 12)}`,\n title: parsed.hostname,\n sourceName: '网页资源',\n url,\n snippet: '直接下载并提取这个链接的可读正文。',\n resourceType: 'web',\n };\n}\n\nfunction looksLikeAudioQuery(query) {\n return /音乐|音樂|音频|音訊|mp3|audio|念诵|念誦|诵经|誦經|唱诵|唱誦|读诵|讀誦/.test(\n safeUserText(query).toLowerCase(),\n );\n}\n\nfunction archiveSearchTerm(query) {\n return (\n safeUserText(query)\n .replace(/下载|查找|寻找|找一?个|资源|音频|音訊|音乐|音樂|mp3|audio|念诵|念誦|诵经|誦經|唱诵|唱誦|读诵|讀誦/gi, ' ')\n .replace(/\\s+/g, ' ')\n .trim() || safeUserText(query)\n );\n}\n\nfunction solrPhrase(value) {\n return String(value).replace(/\\\\/g, '\\\\\\\\').replace(/\"/g, '\\\\\"');\n}\n\nasync function searchInternetArchiveAudio(query, limit) {\n if (!looksLikeAudioQuery(query)) return [];\n const term = archiveSearchTerm(query);\n if (!term) return [];\n const variants = Array.from(\n new Set([\n term,\n term.replaceAll('金刚经', '金剛經'),\n term.replaceAll('金剛經', '金刚经'),\n ].filter(Boolean)),\n );\n const phraseQuery = variants\n .flatMap((item) => [`title:\"${solrPhrase(item)}\"`, `description:\"${solrPhrase(item)}\"`])\n .join(' OR ');\n const url = buildUrl('https://archive.org', 'advancedsearch.php', {\n q: `(${phraseQuery}) AND mediatype:audio`,\n rows: Math.min(Math.max(limit, 1), 10),\n output: 'json',\n 'fl[]': ['identifier', 'title', 'description'],\n });\n const data = await fetchJson(url);\n const docs = Array.isArray(data?.response?.docs) ? data.response.docs : [];\n return docs\n .map((item) => {\n const identifier = firstText(item, ['identifier']);\n const title = firstText(item, ['title']) || identifier;\n if (!identifier || !title) return null;\n return {\n id: `ia-audio-${identifier}`,\n title,\n sourceName: 'Internet Archive 音频',\n url: `ia:${identifier}`,\n snippet: compactText(firstText(item, ['description'])) || '公开可访问的音频资源,将下载第一个可用 MP3 文件。',\n resourceType: 'audio',\n identifier,\n };\n })\n .filter(Boolean);\n}\n\nconst curatedCbetaWorks = [\n {\n work: 'T0235',\n juan: 1,\n title: '金剛般若波羅蜜經',\n keywords: ['金剛般若波羅蜜經', '金刚般若波罗蜜经', '金剛經', '金刚经'],\n snippet: '姚秦 鳩摩羅什譯。常用于读诵、共修与法布施的般若经典。',\n },\n {\n work: 'T0251',\n juan: 1,\n title: '般若波羅蜜多心經',\n keywords: ['般若波羅蜜多心經', '般若波罗蜜多心经', '心經', '心经'],\n snippet: '唐 玄奘譯。篇幅短,适合加入禅室功课本与日常持诵。',\n },\n {\n work: 'T0262',\n juan: 1,\n title: '妙法蓮華經',\n keywords: ['妙法蓮華經', '妙法莲华经', '法華經', '法华经'],\n snippet: '姚秦 鳩摩羅什譯。可按卷下载正文,用于长期功课与分享。',\n },\n {\n work: 'T0279',\n juan: 1,\n title: '大方廣佛華嚴經',\n keywords: ['大方廣佛華嚴經', '大方广佛华严经', '華嚴經', '华严经'],\n snippet: '唐 實叉難陀譯。可按卷下载正文,用于系统研读与法布施。',\n },\n {\n work: 'T0360',\n juan: 1,\n title: '佛說無量壽經',\n keywords: ['佛說無量壽經', '佛说无量寿经', '無量壽經', '无量寿经'],\n snippet: '净土经典。可下载正文,用于净土功课本与全球法布施。',\n },\n {\n work: 'T0365',\n juan: 1,\n title: '佛說觀無量壽佛經',\n keywords: ['佛說觀無量壽佛經', '佛说观无量寿佛经', '觀無量壽經', '观无量寿经'],\n snippet: '净土三经之一。适合查找下载后加入净土功课。',\n },\n {\n work: 'T0366',\n juan: 1,\n title: '佛說阿彌陀經',\n keywords: ['佛說阿彌陀經', '佛说阿弥陀经', '阿彌陀經', '阿弥陀经'],\n snippet: '姚秦 鳩摩羅什譯。篇幅适中,适合共修、持诵与分享。',\n },\n {\n work: 'T0475',\n juan: 1,\n title: '維摩詰所說經',\n keywords: ['維摩詰所說經', '维摩诘所说经', '維摩詰經', '维摩诘经'],\n snippet: '姚秦 鳩摩羅什譯。大乘经典,可按卷下载用于研读。',\n },\n {\n work: 'T0676',\n juan: 1,\n title: '解深密經',\n keywords: ['解深密經', '解深密经'],\n snippet: '唯识相关大乘经典。适合研读、整理笔记与法布施。',\n },\n {\n work: 'T1666',\n juan: 1,\n title: '大乘起信論',\n keywords: ['大乘起信論', '大乘起信论', '起信論', '起信论'],\n snippet: '大乘论典。适合下载后作为研习功课资料。',\n },\n];\n\nfunction localCbetaResults(query, limit) {\n const normalizedQuery = safeUserText(query).toLowerCase();\n if (!normalizedQuery) return [];\n return curatedCbetaWorks\n .filter((item) =>\n item.keywords.some((keyword) => {\n const normalizedKeyword = keyword.toLowerCase();\n return normalizedKeyword.includes(normalizedQuery) || normalizedQuery.includes(normalizedKeyword);\n }),\n )\n .slice(0, limit)\n .map((item) => ({\n id: `cbeta-curated-${item.work}-${item.juan}`,\n title: item.title,\n sourceName: 'CBETA 佛典',\n url: `cbeta:${item.work}:${item.juan}`,\n snippet: item.snippet,\n resourceType: 'scripture',\n work: item.work,\n juan: item.juan,\n }));\n}\n\nfunction uniqueResources(items, limit) {\n const seen = new Set();\n const results = [];\n for (const item of items) {\n const key = `${item.url}:${item.title}`;\n if (seen.has(key)) continue;\n seen.add(key);\n results.push(item);\n if (results.length >= limit) break;\n }\n return results;\n}\n\nasync function findResourceCandidates(query, limit) {\n const direct = directUrlResource(query);\n const results = [];\n if (direct) results.push(direct);\n try {\n results.push(...(await searchInternetArchiveAudio(query, limit)));\n } catch (error) {\n logger.warn({ error: String(error), query }, 'Internet Archive audio search skipped');\n }\n results.push(...localCbetaResults(query, limit));\n\n const [titleResults, contentResults] = await Promise.allSettled([\n searchCbetaTitle(query, limit),\n searchCbetaContent(query, limit),\n ]);\n if (titleResults.status === 'fulfilled') results.push(...titleResults.value);\n if (contentResults.status === 'fulfilled') results.push(...contentResults.value);\n\n return uniqueResources(results, limit);\n}\n\nfunction looksLikeResourceTask(message) {\n const text = safeUserText(message).toLowerCase();\n if (!text) return false;\n return /下载|查找|寻找|找一?个|资源|經文|经文|佛经|佛經|仪轨|儀軌|音乐|音樂|mp3|audio|念诵|念誦|诵经|誦經/.test(text);\n}\n\nfunction extractResourceQuery(message) {\n const text = safeUserText(message)\n .replace(/请|請|帮我|幫我|麻烦|麻煩|可以|能不能|需要|我要|想要/g, ' ')\n .replace(/下载|下載|查找|寻找|尋找|找一个|找一個|找|资源|資源|并|並|然后|然後|用于|用於|全球法布施|加入功课本|加入功課本/g, ' ')\n .replace(/\\s+/g, ' ')\n .trim();\n return text || safeUserText(message);\n}\n\nfunction resourceContextMessage(skillResult) {\n if (!skillResult) return '';\n const lines = [\n '本轮资源检索/下载已经由大乘 App 后端完成。',\n '不要再次调用 search_dharma_resources、download_dharma_resource 或其他资源 MCP 工具;请直接基于以下结果回答用户。',\n `后端已调用 skill: ${skillResult.skillName}`,\n `执行目标: ${skillResult.query}`,\n ];\n if (skillResult.selected) {\n lines.push(`候选资源: ${skillResult.selected.title}`);\n lines.push(`来源: ${skillResult.selected.sourceName}`);\n lines.push(`资源地址: ${skillResult.selected.url}`);\n }\n if (skillResult.downloaded) {\n lines.push(`下载标题: ${skillResult.downloaded.title}`);\n lines.push(`下载文件: ${skillResult.downloaded.fileName}`);\n const text = safeUserText(skillResult.downloaded.contentText);\n if (text) {\n lines.push('下载正文摘录:');\n lines.push(text.slice(0, 8000));\n }\n }\n return lines.join('\\n');\n}\n\nfunction determineClientAction(skillResult, message) {\n if (skillResult && skillResult.downloaded) {\n const text = safeUserText(message);\n if (/法布施|全球法布施|分享/.test(text)) {\n return {\n type: 'prepare_dharma_share_text',\n title: skillResult.downloaded.title,\n text: skillResult.downloaded.contentText || '',\n };\n }\n }\n return null;\n}\n\nasync function runResourceFinderSkill({ message, user, onStep }) {\n if (!looksLikeResourceTask(message)) return null;\n\n const skillName = 'resource-finder-downloader';\n const query = extractResourceQuery(message);\n const audioTask = looksLikeAudioQuery(message) || looksLikeAudioQuery(query);\n onStep?.({\n type: 'skill.step',\n skillName,\n stage: 'start',\n title: '调用资源查找下载 skill',\n message: `识别到资源任务: ${query}`,\n });\n\n onStep?.({\n type: 'skill.step',\n skillName,\n stage: 'search',\n title: '搜索可分享资源',\n message: audioTask\n ? '优先检索公开可访问音频资源,失败时再回落到佛典正文资源。'\n : '优先检索 CBETA 佛典正文接口,搜索索引不可用时使用内置经典映射。',\n });\n const candidates = await findResourceCandidates(query, 8);\n if (candidates.length === 0) {\n onStep?.({\n type: 'skill.step',\n skillName,\n stage: 'empty',\n title: '没有找到可下载资源',\n message: '后端没有找到明确可下载且可分享的资源。',\n });\n return {\n skillName,\n query,\n candidates: [],\n selected: null,\n downloaded: null,\n };\n }\n\n const selected = candidates[0];\n onStep?.({\n type: 'skill.step',\n skillName,\n stage: 'select',\n title: '选择可信候选资源',\n message: `${selected.title} / ${selected.sourceName}`,\n });\n\n onStep?.({\n type: 'skill.step',\n skillName,\n stage: 'download',\n title: '下载并提取资源',\n message: selected.url,\n });\n const cbeta = parseCbetaResource(selected);\n const internetArchive = parseInternetArchiveResource(selected);\n const downloaded = cbeta\n ? await downloadCbetaResource({ ...selected, ...cbeta })\n : internetArchive\n ? await downloadInternetArchiveAudio({ ...selected, ...internetArchive })\n : await downloadWebResource(selected);\n const persisted = persistDownloadedResource(user, downloaded);\n\n onStep?.({\n type: 'skill.step',\n skillName,\n stage: 'done',\n title: '资源已准备完成',\n message: downloaded.fileName,\n });\n\n return {\n skillName,\n query,\n candidates,\n selected,\n downloaded: {\n ...downloaded,\n downloadId: persisted.id,\n },\n };\n}\n\napp.post(\n '/api/resources/search',\n asyncHandler(async (req, res) => {\n const query = safeUserText(req.body?.query);\n const limit = Math.min(Math.max(Number(req.body?.limit || 12), 1), 20);\n if (query.length < 2) {\n return jsonResponse(res, 400, { success: false, message: 'query is required' });\n }\n\n const items = await findResourceCandidates(query, limit);\n\n jsonResponse(res, 200, {\n success: true,\n source: 'resource-index',\n items,\n });\n }),\n);\n\nfunction parseCbetaResource(input) {\n const work = safeUserText(input.work);\n const juan = Number(input.juan || 1);\n if (work) return { work, juan: Number.isFinite(juan) ? juan : 1 };\n\n const url = safeUserText(input.url);\n const match = /^cbeta:([^:]+):(\\d+)$/i.exec(url);\n if (!match) return null;\n return {\n work: match[1].toUpperCase(),\n juan: Number(match[2] || 1),\n };\n}\n\nfunction parseInternetArchiveResource(input) {\n const url = safeUserText(input.url);\n const match = /^ia:([^:]+)$/i.exec(url);\n const identifier = match?.[1] || safeUserText(input.identifier);\n if (!identifier) return null;\n return { identifier };\n}\n\nfunction safeFileName(value) {\n const normalized = String(value || 'dharma-resource')\n .replace(/[\\\\/:*?\"<>|]+/g, '-')\n .replace(/\\s+/g, '-')\n .replace(/-+/g, '-')\n .replace(/^-|-$/g, '')\n .slice(0, 80);\n return normalized || 'dharma-resource';\n}\n\nfunction extractCbetaHtml(data) {\n const first = Array.isArray(data?.results) ? data.results[0] : null;\n if (typeof first === 'string') return first;\n if (first && typeof first === 'object' && typeof first.html === 'string') return first.html;\n if (typeof data?.html === 'string') return data.html;\n return '';\n}\n\nasync function downloadCbetaResource(resource) {\n const url = buildUrl(cbetaApiRoot, 'juans', {\n work: resource.work,\n juan: resource.juan,\n work_info: 1,\n toc: 1,\n });\n const data = await fetchJson(url);\n const workInfo = data.work_info || {};\n const title = safeUserText(workInfo.title) || safeUserText(resource.title) || resource.work;\n const html = extractCbetaHtml(data);\n const text = cleanExtractedText(stripHtml(html));\n if (!text) throw new Error('CBETA resource returned empty content');\n return {\n title,\n sourceName: 'CBETA 佛典',\n url: `cbeta:${resource.work}:${resource.juan}`,\n contentText: text.slice(0, maxResourceTextChars),\n fileName: `${resource.work}_${resource.juan}_${safeFileName(title)}.txt`,\n };\n}\n\nasync function downloadInternetArchiveAudio(resource) {\n const identifier = safeUserText(resource.identifier);\n if (!identifier) throw new Error('Internet Archive identifier is required');\n const metadata = await fetchJson(`https://archive.org/metadata/${encodeURIComponent(identifier)}`);\n const title = safeUserText(metadata?.metadata?.title) || safeUserText(resource.title) || identifier;\n const files = Array.isArray(metadata?.files) ? metadata.files : [];\n const audioFile = files.find((file) => {\n const name = safeUserText(file?.name);\n const format = safeUserText(file?.format).toLowerCase();\n return /\\.(mp3|m4a|ogg)$/i.test(name) || format.includes('mp3');\n });\n if (!audioFile?.name) {\n throw new Error('Internet Archive audio item has no downloadable audio file');\n }\n const sourceFileName = audioFile.name;\n const fileUrl = `https://archive.org/download/${encodeURIComponent(identifier)}/${sourceFileName\n .split('/')\n .map((part) => encodeURIComponent(part))\n .join('/')}`;\n const response = await fetch(fileUrl, {\n headers: {\n Accept: 'audio/mpeg, audio/*;q=0.9, */*;q=0.5',\n 'User-Agent': 'DachengResourceDownloader/1.0 (+https://ombhrum.com)',\n },\n signal: AbortSignal.timeout(60_000),\n });\n if (!response.ok) {\n throw new Error(`Internet Archive audio download failed: ${response.status}`);\n }\n const binaryContent = Buffer.from(await response.arrayBuffer());\n if (!binaryContent.length) throw new Error('Internet Archive audio returned empty content');\n const sizeMb = (binaryContent.length / 1024 / 1024).toFixed(1);\n return {\n title,\n sourceName: 'Internet Archive 音频',\n url: fileUrl,\n contentText: `已下载音频资源: ${title}\\n文件名: ${sourceFileName}\\n大小: ${sizeMb} MB\\n来源: ${fileUrl}`,\n binaryContent,\n fileName: safeFileName(sourceFileName),\n };\n}\n\nasync function downloadWebResource(input) {\n const url = safeUserText(input.url);\n const parsed = new URL(url);\n if (!['http:', 'https:'].includes(parsed.protocol)) {\n throw new Error('Only http/https resources can be downloaded');\n }\n\n const response = await fetch(parsed, {\n headers: {\n Accept: 'text/html, text/plain, application/xhtml+xml, application/xml;q=0.8, */*;q=0.5',\n 'User-Agent': 'DachengResourceDownloader/1.0 (+https://ombhrum.com)',\n },\n signal: AbortSignal.timeout(25_000),\n });\n if (!response.ok) throw new Error(`Resource download failed: ${response.status}`);\n const contentType = response.headers.get('content-type') || '';\n const body = await response.text();\n const title = safeUserText(input.title) || parsed.hostname;\n const contentText = contentType.includes('html') ? cleanExtractedText(stripHtml(body)) : body.trim();\n if (!contentText) throw new Error('Resource returned empty content');\n return {\n title,\n sourceName: safeUserText(input.sourceName) || parsed.hostname,\n url,\n contentText: contentText.slice(0, maxResourceTextChars),\n fileName: `${safeFileName(title)}.txt`,\n };\n}\n\nfunction persistDownloadedResource(user, content) {\n const id = crypto.randomUUID();\n const fileName = `${id}-${safeFileName(content.fileName)}`;\n const filePath = path.join(resourcesDir, fileName);\n if (content.binaryContent) {\n fs.writeFileSync(filePath, content.binaryContent);\n } else {\n fs.writeFileSync(filePath, content.contentText, 'utf8');\n }\n statements.insertResourceDownload.run({\n id,\n userId: user.userId,\n title: content.title,\n sourceName: content.sourceName,\n sourceUrl: content.url,\n fileName: content.fileName,\n filePath,\n createdAt: nowIso(),\n });\n return { id, fileName, filePath };\n}\n\nfunction createLibreChatMcpServer() {\n const server = new McpServer({\n name: 'dacheng-ai-tools',\n version: '0.1.0',\n });\n\n server.registerTool(\n 'search_dharma_resources',\n {\n title: 'Search dharma resources',\n description:\n 'Search for legally shareable Buddhist scripture, text, web, or audio resources that can be prepared for Fabushi sharing.',\n inputSchema: {\n query: z.string().min(2).max(200),\n limit: z.number().int().min(1).max(12).optional(),\n },\n outputSchema: {\n query: z.string(),\n items: z.array(\n z.object({\n id: z.string(),\n title: z.string(),\n sourceName: z.string(),\n url: z.string(),\n snippet: z.string(),\n resourceType: z.string(),\n work: z.string().optional(),\n juan: z.number().optional(),\n }),\n ),\n },\n annotations: {\n readOnlyHint: true,\n destructiveHint: false,\n openWorldHint: true,\n },\n },\n async ({ query, limit }) => {\n const items = await findResourceCandidates(query, limit ?? 8);\n return textToolResult(`Found ${items.length} candidate resources for \"${query}\".`, {\n query,\n items,\n });\n },\n );\n\n server.registerTool(\n 'download_dharma_resource',\n {\n title: 'Download dharma resource',\n description:\n 'Download and extract a selected resource. Use the url/work/juan returned by search_dharma_resources whenever possible.',\n inputSchema: {\n url: z.string().min(1).max(1000),\n title: z.string().max(300).optional(),\n sourceName: z.string().max(120).optional(),\n work: z.string().max(40).optional(),\n juan: z.number().int().min(1).optional(),\n identifier: z.string().max(300).optional(),\n },\n outputSchema: {\n title: z.string(),\n sourceName: z.string(),\n url: z.string(),\n fileName: z.string(),\n downloadId: z.string(),\n contentText: z.string(),\n },\n annotations: {\n readOnlyHint: false,\n destructiveHint: false,\n openWorldHint: true,\n },\n },\n async (input) => {\n const user = { userId: 'librechat:mcp' };\n const cbeta = parseCbetaResource(input);\n const internetArchive = parseInternetArchiveResource(input);\n const content = cbeta\n ? await downloadCbetaResource({ ...input, ...cbeta })\n : internetArchive\n ? await downloadInternetArchiveAudio({ ...input, ...internetArchive })\n : await downloadWebResource(input);\n const persisted = persistDownloadedResource(user, content);\n const { binaryContent: _binaryContent, ...publicContent } = content;\n return textToolResult(`Downloaded ${publicContent.title} as ${publicContent.fileName}.`, {\n ...publicContent,\n downloadId: persisted.id,\n });\n },\n );\n\n server.registerTool(\n 'prepare_dharma_share_text',\n {\n title: 'Prepare dharma sharing text',\n description:\n 'Prepare a text payload for the Fabushi app to confirm and add to global dharma sharing. This returns a clientAction; it does not start sending.',\n inputSchema: {\n title: z.string().min(1).max(120),\n text: z.string().min(1).max(20000),\n },\n outputSchema: {\n clientAction: z.object({\n type: z.literal('prepare_dharma_share_text'),\n title: z.string(),\n text: z.string(),\n }),\n },\n annotations: {\n readOnlyHint: false,\n destructiveHint: false,\n openWorldHint: false,\n },\n },\n async ({ title, text }) =>\n textToolResult(`Prepared \"${title}\" for user confirmation in the Fabushi app.`, {\n clientAction: {\n type: 'prepare_dharma_share_text',\n title: safeUserText(title).slice(0, 120),\n text: String(text || '').trim().slice(0, 20000),\n },\n }),\n );\n\n server.registerTool(\n 'prepare_practice_book_item',\n {\n title: 'Prepare practice book item',\n description:\n 'Prepare a scripture, chant, or practice item for the Fabushi app practice book. This returns a clientAction; it does not mutate the app by itself.',\n inputSchema: {\n title: z.string().min(1).max(120),\n sourceName: z.string().max(120).optional(),\n text: z.string().min(1).max(20000),\n },\n outputSchema: {\n clientAction: z.object({\n type: z.literal('prepare_practice_book_item'),\n title: z.string(),\n sourceName: z.string(),\n text: z.string(),\n }),\n },\n annotations: {\n readOnlyHint: false,\n destructiveHint: false,\n openWorldHint: false,\n },\n },\n async ({ title, sourceName, text }) =>\n textToolResult(`Prepared \"${title}\" for the Fabushi practice book confirmation flow.`, {\n clientAction: {\n type: 'prepare_practice_book_item',\n title: safeUserText(title).slice(0, 120),\n sourceName: safeUserText(sourceName || '大乘 AI').slice(0, 120),\n text: String(text || '').trim().slice(0, 20000),\n },\n }),\n );\n\n return server;\n}\n\nasync function handleMcpRequest(req, res) {\n const server = createLibreChatMcpServer();\n const transport = new StreamableHTTPServerTransport({\n sessionIdGenerator: undefined,\n enableJsonResponse: true,\n });\n\n res.on('close', () => {\n transport.close();\n server.close();\n });\n\n await server.connect(transport);\n await transport.handleRequest(req, res, req.body);\n}\n\napp.all('/mcp', async (req, res) => {\n try {\n await handleMcpRequest(req, res);\n } catch (error) {\n logger.error({ error: error.stack || String(error) }, 'MCP request failed');\n if (!res.headersSent) {\n res.status(500).json({\n error: 'MCP request failed',\n message: publicAiErrorMessage(error),\n });\n }\n }\n});\n\napp.post(\n '/api/resources/download',\n asyncHandler(async (req, res) => {\n const user = await resolveUser(req, req.body || {});\n const cbeta = parseCbetaResource(req.body || {});\n const internetArchive = parseInternetArchiveResource(req.body || {});\n const content = cbeta\n ? await downloadCbetaResource({ ...req.body, ...cbeta })\n : internetArchive\n ? await downloadInternetArchiveAudio({ ...req.body, ...internetArchive })\n : await downloadWebResource(req.body || {});\n const persisted = persistDownloadedResource(user, content);\n const { binaryContent: _binaryContent, ...publicContent } = content;\n jsonResponse(res, 200, {\n success: true,\n downloadId: persisted.id,\n ...publicContent,\n });\n }),\n);\n\napp.post(\n '/api/codex/resource-task',\n asyncHandler(async (req, res) => {\n if (process.env.ENABLE_CODEX_SDK !== 'true') {\n return jsonResponse(res, 403, {\n success: false,\n message: 'Codex SDK task runner is disabled. Set ENABLE_CODEX_SDK=true on the server to enable it.',\n });\n }\n const prompt = safeUserText(req.body?.prompt);\n if (!prompt) {\n return jsonResponse(res, 400, { success: false, message: 'prompt is required' });\n }\n const user = await resolveUser(req, req.body || {});\n const { Codex } = await import('@openai/codex-sdk');\n const codexRuntime = createCodexDeepSeekRuntime(user);\n const codex = new Codex(codexRuntime.enabled ? codexRuntime.options : {});\n const thread = codex.startThread(\n codexRuntime.enabled\n ? codexRuntime.threadOptions\n : { skipGitRepoCheck: true, networkAccessEnabled: true },\n );\n const result = await thread.run(prompt);\n const usage = {\n promptTokens: Number(result.usage?.input_tokens || 0),\n completionTokens: Number(result.usage?.output_tokens || 0),\n totalTokens: Number((result.usage?.input_tokens || 0) + (result.usage?.output_tokens || 0)),\n };\n if (!codexRuntime.enabled) {\n recordUsage(user.userId, usage.totalTokens || estimateTokens(JSON.stringify(result)));\n }\n jsonResponse(res, 200, {\n success: true,\n threadId: thread.id,\n provider: codexRuntime.provider,\n model: deepseekModel,\n usage,\n result,\n });\n }),\n);\n\nregisterPlatformApi({ app, db, resolveUser, asyncHandler });\n\napp.use((error, _req, res, _next) => {\n const status = Number(error.statusCode || error.status || 500);\n const safeStatus = status >= 400 && status < 600 ? status : 500;\n logger.error({ error: error.stack || String(error) }, 'request failed');\n jsonResponse(res, safeStatus, {\n success: false,\n message: error.message || 'Internal Server Error',\n details: error.details,\n });\n});\n\nconst port = Number(process.env.PORT || 8788);\napp.listen(port, '0.0.0.0', () => {\n logger.info(\n {\n port,\n dbPath,\n dataDir,\n deepseekModel,\n cbetaApiRoot,\n },\n 'Dacheng AI backend started',\n );\n});\n"}, "80a536174cf6d18539625f45f609e1f9c2ba15a7": {"t": 1785757964.3159351, "n": 146133, "h": "aa23f207a60ca98a", "c": "import crypto from 'node:crypto';\nimport fs from 'node:fs';\nimport path from 'node:path';\nimport process from 'node:process';\nimport { fileURLToPath } from 'node:url';\n\nimport { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';\nimport { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';\nimport Database from 'better-sqlite3';\nimport cors from 'cors';\nimport dotenv from 'dotenv';\nimport express from 'express';\nimport rateLimit from 'express-rate-limit';\nimport helmet from 'helmet';\nimport pino from 'pino';\nimport { z } from 'zod';\n\nimport { registerPlatformApi } from './platform_api.js';\n\ndotenv.config();\n\nconst __dirname = path.dirname(fileURLToPath(import.meta.url));\nconst rootDir = path.resolve(__dirname, '..');\nconst dataDir = process.env.DATA_DIR || path.join(rootDir, 'data');\nconst resourcesDir = path.join(dataDir, 'resources');\nconst miniAppsDir = path.join(dataDir, 'miniapps');\nconst miniAppsStorePath = path.join(miniAppsDir, 'sandbox-miniapps.json');\nconst openClawRuntimeUpdatesDir =\n process.env.OPENCLAW_RUNTIME_UPDATES_DIR || path.join(dataDir, 'openclaw-runtime');\nconst openClawRuntimeEnableMacosUpdates =\n process.env.OPENCLAW_RUNTIME_ENABLE_MACOS_UPDATES === 'true';\nconst codexHomeDir = process.env.CODEX_HOME || path.join(dataDir, 'codex-home');\nconst codexTempDir = process.env.CODEX_TMPDIR || path.join(dataDir, 'codex-tmp');\nconst codexRuntimeDir = process.env.XDG_RUNTIME_DIR || path.join(dataDir, 'codex-runtime');\nconst dbPath = process.env.SQLITE_PATH || path.join(dataDir, 'dacheng-ai.sqlite');\n\nfs.mkdirSync(resourcesDir, { recursive: true });\nfs.mkdirSync(miniAppsDir, { recursive: true });\nfs.mkdirSync(openClawRuntimeUpdatesDir, { recursive: true });\nfs.mkdirSync(codexHomeDir, { recursive: true });\nfs.mkdirSync(codexTempDir, { recursive: true });\nfs.mkdirSync(codexRuntimeDir, { recursive: true });\n\nconst logger = pino({\n level: process.env.LOG_LEVEL || 'info',\n});\n\nconst app = express();\napp.disable('x-powered-by');\napp.set('trust proxy', Number(process.env.TRUST_PROXY_HOPS || 1));\napp.use(helmet());\nconst corsOrigin =\n !process.env.CORS_ORIGIN || process.env.CORS_ORIGIN.trim() === '*'\n ? true\n : process.env.CORS_ORIGIN.split(',').map((item) => item.trim()).filter(Boolean);\napp.use(cors({ origin: corsOrigin }));\napp.use(express.json({ limit: '1mb' }));\napp.use(\n rateLimit({\n windowMs: 60_000,\n limit: Number(process.env.RATE_LIMIT_PER_MINUTE || 90),\n standardHeaders: true,\n legacyHeaders: false,\n }),\n);\n\nconst db = new Database(dbPath);\ndb.pragma('journal_mode = WAL');\ndb.pragma('foreign_keys = ON');\n\ndb.exec(`\n CREATE TABLE IF NOT EXISTS conversations (\n id TEXT PRIMARY KEY,\n user_id TEXT NOT NULL,\n title TEXT NOT NULL,\n provider TEXT NOT NULL,\n model TEXT NOT NULL,\n created_at TEXT NOT NULL,\n updated_at TEXT NOT NULL\n );\n\n CREATE INDEX IF NOT EXISTS idx_conversations_user_updated\n ON conversations (user_id, updated_at DESC);\n\n CREATE TABLE IF NOT EXISTS messages (\n id TEXT PRIMARY KEY,\n conversation_id TEXT NOT NULL,\n role TEXT NOT NULL,\n content TEXT NOT NULL,\n prompt_tokens INTEGER NOT NULL DEFAULT 0,\n completion_tokens INTEGER NOT NULL DEFAULT 0,\n total_tokens INTEGER NOT NULL DEFAULT 0,\n created_at TEXT NOT NULL,\n FOREIGN KEY (conversation_id) REFERENCES conversations(id) ON DELETE CASCADE\n );\n\n CREATE INDEX IF NOT EXISTS idx_messages_conversation_created\n ON messages (conversation_id, created_at ASC);\n\n CREATE TABLE IF NOT EXISTS usage_months (\n user_id TEXT NOT NULL,\n month TEXT NOT NULL,\n total_tokens INTEGER NOT NULL DEFAULT 0,\n updated_at TEXT NOT NULL,\n PRIMARY KEY (user_id, month)\n );\n\n CREATE TABLE IF NOT EXISTS resource_downloads (\n id TEXT PRIMARY KEY,\n user_id TEXT NOT NULL,\n title TEXT NOT NULL,\n source_name TEXT NOT NULL,\n source_url TEXT NOT NULL,\n file_name TEXT NOT NULL,\n file_path TEXT NOT NULL,\n created_at TEXT NOT NULL\n );\n\n CREATE TABLE IF NOT EXISTS agent_runs (\n id TEXT PRIMARY KEY,\n user_id TEXT NOT NULL,\n conversation_id TEXT NOT NULL,\n message_id TEXT NOT NULL,\n openclaw_run_id TEXT,\n openclaw_session_key TEXT NOT NULL,\n status TEXT NOT NULL,\n mode TEXT NOT NULL,\n provider TEXT NOT NULL,\n model TEXT,\n input_tokens INTEGER NOT NULL DEFAULT 0,\n output_tokens INTEGER NOT NULL DEFAULT 0,\n tool_call_count INTEGER NOT NULL DEFAULT 0,\n cost_microusd INTEGER NOT NULL DEFAULT 0,\n started_at TEXT NOT NULL,\n completed_at TEXT,\n failed_at TEXT,\n error_code TEXT,\n error_message TEXT\n );\n\n CREATE INDEX IF NOT EXISTS idx_agent_runs_user_started\n ON agent_runs (user_id, started_at DESC);\n\n CREATE INDEX IF NOT EXISTS idx_agent_runs_status_started\n ON agent_runs (status, started_at DESC);\n\n CREATE TABLE IF NOT EXISTS agent_message_feedback (\n id TEXT PRIMARY KEY,\n message_id TEXT NOT NULL,\n user_id TEXT NOT NULL,\n rating TEXT NOT NULL,\n reason TEXT,\n comment TEXT,\n created_at TEXT NOT NULL\n );\n`);\n\nconst statements = {\n insertConversation: db.prepare(`\n INSERT INTO conversations (id, user_id, title, provider, model, created_at, updated_at)\n VALUES (@id, @userId, @title, @provider, @model, @createdAt, @updatedAt)\n `),\n updateConversation: db.prepare(`\n UPDATE conversations\n SET title = @title, updated_at = @updatedAt\n WHERE id = @id AND user_id = @userId\n `),\n getConversation: db.prepare(`\n SELECT * FROM conversations WHERE id = ? AND user_id = ? LIMIT 1\n `),\n listConversations: db.prepare(`\n SELECT id, title, updated_at AS updatedAt\n FROM conversations\n WHERE user_id = ?\n ORDER BY updated_at DESC\n LIMIT 30\n `),\n insertMessage: db.prepare(`\n INSERT INTO messages (\n id, conversation_id, role, content,\n prompt_tokens, completion_tokens, total_tokens, created_at\n )\n VALUES (\n @id, @conversationId, @role, @content,\n @promptTokens, @completionTokens, @totalTokens, @createdAt\n )\n `),\n listMessages: db.prepare(`\n SELECT role, content, prompt_tokens AS promptTokens,\n completion_tokens AS completionTokens, total_tokens AS totalTokens,\n created_at AS createdAt\n FROM messages\n WHERE conversation_id = ?\n ORDER BY created_at ASC\n `),\n getUsage: db.prepare(`\n SELECT total_tokens AS totalTokens FROM usage_months WHERE user_id = ? AND month = ?\n `),\n upsertUsage: db.prepare(`\n INSERT INTO usage_months (user_id, month, total_tokens, updated_at)\n VALUES (@userId, @month, @totalTokens, @updatedAt)\n ON CONFLICT(user_id, month)\n DO UPDATE SET total_tokens = usage_months.total_tokens + excluded.total_tokens,\n updated_at = excluded.updated_at\n `),\n insertResourceDownload: db.prepare(`\n INSERT INTO resource_downloads (\n id, user_id, title, source_name, source_url, file_name, file_path, created_at\n )\n VALUES (\n @id, @userId, @title, @sourceName, @sourceUrl, @fileName, @filePath, @createdAt\n )\n `),\n insertAgentRun: db.prepare(`\n INSERT INTO agent_runs (\n id, user_id, conversation_id, message_id, openclaw_run_id, openclaw_session_key,\n status, mode, provider, model, started_at\n )\n VALUES (\n @id, @userId, @conversationId, @messageId, @openClawRunId, @openClawSessionKey,\n @status, @mode, @provider, @model, @startedAt\n )\n `),\n getAgentRun: db.prepare(`\n SELECT * FROM agent_runs WHERE id = ? AND user_id = ? LIMIT 1\n `),\n updateAgentRunStatus: db.prepare(`\n UPDATE agent_runs\n SET status = @status,\n openclaw_run_id = COALESCE(@openClawRunId, openclaw_run_id),\n input_tokens = COALESCE(@inputTokens, input_tokens),\n output_tokens = COALESCE(@outputTokens, output_tokens),\n tool_call_count = COALESCE(@toolCallCount, tool_call_count),\n completed_at = @completedAt,\n failed_at = @failedAt,\n error_code = @errorCode,\n error_message = @errorMessage\n WHERE id = @id AND user_id = @userId\n `),\n insertAgentFeedback: db.prepare(`\n INSERT INTO agent_message_feedback (id, message_id, user_id, rating, reason, comment, created_at)\n VALUES (@id, @messageId, @userId, @rating, @reason, @comment, @createdAt)\n `),\n};\n\nconst deepseekApiKey = process.env.DEEPSEEK_API_KEY || '';\nconst deepseekBaseUrl = (process.env.DEEPSEEK_BASE_URL || 'https://api.deepseek.com').replace(/\\/+$/, '');\nconst deepseekModel = process.env.DEEPSEEK_MODEL || 'deepseek-chat';\nconst deepseekAvailableModels = Array.from(\n new Set(\n (process.env.DEEPSEEK_MODELS || `${deepseekModel},deepseek-chat,deepseek-reasoner`)\n .split(',')\n .map((item) => normalizeDeepSeekModelName(item))\n .filter(Boolean),\n ),\n);\nconst cbetaApiRoot = (process.env.CBETA_API_ROOT || 'https://api.ombhrum.com/api/cbeta').replace(/\\/+$/, '');\nconst fabushiApiBaseUrl = (process.env.FABUSHI_API_BASE_URL || 'https://api.ombhrum.com').replace(/\\/+$/, '');\nconst memberMonthlyLimit = Number(process.env.MEMBER_MONTHLY_TOKEN_LIMIT || 1_000_000);\nconst freeMonthlyLimit = Number(process.env.FREE_MONTHLY_TOKEN_LIMIT || 1_000);\nconst requireMemberForAi = process.env.REQUIRE_MEMBER_FOR_AI === 'true';\nconst deepseekMaxCompletionTokens = Math.max(1, Number(process.env.DEEPSEEK_MAX_COMPLETION_TOKENS || 1400));\nconst maxResourceTextChars = Number(process.env.MAX_RESOURCE_TEXT_CHARS || 80_000);\nconst openClawRuntimeManifestPath =\n process.env.OPENCLAW_RUNTIME_MANIFEST_PATH || path.join(openClawRuntimeUpdatesDir, 'manifest.json');\nconst openClawRuntimePublicBaseUrl = (process.env.OPENCLAW_RUNTIME_PUBLIC_BASE_URL || '').replace(/\\/+$/, '');\nconst enableLibreChatAgentChat = process.env.ENABLE_LIBRECHAT_AGENT_CHAT === 'true';\nconst libreChatAgentsBaseUrl = (\n process.env.LIBRECHAT_AGENTS_BASE_URL || 'http://127.0.0.1:3080/api/agents/v1'\n).replace(/\\/+$/, '');\nconst libreChatAgentApiKey = process.env.LIBRECHAT_AGENT_API_KEY || '';\nconst libreChatAgentId = process.env.LIBRECHAT_AGENT_ID || '';\nconst enableCodexSdkChat = process.env.ENABLE_CODEX_SDK_CHAT === 'true';\nconst enableOpenClawAgentChat = process.env.ENABLE_OPENCLAW_AGENT_CHAT === 'true';\nconst openClawGatewayUrl = (process.env.OPENCLAW_GATEWAY_URL || 'http://127.0.0.1:18789').replace(/\\/+$/, '');\nconst openClawGatewayToken = process.env.OPENCLAW_GATEWAY_TOKEN || '';\nconst openClawAgentId = process.env.OPENCLAW_AGENT_ID || 'fabushi-public-agent';\nconst openClawRunsEndpoint = process.env.OPENCLAW_RUNS_ENDPOINT || '';\nconst codexDeepSeekProviderId = 'deepseek-chat-completions';\nconst codexResponsesBaseUrl = (\n process.env.CODEX_DEEPSEEK_RESPONSES_BASE_URL ||\n `http://127.0.0.1:${process.env.PORT || 8788}/codex-deepseek/v1`\n).replace(/\\/+$/, '');\n\nconst systemPrompt = [\n '你是“大乘”App 的 AI 助手。',\n '你的核心任务是帮助用户查找、整理、理解并全球法布施合法可分享的佛法资源。',\n '回答要庄重、简洁、可执行;涉及经典、仪轨或资源时,提醒用户尊重版权、来源和当地法规。',\n '如果用户想找资源、下载经文、寻找音频或准备可分享资料,你必须先在后端自动执行搜索、验证和下载步骤;不要要求用户去前端 + 菜单手动查找。',\n '你可以把执行进度总结给用户,但不要输出隐藏推理原文。',\n].join('\\n');\n\nfunction nowIso() {\n return new Date().toISOString();\n}\n\nfunction monthKey(date = new Date()) {\n return `${date.getUTCFullYear()}-${String(date.getUTCMonth() + 1).padStart(2, '0')}`;\n}\n\nfunction jsonResponse(res, status, payload) {\n res.status(status).json(payload);\n}\n\nasync function readJsonFile(filePath) {\n const raw = await fs.promises.readFile(filePath, 'utf8');\n return JSON.parse(raw);\n}\n\nfunction requestOrigin(req) {\n if (openClawRuntimePublicBaseUrl) return openClawRuntimePublicBaseUrl;\n const proto = req.get('X-Forwarded-Proto') || req.protocol || 'https';\n const host = req.get('X-Forwarded-Host') || req.get('Host');\n return host ? `${proto}://${host}` : '';\n}\n\nfunction absolutizeOpenClawRuntimeUrl(req, value) {\n const text = readText(value);\n if (!text) return '';\n if (/^https?:\\/\\//i.test(text)) return text;\n const normalized = text.startsWith('/')\n ? text\n : `/api/openclaw/runtime/files/${text.replace(/^\\/+/, '')}`;\n const origin = requestOrigin(req);\n return origin ? new URL(normalized, `${origin}/`).toString() : normalized;\n}\n\nfunction textToolResult(text, structuredContent = {}) {\n return {\n structuredContent,\n content: [{ type: 'text', text }],\n };\n}\n\nfunction sseHeaders(res) {\n res.status(200);\n res.setHeader('Content-Type', 'text/event-stream; charset=utf-8');\n res.setHeader('Cache-Control', 'no-cache, no-transform');\n res.setHeader('Connection', 'keep-alive');\n res.setHeader('X-Accel-Buffering', 'no');\n if (typeof res.flushHeaders === 'function') res.flushHeaders();\n}\n\nfunction writeSse(res, event, payload) {\n res.write(`event: ${event}\\n`);\n res.write(`data: ${JSON.stringify(payload)}\\n\\n`);\n}\n\nfunction asyncHandler(fn) {\n return (req, res, next) => {\n Promise.resolve(fn(req, res, next)).catch(next);\n };\n}\n\nfunction bearerToken(req) {\n const delegated = readText(req.get('x-dacheng-auth-token'));\n if (\n delegated &&\n delegated !== 'DACHENG_AUTH_TOKEN' &&\n !delegated.startsWith('secretref-env:') &&\n delegated !== 'dacheng-openclaw-proxy'\n ) {\n return delegated;\n }\n const header = req.get('Authorization') || '';\n if (!header.startsWith('Bearer ')) return '';\n const token = header.slice('Bearer '.length).trim();\n return token === 'dacheng-openclaw-proxy' ? '' : token;\n}\n\nfunction sha256(value) {\n return crypto.createHash('sha256').update(value).digest('hex');\n}\n\nfunction safeUserText(value, fallback = '') {\n if (typeof value !== 'string') return fallback;\n return value.replace(/\\u0000/g, '').trim();\n}\n\nfunction estimateTokens(text) {\n const normalized = String(text || '').trim();\n if (!normalized) return 0;\n return Math.max(1, Math.ceil(normalized.length / 3));\n}\n\nfunction readText(value) {\n if (value === undefined || value === null) return '';\n return String(value).replace(/\\u0000/g, '').trim();\n}\n\nconst miniAppHostApiVersion = '2.0';\nconst miniAppHighRiskPermissions = new Set([\n 'auth.token',\n 'payments.alipay',\n 'payments.fudeGold',\n 'desktop.control',\n 'network.http',\n 'network.udp',\n 'network.interfaces',\n 'system.keepAwake',\n 'hotspot.settings',\n 'local.loopback',\n 'fs.readWrite',\n 'runtime.process',\n 'shell.execute',\n 'browser.external',\n 'files.write',\n 'projects.write',\n 'openclaw.restart',\n]);\n\nfunction officialMiniAppRegistry(req) {\n const webBase = process.env.MINIAPP_WEB_BASE_URL || 'https://fabushi.ombhrum.com';\n const official = [\n {\n botId: 'bot.global-dharma',\n miniAppId: 'official.global-dharma',\n title: '全球法布施',\n subtitle: '地区、循环、场能都在对话框上方设置',\n initials: '法',\n icon: 'public',\n avatarColor: '#4CAF7A',\n kind: 'global_dharma',\n entryUrl: process.env.GLOBAL_DHARMA_MINIAPP_URL,\n greeting: '把要分享的文字、链接或素材发给我,我会按上方选择的地区与模式启动全球法布施。',\n inputHint: '输入要法布施的文字/链接,或点 + 添加素材',\n permissions: [\n 'app.context',\n 'bot.chat',\n 'auth.session',\n 'wallet.balance',\n 'payments.entitlement',\n 'payments.fudeGold',\n 'payments.alipay',\n 'files.pick',\n 'network.http',\n 'network.udp',\n 'network.interfaces',\n 'system.keepAwake',\n 'hotspot.settings',\n 'local.loopback',\n 'openclaw.status',\n 'openclaw.chat',\n 'desktop.control',\n 'fs.readWrite',\n 'runtime.process',\n ],\n },\n {\n botId: 'bot.flashcards',\n miniAppId: 'official.flashcards',\n title: '背诵闪卡制作',\n subtitle: '随机挖空 / AI 制卡从顶部模式按钮选择',\n initials: '卡',\n icon: 'flashcards',\n avatarColor: '#7E57C2',\n kind: 'flashcards',\n entryUrl: process.env.FLASHCARDS_MINIAPP_URL,\n greeting: '粘贴经文、文章正文或链接即可制作背诵闪卡。制卡模式请在上方按钮切换。',\n inputHint: '粘贴链接或正文,发送后制作闪卡',\n permissions: ['app.context', 'bot.chat', 'flashcards.create', 'openclaw.status', 'openclaw.chat'],\n },\n {\n botId: 'bot.platform-publish',\n miniAppId: 'official.platform-publish',\n title: '法布施到平台',\n subtitle: '选择平台后生成发布草稿并打开入口',\n initials: '发',\n icon: 'publish',\n avatarColor: '#FF9F43',\n kind: 'platform_publish',\n entryUrl: process.env.PLATFORM_PUBLISH_MINIAPP_URL,\n greeting: '把要发布的正文或链接发给我,上方选择平台后,我会生成发布草稿并打开对应平台入口。',\n inputHint: '输入要发布到平台的正文/链接',\n permissions: [\n 'app.context',\n 'bot.chat',\n 'platform.publish',\n 'files.pick',\n 'fs.readWrite',\n 'runtime.process',\n 'shell.execute',\n 'browser.external',\n ],\n },\n {\n botId: 'bot.father',\n miniAppId: 'official.bot-father',\n title: '机器人之父',\n subtitle: '用对话生成个人沙箱小程序',\n initials: '父',\n icon: 'bot_father',\n avatarColor: '#3D8BFF',\n kind: 'bot_father',\n entryUrl: process.env.BOT_FATHER_MINIAPP_URL,\n greeting: '告诉我你想要的小程序,我会生成 manifest、界面和权限声明,并放进你的个人沙箱。',\n inputHint: '描述你想创建的小程序',\n permissions: ['app.context', 'bot.chat', 'miniapps.dev'],\n },\n ];\n return {\n bots: official.map((bot, index) => ({\n ...bot,\n destinationIndex: index,\n source: 'official',\n })),\n miniApps: official.map((bot) => ({\n miniAppId: bot.miniAppId,\n botId: bot.botId,\n title: bot.title,\n subtitle: bot.subtitle,\n entryUrl:\n bot.entryUrl ||\n `${webBase.replace(/\\/+$/, '')}/miniapps/${encodeURIComponent(bot.miniAppId)}`,\n version: '2.0.0',\n permissions: bot.permissions,\n surfaces: ['homePinned', 'chatPanel'],\n theme: 'telegramDark',\n signature: 'official',\n reviewStatus: 'trusted',\n source: 'official',\n })),\n };\n}\n\nasync function readMiniAppStore() {\n try {\n const raw = await fs.promises.readFile(miniAppsStorePath, 'utf8');\n const decoded = JSON.parse(raw);\n return {\n apps: Array.isArray(decoded.apps) ? decoded.apps : [],\n };\n } catch (error) {\n if (error?.code === 'ENOENT') return { apps: [] };\n throw error;\n }\n}\n\nasync function writeMiniAppStore(store) {\n await fs.promises.mkdir(miniAppsDir, { recursive: true });\n await fs.promises.writeFile(\n miniAppsStorePath,\n JSON.stringify({ apps: store.apps || [] }, null, 2),\n );\n}\n\nfunction miniAppEntryUrl(req, miniAppId) {\n const origin = requestOrigin(req) || '';\n const pathName = `/api/miniapps/dev/${encodeURIComponent(miniAppId)}/index.html`;\n return origin ? new URL(pathName, `${origin}/`).toString() : pathName;\n}\n\nfunction sanitizeMiniAppPermissions(value, { allowHighRisk = false } = {}) {\n const raw = Array.isArray(value) ? value : [];\n const normalized = raw\n .map((item) => readText(item).toLowerCase())\n .filter((item) => /^[a-z][a-z0-9_.:-]{0,64}$/.test(item));\n const deduped = [...new Set(['app.context', 'bot.chat', ...normalized])];\n return deduped.filter((permission) => allowHighRisk || !miniAppHighRiskPermissions.has(permission));\n}\n\nfunction miniAppScan(permissions, sourceHtml = '') {\n const highRisk = permissions.filter((permission) => miniAppHighRiskPermissions.has(permission));\n const blockedPatterns = [\n /authorization\\s*:\\s*['\"`]bearer/i,\n /desktop_control_bridge_token/i,\n /openclaw_gateway_token/i,\n /eval\\s*\\(/i,\n ];\n const blocked = blockedPatterns\n .filter((pattern) => pattern.test(sourceHtml))\n .map((pattern) => pattern.toString());\n return {\n passed: blocked.length === 0,\n highRiskPermissions: highRisk,\n blockedPatterns: blocked,\n };\n}\n\nfunction inferMiniAppPermissionsFromText(...values) {\n const text = values.map((value) => readText(value)).join('\\n').toLowerCase();\n const rules = [\n ['auth.session', [/auth\\.(getsession|requirelogin)/, /登录|用户信息|宿主账号/]],\n ['auth.token', [/auth\\.getaccesstoken/, /access token|访问 token/]],\n ['payments.alipay', [/payments\\.alipay/, /支付宝|支付|购买|订单/]],\n ['payments.entitlement', [/payments\\.checkentitlement|权益|解锁状态|购买状态/]],\n ['payments.fudeGold', [/payments\\.requestpayment|福德金|代币|钱包|余额/]],\n ['wallet.balance', [/wallet\\.getbalance|福德金|代币|钱包|余额/]],\n ['network.http', [/network\\.http|http\\.fetch|fetch\\s*\\(/, /链接正文|抓取网页|http 请求|网页内容/]],\n ['network.udp', [/network\\.udp|udp|datagram/, /全球法布施|法布施发送|数据报/]],\n ['network.interfaces', [/network\\.interfaces|网卡|网络接口|广播地址/]],\n ['system.keepAwake', [/system\\.keepawake|保持唤醒|后台任务|keep awake/]],\n ['hotspot.settings', [/hotspot\\.opensettings|热点|本地场能/]],\n ['local.loopback', [/localloopback\\./, /localhost|127\\.0\\.0\\.1|本地回环|本地转经轮/]],\n ['flashcards.create', [/flashcards\\./, /闪卡|背诵卡|挖空/]],\n ['platform.publish', [/platformpublish\\./, /发布草稿|发布到平台|公众号|小红书|知乎/]],\n ['files.pick', [/files\\.pick/, /选择文件|上传文件|本地素材/]],\n ['fs.readWrite', [/fs\\.(writefile|readfile)/, /本地文件|写文件|读文件|文件处理/]],\n ['runtime.process', [/runtime\\.process|process\\.execute/, /本地进程|执行程序|命令行|cli|playwright/]],\n ['shell.execute', [/shell\\.execute/, /shell/]],\n ['browser.external', [/browser\\.open/, /浏览器|打开网页|打开平台/]],\n ['desktop.control', [/desktopcontrol\\./, /桌面控制|点击|键盘|鼠标/]],\n ['openclaw.chat', [/openclaw\\.chat/, /openclaw/]],\n ];\n return rules\n .filter(([, patterns]) => patterns.some((pattern) => pattern.test(text)))\n .map(([permission]) => permission);\n}\n\nfunction miniAppRecordToBot(record) {\n return {\n botId: record.botId,\n miniAppId: record.miniAppId,\n title: record.title,\n subtitle: record.subtitle,\n initials: record.initials || '小',\n icon: record.icon || 'code',\n avatarColor: record.avatarColor || '#3D8BFF',\n kind: 'third_party',\n greeting: record.greeting || '你好,我是这个小程序的机器人。',\n inputHint: record.inputHint || '输入消息,或打开小程序面板',\n permissions: record.permissions || ['app.context', 'bot.chat'],\n source: record.source || 'sandbox',\n };\n}\n\nfunction miniAppRecordToManifest(req, record) {\n return {\n miniAppId: record.miniAppId,\n botId: record.botId,\n title: record.title,\n subtitle: record.subtitle,\n entryUrl: miniAppEntryUrl(req, record.miniAppId),\n version: record.version || '0.0.1',\n permissions: record.permissions || ['app.context', 'bot.chat'],\n surfaces: record.surfaces || ['chatPanel'],\n theme: record.theme || 'telegramDark',\n signature: record.signature || `sandbox:${record.ownerId}`,\n reviewStatus: record.reviewStatus || 'sandbox',\n source: record.source || 'sandbox',\n };\n}\n\nfunction safeMiniAppId(value) {\n return readText(value).replace(/[^a-zA-Z0-9_.-]/g, '-').slice(0, 80);\n}\n\nfunction titleFromPromptForMiniApp(prompt) {\n const text = readText(prompt).replace(/\\s+/g, ' ');\n if (!text) return '个人小程序';\n return text.length <= 18 ? text : text.slice(0, 18);\n}\n\nfunction escapeHtml(value) {\n return String(value || '')\n .replace(/&/g, '&')\n .replace(/</g, '<')\n .replace(/>/g, '>')\n .replace(/\"/g, '"')\n .replace(/'/g, ''');\n}\n\nfunction generatedMiniAppHtml({ title, prompt }) {\n const safeTitle = escapeHtml(title);\n const safePrompt = escapeHtml(prompt);\n const promptJson = JSON.stringify(readText(prompt));\n return `<!doctype html>\n<html lang=\"zh-Hans\">\n<head>\n <meta charset=\"utf-8\" />\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n <title>${safeTitle}</title>\n <style>\n body { margin:0; font-family:-apple-system,BlinkMacSystemFont,\"Segoe UI\",sans-serif; background:#0f1722; color:#fff; }\n main { padding:20px; display:grid; gap:14px; }\n .card { border:1px solid #263445; background:#17212b; border-radius:16px; padding:16px; }\n button { border:0; border-radius:999px; padding:10px 14px; background:#3390ec; color:white; font-weight:800; }\n pre { white-space:pre-wrap; color:#9ec7ff; }\n </style>\n</head>\n<body>\n <main>\n <section class=\"card\">\n <h1>${safeTitle}</h1>\n <p>这是机器人之父根据你的描述生成的个人沙箱小程序。</p>\n <pre>${safePrompt}</pre>\n </section>\n <section class=\"card\">\n <button id=\"context\">读取宿主上下文</button>\n <button id=\"spec\">读取宿主能力</button>\n <button id=\"chat\">让机器人解释这个小程序</button>\n <pre id=\"output\">等待操作...</pre>\n </section>\n </main>\n <script>\n const out = document.getElementById('output');\n async function invoke(method, params) {\n if (!window.FabushiMiniApp) throw new Error('FabushiMiniApp host SDK not ready');\n const result = await window.FabushiMiniApp.invoke(method, params || {});\n out.textContent = JSON.stringify(result, null, 2);\n }\n document.getElementById('context').onclick = () => invoke('app.getContext');\n document.getElementById('spec').onclick = () => invoke('app.getHostApiSpec');\n const prompt = ${promptJson};\n document.getElementById('chat').onclick = () => invoke('bot.sendMessage', {\n message: '请简要说明这个小程序能做什么:' + prompt\n });\n const seenCommands = new Set();\n function deliverMiniAppCommand(detail) {\n if (!detail || typeof detail !== 'object') return;\n const commandKey = detail.commandId || detail.id || [detail.createdAt, detail.command, detail.rawText || detail.text].filter(Boolean).join(':');\n if (commandKey) {\n if (seenCommands.has(commandKey)) return;\n seenCommands.add(commandKey);\n }\n handleMiniAppCommand(detail);\n }\n async function handleMiniAppCommand(detail) {\n out.textContent = '收到后台命令:' + JSON.stringify(detail, null, 2);\n try {\n await invoke('bot.postMessage', {\n commandId: detail.commandId || detail.id,\n message: '「${safeTitle}」已收到后台命令:' + (detail.args || detail.text || '')\n });\n await invoke('bot.reportCommandResult', {\n commandId: detail.commandId || detail.id,\n status: 'completed',\n message: '「${safeTitle}」后台命令已处理。'\n });\n } catch (error) {\n out.textContent = String(error && error.message || error);\n }\n }\n window.addEventListener('fabushi-miniapp-command', (event) => {\n deliverMiniAppCommand(event.detail || {});\n });\n if (window.__fabushiLastMiniAppCommand) {\n (window.queueMicrotask || ((fn) => setTimeout(fn, 0)))(() => {\n deliverMiniAppCommand(window.__fabushiLastMiniAppCommand);\n });\n }\n </script>\n</body>\n</html>`;\n}\n\nfunction extractMiniAppHtml(value) {\n const text = readText(value);\n if (!text) return '';\n const fenced = text.match(/```(?:html)?\\s*([\\s\\S]*?)```/i);\n const candidate = (fenced ? fenced[1] : text).trim();\n if (!/<html[\\s>]/i.test(candidate) || !/<script[\\s>]/i.test(candidate)) return '';\n return candidate.startsWith('<!doctype') || candidate.startsWith('<html')\n ? candidate\n : `<!doctype html>\\n${candidate}`;\n}\n\nasync function generateBotFatherMiniAppHtml({ title, prompt, user }) {\n const fallback = generatedMiniAppHtml({ title, prompt });\n try {\n const result = await runAgentModel({\n user,\n mode: 'miniapp_generation',\n maxCompletionTokens: deepseekMaxCompletionTokens,\n messages: [\n {\n role: 'system',\n content: [\n '你是 Fabushi App 的小程序生成器。',\n '只输出一个完整静态 HTML 文件,不要 Markdown,不要解释。',\n '小程序只能通过 window.FabushiMiniApp.invoke(method, params) 调用宿主能力。',\n '先调用 app.getHostApiSpec 或 app.getCapabilities 判断宿主版本和已授权权限,再显示对应 UI。',\n '聊天后台调用协议:优先使用 window.FabushiMiniApp.bot.onCommand(\"/start\", handler) 或 bot.onAnyCommand(handler);如果手写监听 window 的 fabushi-miniapp-command 事件,注册后也要读取 window.__fabushiLastMiniAppCommand 并按 commandId 去重补处理。detail.command 默认为 /start,detail.args 是用户文本;处理完成后调用 bot.postMessage 或 bot.reportCommandResult 把进度/结果写回聊天框。',\n '标准能力:auth.getSession/auth.requireLogin 复用宿主登录;payments.requestPayment 使用福德金支付并由宿主登记权益;payments.checkEntitlement 查询商品是否已解锁;payments.alipay.pay/createOrder 仅作为宿主官方现金支付兜底;network.http.fetch/network.udp/network.interfaces 提供 HTTP、UDP 和网卡原语;system.keepAwake/hotspot.openSettings 提供系统级辅助;flashcards.createDeck 接入背诵闪卡;platformPublish.createDraft 接入发布草稿;fs.writeFile/fs.readFile、runtime.process.execute、browser.open 用于本地文件、进程和浏览器。',\n '高危权限包括 auth.token、payments.alipay、payments.fudeGold、network.http、network.udp、network.interfaces、system.keepAwake、hotspot.settings、local.loopback、fs.readWrite、runtime.process、shell.execute、browser.external、desktop.control;只有用户需求明确需要时才使用。',\n '禁止外链脚本、eval、Authorization token、桌面桥 token、OpenClaw token。',\n '不要调用 auth.getAccessToken,除非用户明确要求对接需要 token 的受信服务。',\n '优先实现一个可点击、可立即使用的小面板,包含 app.getContext 或 bot.sendMessage 示例。',\n ].join('\\n'),\n },\n {\n role: 'user',\n content: [\n `小程序标题:${title}`,\n `用户需求:${prompt}`,\n '请生成 HTML/CSS/JS 单文件。',\n ].join('\\n'),\n },\n ],\n });\n const sourceHtml = extractMiniAppHtml(result.message);\n if (!sourceHtml) {\n return {\n sourceHtml: fallback,\n generation: { provider: 'template', fallbackReason: 'ai_output_not_html' },\n };\n }\n return {\n sourceHtml,\n generation: {\n provider: result.provider || 'ai',\n model: result.model || '',\n },\n };\n } catch (error) {\n return {\n sourceHtml: fallback,\n generation: {\n provider: 'template',\n fallbackReason: readText(error?.message || error),\n },\n };\n }\n}\n\nfunction clientContextMessage(client) {\n if (!client || typeof client !== 'object') return '';\n const selectedModel = readText(client.selectedModel || client.model);\n const project = client.project && typeof client.project === 'object' ? client.project : {};\n const projectName = readText(project.name);\n const projectPath = readText(project.path);\n const lines = ['桌面端上下文:'];\n if (selectedModel) lines.push(`用户选择的 DeepSeek 模型:${normalizeDeepSeekModelName(selectedModel)}`);\n if (projectPath) {\n lines.push(`当前项目名称:${projectName || projectPath}`);\n lines.push(`当前项目路径:${projectPath}`);\n lines.push('如果本轮任务涉及读取、修改、运行、分析或整理项目,请优先围绕当前项目路径工作;不要擅自操作其他目录。');\n }\n return lines.length > 1 ? lines.join('\\n') : '';\n}\nfunction unwrapPayload(payload) {\n if (!payload || typeof payload !== 'object') return {};\n return payload.data && typeof payload.data === 'object' ? payload.data : payload;\n}\n\nfunction readFirstText(values) {\n for (const value of values) {\n const text = readText(value);\n if (text) return text;\n }\n return '';\n}\n\nfunction activeFlag(value) {\n if (value === true) return true;\n if (value === 1) return true;\n if (typeof value !== 'string') return false;\n return ['true', '1', 'yes', 'active'].includes(value.trim().toLowerCase());\n}\n\nfunction futureDate(value) {\n const text = readText(value);\n if (!text) return false;\n const timestamp = Date.parse(text);\n return Number.isFinite(timestamp) && timestamp > Date.now();\n}\n\nfunction normalizeMembershipPayload(payload) {\n if (!payload || typeof payload !== 'object') return false;\n const data = unwrapPayload(payload);\n const membership = data.membership && typeof data.membership === 'object' ? data.membership : data;\n const type = readText(membership.type || membership.membershipType || data.membershipType).toLowerCase();\n const status = readText(membership.status || data.status).toLowerCase();\n const expiry = membership.expiresAt ||\n membership.expires_at ||\n membership.membershipExpiry ||\n membership.membership_expires_at ||\n data.membershipExpiry ||\n data.membership_expires_at;\n const hasExplicitActiveFlag =\n membership.isActive !== undefined ||\n membership.active !== undefined ||\n membership.hasPremiumAccess !== undefined ||\n membership.hasPremiumMembership !== undefined ||\n membership.isPremium !== undefined;\n\n if (hasExplicitActiveFlag) {\n return Boolean(\n activeFlag(membership.isActive) ||\n activeFlag(membership.active) ||\n activeFlag(membership.hasPremiumAccess) ||\n activeFlag(membership.hasPremiumMembership) ||\n activeFlag(membership.isPremium),\n );\n }\n\n return Boolean(\n status === 'active' ||\n type === 'paid' ||\n type === 'premium' ||\n type === 'trial' ||\n futureDate(expiry),\n );\n}\n\nfunction extractRemoteAccount(payload) {\n const data = unwrapPayload(payload);\n const nestedUser = data.user && typeof data.user === 'object' ? data.user : {};\n const id = readFirstText([\n data.userId,\n data.user_id,\n data.userNo,\n data.user_no,\n data.id,\n nestedUser.userId,\n nestedUser.user_id,\n nestedUser.userNo,\n nestedUser.user_no,\n nestedUser.id,\n ]);\n const username = readFirstText([data.username, data.name, nestedUser.username, nestedUser.name]);\n const email = readFirstText([data.email, nestedUser.email]);\n const membership = data.membership && typeof data.membership === 'object' ? data.membership : null;\n return {\n authenticated: Boolean(id || username || email || membership || data.success === true),\n id,\n username,\n email,\n isMember: normalizeMembershipPayload(payload),\n membership,\n };\n}\n\nasync function resolveRemoteAccount(token) {\n const fallback = {\n authenticated: false,\n id: '',\n username: '',\n email: '',\n isMember: false,\n membership: null,\n };\n if (!token || !fabushiApiBaseUrl) return fallback;\n\n const endpoints = [\n '/api/stripe/membership-status',\n '/api/alipay/check-membership',\n '/api/auth/user-info',\n ];\n\n for (const endpoint of endpoints) {\n try {\n const response = await fetch(`${fabushiApiBaseUrl}${endpoint}`, {\n headers: {\n Accept: 'application/json',\n Authorization: `Bearer ${token}`,\n },\n signal: AbortSignal.timeout(8_000),\n });\n if (response.status === 401 || response.status === 403) return fallback;\n if (!response.ok) continue;\n const payload = await response.json();\n const account = extractRemoteAccount(payload);\n if (account.authenticated || account.isMember) {\n return {\n ...fallback,\n ...account,\n authenticated: true,\n };\n }\n } catch (error) {\n logger.debug({ endpoint, error: String(error) }, 'membership lookup skipped');\n }\n }\n\n return fallback;\n}\n\nasync function resolveUser(req, body = {}) {\n const token = bearerToken(req);\n const usernameHint = safeUserText(body.username || req.query.username);\n\n const testToken = process.env.TEST_ACCOUNT_TOKEN || 'dacheng-test-token-bypass-limits';\n if (token && token === testToken) {\n return {\n userId: 'user:test_account',\n username: 'TestAccount',\n tokenHash: sha256(token).slice(0, 24),\n isAuthenticated: true,\n isMember: true,\n isTestAccount: true,\n membership: { type: 'lifetime', active: true },\n };\n }\n\n const tokenHash = token ? sha256(token).slice(0, 24) : '';\n const anonymousHash = sha256(`${req.ip}|${req.get('User-Agent') || ''}`).slice(0, 24);\n const remoteAccount = await resolveRemoteAccount(token);\n const remoteIdentity = remoteAccount.id ||\n remoteAccount.username ||\n remoteAccount.email ||\n (remoteAccount.authenticated ? tokenHash : '');\n const userId = remoteAccount.authenticated\n ? `user:${remoteIdentity}`\n : tokenHash\n ? `token:${tokenHash}`\n : `anon:${anonymousHash}`;\n return {\n userId,\n username: remoteAccount.username || usernameHint,\n tokenHash,\n isAuthenticated: remoteAccount.authenticated,\n isMember: remoteAccount.isMember,\n membership: remoteAccount.membership,\n };\n}\n\nfunction usageFor(userId) {\n const month = monthKey();\n const row = statements.getUsage.get(userId, month);\n return {\n month,\n used: Number(row?.totalTokens || 0),\n };\n}\n\nfunction enforceTokenBudget(user, estimatedTokensForRequest) {\n if (user && user.isTestAccount) {\n return { limit: 999999999, usage: { month: monthKey(), used: 0 } };\n }\n if (requireMemberForAi && !user.isMember) {\n const error = new Error('大乘 AI 需要开通会员后使用');\n error.statusCode = 403;\n error.code = 'AI_MEMBERSHIP_REQUIRED';\n error.details = {\n requireMemberForAi,\n monthlyLimit: memberMonthlyLimit,\n remainingTokens: 0,\n };\n throw error;\n }\n\n const limit = user.isMember ? memberMonthlyLimit : freeMonthlyLimit;\n const usage = usageFor(user.userId);\n if (usage.used + estimatedTokensForRequest > limit) {\n const error = new Error('本月 AI token 额度已不足');\n error.statusCode = 429;\n error.code = 'USER_TOKEN_QUOTA_EXCEEDED';\n error.details = {\n monthlyLimit: limit,\n usedTokens: usage.used,\n remainingTokens: Math.max(0, limit - usage.used),\n };\n throw error;\n }\n return { limit, usage };\n}\n\nfunction completionBudgetFor(budget, estimatedPromptTokens) {\n const remainingAfterPrompt = budget.limit - budget.usage.used - estimatedPromptTokens;\n return Math.max(1, Math.min(deepseekMaxCompletionTokens, Math.floor(remainingAfterPrompt)));\n}\n\nfunction recordUsage(userId, totalTokens) {\n if (!totalTokens || totalTokens < 1) return;\n statements.upsertUsage.run({\n userId,\n month: monthKey(),\n totalTokens,\n updatedAt: nowIso(),\n });\n}\n\nfunction titleFromMessage(message) {\n const compact = message.replace(/\\s+/g, ' ').trim();\n if (!compact) return '新对话';\n return compact.length > 22 ? `${compact.slice(0, 22)}...` : compact;\n}\n\nfunction normalizeMessages(rows) {\n return rows.map((row) => ({\n role: row.role,\n content: row.content,\n }));\n}\n\nfunction normalizeMaxCompletionTokens(value) {\n const numeric = Number(value || deepseekMaxCompletionTokens);\n if (!Number.isFinite(numeric) || numeric < 1) return deepseekMaxCompletionTokens;\n return Math.max(1, Math.min(deepseekMaxCompletionTokens, Math.floor(numeric)));\n}\n\nfunction normalizeDeepSeekModelName(value) {\n const raw = readText(value || deepseekModel);\n if (!raw) return deepseekModel;\n const modelId = raw.includes('/') ? raw.split('/').pop() : raw;\n return modelId || deepseekModel;\n}\n\nasync function callDeepSeek(messages, options = {}) {\n if (!deepseekApiKey) {\n const error = new Error('DeepSeek API key is not configured');\n error.statusCode = 503;\n throw error;\n }\n\n const model = normalizeDeepSeekModelName(options.model);\n const response = await fetch(`${deepseekBaseUrl}/chat/completions`, {\n method: 'POST',\n headers: {\n Accept: 'application/json',\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${deepseekApiKey}`,\n },\n body: JSON.stringify({\n model,\n messages,\n temperature: 0.4,\n max_tokens: normalizeMaxCompletionTokens(options.maxCompletionTokens),\n stream: false,\n }),\n signal: AbortSignal.timeout(60_000),\n });\n\n const bodyText = await response.text();\n let payload;\n try {\n payload = bodyText ? JSON.parse(bodyText) : {};\n } catch {\n payload = { error: bodyText };\n }\n\n if (!response.ok) {\n const upstreamMessage = payload?.error?.message || payload?.message || '';\n const friendlyMessage =\n response.status === 402 || /insufficient\\s+balance/i.test(upstreamMessage)\n ? '大乘 AI 服务额度暂不可用,请联系管理员充值 DeepSeek 账户后重试。'\n : upstreamMessage || `DeepSeek request failed: ${response.status}`;\n const error = new Error(friendlyMessage);\n error.statusCode = response.status;\n if (response.status === 402 || /insufficient\\s+balance/i.test(upstreamMessage)) {\n error.code = 'DEEPSEEK_PROVIDER_QUOTA';\n }\n throw error;\n }\n\n const message = payload?.choices?.[0]?.message?.content?.trim();\n if (!message) {\n const error = new Error('DeepSeek returned an empty response');\n error.statusCode = 502;\n throw error;\n }\n\n return {\n message,\n model,\n usage: {\n promptTokens: Number(payload?.usage?.prompt_tokens || 0),\n completionTokens: Number(payload?.usage?.completion_tokens || 0),\n totalTokens: Number(payload?.usage?.total_tokens || 0),\n },\n };\n}\n\nasync function callDeepSeekStream(messages, callbacks = {}) {\n if (!deepseekApiKey) {\n const error = new Error('DeepSeek API key is not configured');\n error.statusCode = 503;\n throw error;\n }\n\n const model = normalizeDeepSeekModelName(callbacks.model);\n const response = await fetch(`${deepseekBaseUrl}/chat/completions`, {\n method: 'POST',\n headers: {\n Accept: 'text/event-stream',\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${deepseekApiKey}`,\n },\n body: JSON.stringify({\n model,\n messages,\n temperature: 0.4,\n max_tokens: normalizeMaxCompletionTokens(callbacks.maxCompletionTokens),\n stream: true,\n stream_options: { include_usage: true },\n }),\n signal: callbacks.signal || AbortSignal.timeout(90_000),\n });\n\n if (!response.ok) {\n const bodyText = await response.text();\n let payload;\n try {\n payload = bodyText ? JSON.parse(bodyText) : {};\n } catch {\n payload = { error: bodyText };\n }\n const upstreamMessage = payload?.error?.message || payload?.message || '';\n const friendlyMessage =\n response.status === 402 || /insufficient\\s+balance/i.test(upstreamMessage)\n ? '大乘 AI 服务额度暂不可用,请联系管理员充值 DeepSeek 账户后重试。'\n : upstreamMessage || `DeepSeek request failed: ${response.status}`;\n const error = new Error(friendlyMessage);\n error.statusCode = response.status;\n if (response.status === 402 || /insufficient\\s+balance/i.test(upstreamMessage)) {\n error.code = 'DEEPSEEK_PROVIDER_QUOTA';\n }\n throw error;\n }\n\n let buffer = '';\n let message = '';\n let usage = {\n promptTokens: 0,\n completionTokens: 0,\n totalTokens: 0,\n };\n const decoder = new TextDecoder();\n\n for await (const chunk of response.body) {\n buffer += decoder.decode(chunk, { stream: true });\n const lines = buffer.split(/\\r?\\n/);\n buffer = lines.pop() || '';\n\n for (const rawLine of lines) {\n const line = rawLine.trim();\n if (!line.startsWith('data:')) continue;\n const data = line.slice('data:'.length).trim();\n if (!data || data === '[DONE]') continue;\n\n let payload;\n try {\n payload = JSON.parse(data);\n } catch {\n continue;\n }\n\n const delta = payload?.choices?.[0]?.delta?.content || '';\n if (delta) {\n message += delta;\n callbacks.onToken?.(delta);\n }\n\n if (payload?.usage) {\n usage = {\n promptTokens: Number(payload.usage.prompt_tokens || usage.promptTokens || 0),\n completionTokens: Number(payload.usage.completion_tokens || usage.completionTokens || 0),\n totalTokens: Number(payload.usage.total_tokens || usage.totalTokens || 0),\n };\n }\n }\n }\n\n message = message.trim();\n if (!message) {\n const error = new Error('DeepSeek returned an empty response');\n error.statusCode = 502;\n throw error;\n }\n\n return { message, model, usage };\n}\n\nfunction createCodexDeepSeekRuntime() {\n if (!enableCodexSdkChat || !deepseekApiKey) {\n return {\n enabled: false,\n provider: 'deepseek-direct',\n reason: enableCodexSdkChat ? 'DeepSeek API key is not configured' : 'ENABLE_CODEX_SDK_CHAT is not true',\n };\n }\n\n return {\n enabled: true,\n provider: 'codex-sdk-deepseek',\n options: {\n apiKey: deepseekApiKey,\n baseUrl: deepseekBaseUrl,\n env: {\n ...process.env,\n DEEPSEEK_API_KEY: deepseekApiKey,\n HOME: codexHomeDir,\n CODEX_HOME: codexHomeDir,\n TMPDIR: codexTempDir,\n XDG_RUNTIME_DIR: codexRuntimeDir,\n XDG_CACHE_HOME: path.join(codexHomeDir, '.cache'),\n XDG_CONFIG_HOME: path.join(codexHomeDir, '.config'),\n XDG_DATA_HOME: path.join(codexHomeDir, '.local', 'share'),\n PATH: process.env.PATH || '/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin',\n },\n config: {\n model_provider: codexDeepSeekProviderId,\n model_providers: {\n [codexDeepSeekProviderId]: {\n name: 'DeepSeek Chat Completions',\n base_url: codexResponsesBaseUrl,\n env_key: 'DEEPSEEK_API_KEY',\n wire_api: 'responses',\n query_params: {},\n },\n },\n },\n },\n threadOptions: {\n model: deepseekModel,\n approvalPolicy: 'never',\n sandboxMode: 'read-only',\n skipGitRepoCheck: true,\n networkAccessEnabled: true,\n },\n };\n}\n\nfunction codexPromptFromMessages(messages) {\n return messages\n .map((message) => {\n const role = String(message.role || 'user').toUpperCase();\n return `${role}:\\n${message.content}`;\n })\n .join('\\n\\n');\n}\n\nfunction responseContentText(content) {\n if (typeof content === 'string') return content;\n if (!Array.isArray(content)) return '';\n return content\n .map((part) => {\n if (typeof part === 'string') return part;\n if (!part || typeof part !== 'object') return '';\n return String(part.text || part.input_text || part.output_text || '');\n })\n .filter(Boolean)\n .join('\\n');\n}\n\nfunction codexResponsesPrompt(body) {\n const input = Array.isArray(body?.input) ? body.input : [];\n const userItems = input.filter((item) => item?.role === 'user');\n const source = userItems[userItems.length - 1] || input[input.length - 1];\n const prompt = responseContentText(source?.content).trim();\n if (prompt) return prompt;\n if (typeof body?.input === 'string') return body.input.trim();\n return '';\n}\n\nfunction writeResponsesEvent(res, event, data) {\n res.write(`event: ${event}\\n`);\n res.write(`data: ${JSON.stringify(data)}\\n\\n`);\n}\n\nfunction responseUsage(usage = {}) {\n return {\n input_tokens: Number(usage.promptTokens || usage.input_tokens || 0),\n cached_input_tokens: 0,\n output_tokens: Number(usage.completionTokens || usage.output_tokens || 0),\n reasoning_output_tokens: 0,\n total_tokens: Number(usage.totalTokens || usage.total_tokens || 0),\n };\n}\n\nfunction createLibreChatAgentRuntime() {\n if (!enableLibreChatAgentChat || !libreChatAgentApiKey || !libreChatAgentId) {\n return {\n enabled: false,\n provider: 'librechat-agent',\n reason: !enableLibreChatAgentChat\n ? 'ENABLE_LIBRECHAT_AGENT_CHAT is not true'\n : !libreChatAgentApiKey\n ? 'LIBRECHAT_AGENT_API_KEY is not configured'\n : 'LIBRECHAT_AGENT_ID is not configured',\n };\n }\n\n return {\n enabled: true,\n provider: 'librechat-agent',\n model: libreChatAgentId,\n chatCompletionsUrl: `${libreChatAgentsBaseUrl}/chat/completions`,\n };\n}\n\nfunction libreChatUsage(usage = {}) {\n return {\n promptTokens: Number(usage.prompt_tokens || usage.input_tokens || usage.promptTokens || 0),\n completionTokens: Number(usage.completion_tokens || usage.output_tokens || usage.completionTokens || 0),\n totalTokens: Number(usage.total_tokens || usage.totalTokens || 0),\n };\n}\n\nasync function callLibreChatAgent(messages) {\n const runtime = createLibreChatAgentRuntime();\n if (!runtime.enabled) {\n const error = new Error(runtime.reason || 'LibreChat Agent chat is not enabled');\n error.statusCode = 503;\n throw error;\n }\n\n const response = await fetch(runtime.chatCompletionsUrl, {\n method: 'POST',\n headers: {\n Accept: 'application/json',\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${libreChatAgentApiKey}`,\n },\n body: JSON.stringify({\n model: runtime.model,\n messages,\n temperature: 0.4,\n stream: false,\n }),\n signal: AbortSignal.timeout(120_000),\n });\n\n const bodyText = await response.text();\n let payload;\n try {\n payload = bodyText ? JSON.parse(bodyText) : {};\n } catch {\n payload = { error: bodyText };\n }\n\n if (!response.ok) {\n const upstreamMessage = payload?.error?.message || payload?.message || bodyText || '';\n const error = new Error(upstreamMessage || `LibreChat Agent request failed: ${response.status}`);\n error.statusCode = response.status;\n throw error;\n }\n\n const message = payload?.choices?.[0]?.message?.content?.trim();\n if (!message) {\n const error = new Error('LibreChat Agent returned an empty response');\n error.statusCode = 502;\n throw error;\n }\n\n return {\n message,\n usage: libreChatUsage(payload?.usage),\n };\n}\n\nasync function callLibreChatAgentStream(messages, callbacks = {}) {\n const runtime = createLibreChatAgentRuntime();\n if (!runtime.enabled) {\n const error = new Error(runtime.reason || 'LibreChat Agent chat is not enabled');\n error.statusCode = 503;\n throw error;\n }\n\n const response = await fetch(runtime.chatCompletionsUrl, {\n method: 'POST',\n headers: {\n Accept: 'text/event-stream',\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${libreChatAgentApiKey}`,\n },\n body: JSON.stringify({\n model: runtime.model,\n messages,\n temperature: 0.4,\n stream: true,\n }),\n signal: callbacks.signal || AbortSignal.timeout(180_000),\n });\n\n if (!response.ok) {\n const bodyText = await response.text();\n let payload;\n try {\n payload = bodyText ? JSON.parse(bodyText) : {};\n } catch {\n payload = { error: bodyText };\n }\n const upstreamMessage = payload?.error?.message || payload?.message || bodyText || '';\n const error = new Error(upstreamMessage || `LibreChat Agent request failed: ${response.status}`);\n error.statusCode = response.status;\n throw error;\n }\n\n let buffer = '';\n let message = '';\n let usage = { promptTokens: 0, completionTokens: 0, totalTokens: 0 };\n const decoder = new TextDecoder();\n\n for await (const chunk of response.body) {\n buffer += decoder.decode(chunk, { stream: true });\n const lines = buffer.split(/\\r?\\n/);\n buffer = lines.pop() || '';\n\n for (const rawLine of lines) {\n const line = rawLine.trim();\n if (!line.startsWith('data:')) continue;\n const data = line.slice('data:'.length).trim();\n if (!data || data === '[DONE]') continue;\n\n let payload;\n try {\n payload = JSON.parse(data);\n } catch {\n continue;\n }\n\n const delta = payload?.choices?.[0]?.delta?.content || '';\n if (delta) {\n message += delta;\n callbacks.onToken?.(delta);\n }\n\n if (payload?.usage) {\n usage = libreChatUsage(payload.usage);\n }\n }\n }\n\n message = message.trim();\n if (!message) {\n const error = new Error('LibreChat Agent returned an empty response');\n error.statusCode = 502;\n throw error;\n }\n\n return { message, usage };\n}\n\nfunction isDeepSeekQuotaError(error) {\n if (error?.code === 'DEEPSEEK_PROVIDER_QUOTA') return true;\n if (error?.code === 'USER_TOKEN_QUOTA_EXCEEDED' || error?.code === 'AI_MEMBERSHIP_REQUIRED') return false;\n const message = String(error?.message || error || '');\n return /insufficient\\s+balance|balance/i.test(message);\n}\n\nfunction publicAiErrorMessage(error) {\n if (isDeepSeekQuotaError(error)) {\n return '大乘 AI 服务额度暂不可用,请联系管理员充值 DeepSeek 账户后重试。';\n }\n return error?.message || 'Internal Server Error';\n}\n\nfunction responsesPayload({\n responseId,\n itemId,\n status,\n model,\n text = '',\n usage = null,\n error = null,\n}) {\n return {\n id: responseId,\n object: 'response',\n status,\n model,\n output:\n status === 'completed'\n ? [\n {\n id: itemId,\n type: 'message',\n role: 'assistant',\n status: 'completed',\n content: [{ type: 'output_text', text }],\n },\n ]\n : [],\n ...(usage ? { usage: responseUsage(usage) } : {}),\n ...(error\n ? {\n error: {\n message: error.message || String(error),\n type: 'server_error',\n },\n }\n : {}),\n };\n}\n\nasync function callCodexSdkDeepSeek(messages, callbacks = {}) {\n const codexRuntime = createCodexDeepSeekRuntime();\n if (!codexRuntime.enabled) {\n const error = new Error(codexRuntime.reason || 'Codex SDK chat is not enabled');\n error.statusCode = 503;\n throw error;\n }\n\n const { Codex } = await import('@openai/codex-sdk');\n const codex = new Codex(codexRuntime.options);\n const thread = codex.startThread(codexRuntime.threadOptions);\n const prompt = codexPromptFromMessages(messages);\n\n if (callbacks.onToken || callbacks.onStep) {\n const { events } = await thread.runStreamed(prompt, { signal: callbacks.signal });\n let finalResponse = '';\n let usage = null;\n\n for await (const event of events) {\n if (event.type === 'item.completed') {\n const item = event.item;\n if (item.type === 'agent_message') {\n finalResponse = item.text || finalResponse;\n callbacks.onToken?.(item.text || '');\n } else if (item.type === 'todo_list') {\n callbacks.onStep?.({\n title: 'Codex SDK 执行计划',\n message: item.items.map((todo) => `${todo.completed ? '✓' : '•'} ${todo.text}`).join('\\n'),\n });\n } else if (item.type === 'command_execution') {\n callbacks.onStep?.({\n title: 'Codex SDK 执行命令',\n message: item.command,\n });\n } else if (item.type === 'mcp_tool_call') {\n callbacks.onStep?.({\n title: 'Codex SDK 调用工具',\n message: `${item.server}.${item.tool}`,\n });\n }\n } else if (event.type === 'turn.completed') {\n usage = event.usage;\n } else if (event.type === 'turn.failed') {\n throw new Error(event.error?.message || 'Codex SDK turn failed');\n } else if (event.type === 'error') {\n throw new Error(event.message || 'Codex SDK stream failed');\n }\n }\n\n if (!finalResponse.trim()) {\n const error = new Error('Codex SDK returned an empty response');\n error.statusCode = 502;\n throw error;\n }\n\n return {\n message: finalResponse.trim(),\n usage: {\n promptTokens: Number(usage?.input_tokens || 0),\n completionTokens: Number(usage?.output_tokens || 0),\n totalTokens: Number((usage?.input_tokens || 0) + (usage?.output_tokens || 0)),\n },\n };\n }\n\n const turn = await thread.run(prompt);\n if (!turn.finalResponse?.trim()) {\n const error = new Error('Codex SDK returned an empty response');\n error.statusCode = 502;\n throw error;\n }\n\n return {\n message: turn.finalResponse.trim(),\n usage: {\n promptTokens: Number(turn.usage?.input_tokens || 0),\n completionTokens: Number(turn.usage?.output_tokens || 0),\n totalTokens: Number((turn.usage?.input_tokens || 0) + (turn.usage?.output_tokens || 0)),\n },\n };\n}\n\nconst activeAgentRuns = new Map();\n\nfunction normalizeAgentMode(value) {\n const mode = safeUserText(value || 'dharma_guide');\n return /^[a-z][a-z0-9_:-]{0,48}$/i.test(mode) ? mode : 'dharma_guide';\n}\n\nfunction openClawSessionKey(user, conversationId) {\n const env = process.env.NODE_ENV === 'production' ? 'prod' : 'dev';\n return `fabushi:env:${env}:user:${user.userId}:conversation:${conversationId}`;\n}\n\nfunction createOpenClawRuntime() {\n if (!enableOpenClawAgentChat || !openClawGatewayToken || !openClawGatewayUrl) {\n return {\n enabled: false,\n provider: 'openclaw-gateway',\n reason: !enableOpenClawAgentChat\n ? 'ENABLE_OPENCLAW_AGENT_CHAT is not true'\n : !openClawGatewayToken\n ? 'OPENCLAW_GATEWAY_TOKEN is not configured'\n : 'OPENCLAW_GATEWAY_URL is not configured',\n };\n }\n\n return {\n enabled: true,\n provider: 'openclaw-gateway',\n model: openClawAgentId,\n runsUrl: openClawRunsEndpoint || `${openClawGatewayUrl}/v1/agents/${encodeURIComponent(openClawAgentId)}/runs`,\n };\n}\n\nfunction extractOpenClawText(payload) {\n if (!payload || typeof payload !== 'object') return '';\n const candidates = [\n payload.message,\n payload.content,\n payload.output_text,\n payload.finalResponse,\n payload.final_response,\n payload.result?.message,\n payload.result?.content,\n payload.output?.message,\n payload.output?.content,\n payload.choices?.[0]?.message?.content,\n ];\n for (const candidate of candidates) {\n if (typeof candidate === 'string' && candidate.trim()) return candidate.trim();\n }\n return '';\n}\n\nfunction extractOpenClawUsage(payload) {\n const usage = payload?.usage || payload?.result?.usage || {};\n return {\n promptTokens: Number(usage.input_tokens || usage.prompt_tokens || usage.promptTokens || 0),\n completionTokens: Number(usage.output_tokens || usage.completion_tokens || usage.completionTokens || 0),\n totalTokens: Number(usage.total_tokens || usage.totalTokens || 0),\n };\n}\n\nasync function callOpenClawAgent({ messages, user, conversationId, mode, signal }) {\n const runtime = createOpenClawRuntime();\n if (!runtime.enabled) {\n const error = new Error(runtime.reason || 'OpenClaw Agent chat is not enabled');\n error.statusCode = 503;\n throw error;\n }\n\n const sessionKey = openClawSessionKey(user, conversationId);\n const response = await fetch(runtime.runsUrl, {\n method: 'POST',\n headers: {\n Accept: 'application/json',\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${openClawGatewayToken}`,\n },\n body: JSON.stringify({\n agentId: openClawAgentId,\n sessionKey,\n mode,\n stream: false,\n input: { messages },\n messages,\n metadata: {\n app: 'fabushi',\n userId: user.userId,\n conversationId,\n toolPolicy: 'public-user-read-mostly',\n },\n }),\n signal: signal || AbortSignal.timeout(180_000),\n });\n\n const bodyText = await response.text();\n let payload;\n try {\n payload = bodyText ? JSON.parse(bodyText) : {};\n } catch {\n payload = { message: bodyText };\n }\n\n if (!response.ok) {\n const error = new Error(payload?.error?.message || payload?.message || `OpenClaw request failed: ${response.status}`);\n error.statusCode = response.status;\n throw error;\n }\n\n const message = extractOpenClawText(payload);\n if (!message) {\n const error = new Error('OpenClaw returned an empty response');\n error.statusCode = 502;\n throw error;\n }\n\n return {\n message,\n runId: payload.runId || payload.id || payload.result?.runId || null,\n provider: runtime.provider,\n model: runtime.model,\n usage: extractOpenClawUsage(payload),\n };\n}\n\nasync function runAgentModel({ messages, user, conversationId, mode, callbacks = {}, signal, maxCompletionTokens }) {\n const openClawRuntime = createOpenClawRuntime();\n if (openClawRuntime.enabled && !callbacks.onToken) {\n return await callOpenClawAgent({ messages, user, conversationId, mode, signal });\n }\n\n const codexRuntime = createCodexDeepSeekRuntime();\n const libreChatRuntime = createLibreChatAgentRuntime();\n let provider = libreChatRuntime.enabled\n ? libreChatRuntime.provider\n : codexRuntime.enabled\n ? codexRuntime.provider\n : 'deepseek';\n let model = libreChatRuntime.enabled ? libreChatRuntime.model : deepseekModel;\n\n try {\n if (libreChatRuntime.enabled) {\n const result = callbacks.onToken\n ? await callLibreChatAgentStream(messages, { ...callbacks, signal })\n : await callLibreChatAgent(messages);\n return { ...result, provider, model };\n }\n if (codexRuntime.enabled) {\n const result = await callCodexSdkDeepSeek(messages, { ...callbacks, signal });\n return { ...result, provider, model };\n }\n const result = callbacks.onToken\n ? await callDeepSeekStream(messages, { ...callbacks, signal, maxCompletionTokens })\n : await callDeepSeek(messages, { maxCompletionTokens });\n return { ...result, provider, model };\n } catch (error) {\n if (!codexRuntime.enabled || isDeepSeekQuotaError(error)) throw error;\n logger.warn({ error: String(error) }, 'Agent model failed, falling back to DeepSeek direct');\n const result = callbacks.onToken\n ? await callDeepSeekStream(messages, { ...callbacks, signal, maxCompletionTokens })\n : await callDeepSeek(messages, { maxCompletionTokens });\n return { ...result, provider: 'deepseek', model: deepseekModel };\n }\n}\n\nfunction updateAgentRunStatus(run, status, fields = {}) {\n statements.updateAgentRunStatus.run({\n id: run.id,\n userId: run.user_id || run.userId,\n status,\n openClawRunId: fields.openClawRunId ?? null,\n inputTokens: fields.inputTokens ?? null,\n outputTokens: fields.outputTokens ?? null,\n toolCallCount: fields.toolCallCount ?? null,\n completedAt: status === 'completed' || status === 'cancelled' ? nowIso() : null,\n failedAt: status === 'failed' ? nowIso() : null,\n errorCode: fields.errorCode ?? null,\n errorMessage: fields.errorMessage ?? null,\n });\n}\n\nfunction buildAgentMessages(conversationId) {\n const historyRows = statements.listMessages.all(conversationId).slice(-14);\n return [{ role: 'system', content: systemPrompt }, ...normalizeMessages(historyRows)];\n}\n\nfunction agentUsagePayload(usage, user, budget) {\n const promptTokens = usage.promptTokens || usage.inputTokens || 0;\n const completionTokens = usage.completionTokens || usage.outputTokens || 0;\n const totalTokens = usage.totalTokens || promptTokens + completionTokens;\n const latestUsage = usageFor(user.userId);\n return {\n inputTokens: promptTokens,\n outputTokens: completionTokens,\n totalTokens,\n monthlyLimit: budget.limit,\n remainingTokens: Math.max(0, budget.limit - latestUsage.used),\n };\n}\n\nfunction openAiContentToText(content) {\n if (typeof content === 'string') return content;\n if (Array.isArray(content)) {\n return content\n .map((part) => {\n if (typeof part === 'string') return part;\n if (!part || typeof part !== 'object') return '';\n return readText(part.text || part.content || part.value);\n })\n .filter(Boolean)\n .join('\\n');\n }\n return readText(content);\n}\n\nfunction normalizeOpenAiMessages(value) {\n if (!Array.isArray(value)) return [];\n const allowedRoles = new Set(['system', 'user', 'assistant', 'tool']);\n return value\n .map((item) => {\n if (!item || typeof item !== 'object') return null;\n const role = allowedRoles.has(item.role) ? item.role : 'user';\n const content = openAiContentToText(item.content);\n if (!content) return null;\n return { role, content };\n })\n .filter(Boolean);\n}\n\nfunction truncateForDeepSeekMessage(text, maxChars = 12_000) {\n const value = readText(text);\n if (value.length <= maxChars) return value;\n return `${value.slice(0, maxChars)}\\n\\n[内容已截断]`;\n}\n\nfunction truncateTailForDeepSeekMessage(text, maxChars = 2400) {\n const value = readText(text);\n if (value.length <= maxChars) return value;\n return `[前文已省略]\\n\\n${value.slice(-maxChars)}`;\n}\n\nfunction stripOpenClawPromptNoise(text) {\n const value = readText(text);\n if (!value) return '';\n\n const finalPromptMatch = value.match(/finalPromptText[\"'\\s::]+([\\s\\S]{1,4000})$/i);\n if (finalPromptMatch?.[1]) {\n return readText(finalPromptMatch[1].replace(/^[\"'`]+|[\"'`,}\\]]+$/g, ''));\n }\n\n const markers = [\n 'final prompt:',\n 'final user prompt:',\n 'user prompt:',\n '用户问题:',\n '用户消息:',\n '用户输入:',\n ];\n const lower = value.toLowerCase();\n for (const marker of markers) {\n const index = lower.lastIndexOf(marker.toLowerCase());\n if (index >= 0) {\n const tail = value.slice(index + marker.length).trim();\n if (tail) return tail;\n }\n }\n\n return value;\n}\n\nfunction compactOpenClawMessagesForDeepSeek(messages) {\n const conversationalMessages = messages\n .filter((message) => message.role === 'user' || message.role === 'assistant')\n .map((message) => ({\n role: message.role,\n content: truncateTailForDeepSeekMessage(stripOpenClawPromptNoise(message.content)),\n }))\n .filter((message) => message.content);\n\n const recent = conversationalMessages.slice(-6);\n while (recent.length > 0 && recent[0].role !== 'user') {\n recent.shift();\n }\n\n if (recent.length === 0) {\n const fallback = messages\n .slice()\n .reverse()\n .map((message) => truncateTailForDeepSeekMessage(stripOpenClawPromptNoise(message.content), 2400))\n .find(Boolean);\n if (!fallback) return messages;\n recent.push({ role: 'user', content: fallback });\n }\n\n return [\n {\n role: 'system',\n content: '你是大乘 App 的桌面 AI 助手。请直接回答用户问题,默认使用简洁、准确的中文。',\n },\n ...recent,\n ];\n}\n\nfunction isDeepSeekBlockedError(error) {\n return (\n Number(error?.statusCode) === 403 &&\n /blocked|被拦截|forbidden/i.test(String(error?.message || error || ''))\n );\n}\n\nfunction shouldRetryWithCompactedOpenClawMessages(error, messages, compactedMessages) {\n if (!isDeepSeekBlockedError(error)) return false;\n return JSON.stringify(messages) !== JSON.stringify(compactedMessages);\n}\n\nfunction openClawBlockedFallbackText(messages) {\n const lastUser = messages\n .slice()\n .reverse()\n .find((message) => message.role === 'user')?.content || '';\n const localActionIntent =\n /(file|folder|directory|project|workspace|path|文件|文件夹|目录|项目|工程|路径|本地)/i.test(lastUser) &&\n /(create|make|mkdir|write|edit|modify|rename|delete|move|copy|save|download|read|list|生成|创建|新建|写入|编辑|修改|重命名|删除|移动|复制|保存|下载|读取|列出)/i.test(lastUser);\n\n if (localActionIntent) {\n return [\n '本机工具规划被模型服务拦截了,所以这次没有实际读取、创建、修改或下载本机文件。',\n '如果只是新建文件夹,请先在底部选择项目后重新发送“新建文件夹”;桌面端会直接在已选项目里创建。',\n '如果是更复杂的本地文件操作,请在“连应用”里重启本机 AI 后重试,或把要操作的文件路径和目标说得更具体。',\n ].join('\\n');\n }\n\n return '模型服务拦截了这次回复,未能生成结果。请换一种更具体、更短的表述后重试。';\n}\nfunction usageWithFallback(usage, messages, text) {\n const promptTokens = Number(usage?.promptTokens || usage?.inputTokens || 0);\n const completionTokens = Number(usage?.completionTokens || usage?.outputTokens || 0);\n const totalTokens = Number(usage?.totalTokens || 0);\n if (totalTokens > 0) {\n return {\n promptTokens,\n completionTokens,\n totalTokens,\n };\n }\n const estimatedPrompt = promptTokens || estimateTokens(JSON.stringify(messages));\n const estimatedCompletion = completionTokens || estimateTokens(text);\n return {\n promptTokens: estimatedPrompt,\n completionTokens: estimatedCompletion,\n totalTokens: estimatedPrompt + estimatedCompletion,\n };\n}\n\nfunction openAiUsagePayload(usage, user, budget) {\n const promptTokens = Number(usage.promptTokens || 0);\n const completionTokens = Number(usage.completionTokens || 0);\n const totalTokens = Number(usage.totalTokens || promptTokens + completionTokens);\n const latestUsage = usageFor(user.userId);\n const remainingTokens = Math.max(0, budget.limit - latestUsage.used);\n return {\n prompt_tokens: promptTokens,\n completion_tokens: completionTokens,\n total_tokens: totalTokens,\n promptTokens,\n completionTokens,\n totalTokens,\n monthlyLimit: budget.limit,\n remainingTokens,\n };\n}\n\nfunction writeOpenAiSse(res, payload) {\n res.write(`data: ${JSON.stringify(payload)}\\n\\n`);\n}\n\napp.post(\n '/api/openclaw/deepseek/v1/chat/completions',\n asyncHandler(async (req, res) => {\n const user = await resolveUser(req, req.body || {});\n const incomingMessages = normalizeOpenAiMessages(req.body?.messages);\n if (incomingMessages.length === 0) {\n res.status(400).json({ error: { message: 'messages is required' } });\n return;\n }\n\n const messages = compactOpenClawMessagesForDeepSeek(incomingMessages);\n const promptEstimate = estimateTokens(JSON.stringify(messages));\n const estimated = promptEstimate + 600;\n const budget = enforceTokenBudget(user, estimated);\n const maxCompletionTokens = Math.min(\n completionBudgetFor(budget, promptEstimate),\n normalizeMaxCompletionTokens(req.body?.max_tokens),\n );\n const model = normalizeDeepSeekModelName(req.body?.model);\n const wantsStream =\n req.body?.stream === true ||\n String(req.get('accept') || '').includes('text/event-stream');\n const id = `chatcmpl_${crypto.randomUUID().replaceAll('-', '')}`;\n const created = Math.floor(Date.now() / 1000);\n const compactedMessages = messages;\n\n if (!wantsStream) {\n let result;\n let usageMessages = messages;\n try {\n result = await callDeepSeek(messages, { model, maxCompletionTokens });\n } catch (error) {\n if (!shouldRetryWithCompactedOpenClawMessages(error, messages, compactedMessages)) {\n throw error;\n }\n logger.warn(\n {\n statusCode: error?.statusCode,\n messageCount: messages.length,\n compactedMessageCount: compactedMessages.length,\n },\n 'OpenClaw DeepSeek proxy retrying with compacted messages',\n );\n usageMessages = compactedMessages;\n result = await callDeepSeek(compactedMessages, { model, maxCompletionTokens });\n }\n const usage = usageWithFallback(result.usage, usageMessages, result.message);\n recordUsage(user.userId, usage.totalTokens);\n res.json({\n id,\n object: 'chat.completion',\n created,\n model: result.model || model,\n choices: [\n {\n index: 0,\n message: { role: 'assistant', content: result.message },\n finish_reason: 'stop',\n },\n ],\n usage: openAiUsagePayload(usage, user, budget),\n });\n return;\n }\n\n const abortController = new AbortController();\n req.on('close', () => abortController.abort());\n\n res.status(200);\n res.set({\n 'Content-Type': 'text/event-stream; charset=utf-8',\n 'Cache-Control': 'no-cache, no-transform',\n Connection: 'keep-alive',\n 'X-Accel-Buffering': 'no',\n });\n res.flushHeaders?.();\n\n const baseChunk = {\n id,\n object: 'chat.completion.chunk',\n created,\n model,\n system_fingerprint: 'fabushi-openclaw-deepseek-proxy',\n };\n writeOpenAiSse(res, {\n ...baseChunk,\n choices: [{ index: 0, delta: { role: 'assistant' }, finish_reason: null }],\n });\n\n let text = '';\n let usageMessages = messages;\n try {\n let result;\n try {\n result = await callDeepSeekStream(messages, {\n model,\n maxCompletionTokens,\n signal: abortController.signal,\n onToken: (delta) => {\n text += delta;\n writeOpenAiSse(res, {\n ...baseChunk,\n choices: [{ index: 0, delta: { content: delta }, finish_reason: null }],\n });\n },\n });\n } catch (error) {\n if (!shouldRetryWithCompactedOpenClawMessages(error, messages, compactedMessages)) {\n throw error;\n }\n logger.warn(\n {\n statusCode: error?.statusCode,\n messageCount: messages.length,\n compactedMessageCount: compactedMessages.length,\n },\n 'OpenClaw DeepSeek proxy stream retrying with compacted messages',\n );\n text = '';\n usageMessages = compactedMessages;\n result = await callDeepSeekStream(compactedMessages, {\n model,\n maxCompletionTokens,\n signal: abortController.signal,\n onToken: (delta) => {\n text += delta;\n writeOpenAiSse(res, {\n ...baseChunk,\n choices: [{ index: 0, delta: { content: delta }, finish_reason: null }],\n });\n },\n });\n }\n text = result.message || text;\n const usage = usageWithFallback(result.usage, usageMessages, text);\n recordUsage(user.userId, usage.totalTokens);\n writeOpenAiSse(res, {\n ...baseChunk,\n model: result.model || model,\n choices: [{ index: 0, delta: {}, finish_reason: 'stop' }],\n });\n writeOpenAiSse(res, {\n ...baseChunk,\n model: result.model || model,\n choices: [],\n usage: openAiUsagePayload(usage, user, budget),\n });\n res.write('data: [DONE]\\n\\n');\n } catch (error) {\n if (isDeepSeekBlockedError(error)) {\n const fallbackMessage = openClawBlockedFallbackText(usageMessages);\n const usage = usageWithFallback(null, usageMessages, fallbackMessage);\n recordUsage(user.userId, usage.totalTokens);\n logger.warn(\n { statusCode: error?.statusCode, code: error?.code },\n 'OpenClaw DeepSeek proxy stream blocked; returning local fallback message',\n );\n writeOpenAiSse(res, {\n ...baseChunk,\n choices: [{ index: 0, delta: { content: fallbackMessage }, finish_reason: null }],\n });\n writeOpenAiSse(res, {\n ...baseChunk,\n choices: [{ index: 0, delta: {}, finish_reason: 'stop' }],\n });\n writeOpenAiSse(res, {\n ...baseChunk,\n choices: [],\n usage: openAiUsagePayload(usage, user, budget),\n });\n res.write('data: [DONE]\\n\\n');\n return;\n }\n logger.error(\n { error: String(error), code: error?.code, statusCode: error?.statusCode },\n 'OpenClaw DeepSeek proxy stream failed',\n );\n writeOpenAiSse(res, {\n ...baseChunk,\n choices: [],\n error: {\n message: publicAiErrorMessage(error),\n code: error?.code || 'OPENCLAW_DEEPSEEK_PROXY_ERROR',\n },\n });\n res.write('data: [DONE]\\n\\n');\n } finally {\n res.end();\n }\n }),\n);\n\napp.post(\n '/codex-deepseek/v1/responses',\n asyncHandler(async (req, res) => {\n const bearer = bearerToken(req);\n if (!deepseekApiKey || bearer !== deepseekApiKey) {\n res.status(401).json({\n error: { message: 'Unauthorized Codex DeepSeek adapter request' },\n });\n return;\n }\n\n const prompt = codexResponsesPrompt(req.body);\n if (!prompt) {\n res.status(400).json({ error: { message: 'Responses input is required' } });\n return;\n }\n\n const model = String(req.body?.model || deepseekModel);\n const responseId = `resp_${crypto.randomUUID().replaceAll('-', '')}`;\n const itemId = `msg_${crypto.randomUUID().replaceAll('-', '')}`;\n const wantsStream =\n req.body?.stream !== false ||\n String(req.get('accept') || '').includes('text/event-stream');\n const messages = [{ role: 'user', content: prompt }];\n\n if (!wantsStream) {\n const result = await callDeepSeek(messages);\n res.json(\n responsesPayload({\n responseId,\n itemId,\n status: 'completed',\n model,\n text: result.message,\n usage: result.usage,\n }),\n );\n return;\n }\n\n res.status(200);\n res.set({\n 'Content-Type': 'text/event-stream; charset=utf-8',\n 'Cache-Control': 'no-cache, no-transform',\n Connection: 'keep-alive',\n 'X-Accel-Buffering': 'no',\n });\n res.flushHeaders?.();\n\n writeResponsesEvent(res, 'response.created', {\n type: 'response.created',\n response: responsesPayload({\n responseId,\n itemId,\n status: 'in_progress',\n model,\n }),\n });\n writeResponsesEvent(res, 'response.output_item.added', {\n type: 'response.output_item.added',\n output_index: 0,\n item: {\n id: itemId,\n type: 'message',\n role: 'assistant',\n status: 'in_progress',\n content: [],\n },\n });\n writeResponsesEvent(res, 'response.content_part.added', {\n type: 'response.content_part.added',\n item_id: itemId,\n output_index: 0,\n content_index: 0,\n part: { type: 'output_text', text: '' },\n });\n\n let text = '';\n try {\n const result = await callDeepSeekStream(messages, {\n onToken: (delta) => {\n text += delta;\n writeResponsesEvent(res, 'response.output_text.delta', {\n type: 'response.output_text.delta',\n item_id: itemId,\n output_index: 0,\n content_index: 0,\n delta,\n });\n },\n });\n text = result.message || text;\n writeResponsesEvent(res, 'response.output_text.done', {\n type: 'response.output_text.done',\n item_id: itemId,\n output_index: 0,\n content_index: 0,\n text,\n });\n writeResponsesEvent(res, 'response.content_part.done', {\n type: 'response.content_part.done',\n item_id: itemId,\n output_index: 0,\n content_index: 0,\n part: { type: 'output_text', text },\n });\n writeResponsesEvent(res, 'response.output_item.done', {\n type: 'response.output_item.done',\n output_index: 0,\n item: {\n id: itemId,\n type: 'message',\n role: 'assistant',\n status: 'completed',\n content: [{ type: 'output_text', text }],\n },\n });\n writeResponsesEvent(res, 'response.completed', {\n type: 'response.completed',\n response: responsesPayload({\n responseId,\n itemId,\n status: 'completed',\n model,\n text,\n usage: result.usage,\n }),\n });\n res.end();\n } catch (error) {\n writeResponsesEvent(res, 'response.failed', {\n type: 'response.failed',\n response: responsesPayload({\n responseId,\n itemId,\n status: 'failed',\n model,\n text,\n error,\n }),\n });\n res.end();\n }\n }),\n);\n\napp.use(\n '/api/openclaw/runtime/files',\n express.static(openClawRuntimeUpdatesDir, {\n fallthrough: false,\n immutable: true,\n maxAge: '7d',\n setHeaders(res) {\n res.setHeader('X-Content-Type-Options', 'nosniff');\n },\n }),\n);\n\napp.get(\n '/api/openclaw/runtime/manifest',\n asyncHandler(async (req, res) => {\n const platform = readText(req.query.platform);\n const currentVersion = readText(req.query.currentVersion);\n const appVersion = readText(req.query.appVersion);\n\n let manifest;\n try {\n manifest = await readJsonFile(openClawRuntimeManifestPath);\n } catch (error) {\n logger.info(\n { manifestPath: openClawRuntimeManifestPath, error: String(error) },\n 'OpenClaw runtime manifest unavailable',\n );\n return jsonResponse(res, 200, {\n success: true,\n schema: 1,\n updateAvailable: false,\n reason: 'manifest-unavailable',\n platform,\n currentVersion,\n appVersion,\n });\n }\n\n const latest = manifest.latest && typeof manifest.latest === 'object'\n ? manifest.latest\n : manifest;\n const platforms = latest.platforms && typeof latest.platforms === 'object'\n ? latest.platforms\n : {};\n const platformRelease = platform ? platforms[platform] : null;\n const version = readText(latest.version || manifest.version);\n\n if (!platform || !platformRelease || typeof platformRelease !== 'object') {\n return jsonResponse(res, 200, {\n success: true,\n schema: Number(manifest.schema || 1),\n channel: readText(manifest.channel || latest.channel || 'stable'),\n updateAvailable: false,\n reason: platform ? 'platform-unavailable' : 'platform-required',\n platform,\n currentVersion,\n appVersion,\n latestVersion: version,\n });\n }\n\n if (platform.startsWith('macos-') && !openClawRuntimeEnableMacosUpdates) {\n return jsonResponse(res, 200, {\n success: true,\n schema: Number(manifest.schema || 1),\n channel: readText(manifest.channel || latest.channel || 'stable'),\n updateAvailable: false,\n reason: 'macos-runtime-updates-disabled',\n platform,\n currentVersion,\n appVersion,\n latestVersion: version,\n });\n }\n\n const downloadUrl = absolutizeOpenClawRuntimeUrl(\n req,\n platformRelease.archiveUrl || platformRelease.url || platformRelease.fileName,\n );\n const release = {\n version,\n channel: readText(manifest.channel || latest.channel || 'stable'),\n minAppVersion: readText(latest.minAppVersion || platformRelease.minAppVersion),\n notes: readText(latest.notes || platformRelease.notes),\n nodeExecutable: readText(\n platformRelease.nodeExecutable || latest.nodeExecutable || 'node/bin/node',\n ),\n cliEntrypoint: readText(\n platformRelease.cliEntrypoint || latest.cliEntrypoint || 'openclaw/openclaw.mjs',\n ),\n defaultPort: Number(latest.defaultPort || manifest.defaultPort || 18789),\n defaultModel: readText(latest.defaultModel || manifest.defaultModel || 'openclaw/default'),\n defaultModelOverride: readText(latest.defaultModelOverride || manifest.defaultModelOverride),\n gatewayArgs: Array.isArray(latest.gatewayArgs)\n ? latest.gatewayArgs.map((item) => String(item))\n : ['gateway', '--port', '{port}', '--force'],\n archiveUrl: downloadUrl,\n sha256: readText(platformRelease.sha256),\n size: Number(platformRelease.size || 0),\n };\n\n jsonResponse(res, 200, {\n success: true,\n schema: Number(manifest.schema || 1),\n updateAvailable: Boolean(version && downloadUrl && version !== currentVersion),\n platform,\n currentVersion,\n appVersion,\n latest: release,\n generatedAt: readText(manifest.generatedAt || latest.generatedAt),\n });\n }),\n);\n\napp.get('/health', (_req, res) => {\n const codexRuntime = createCodexDeepSeekRuntime();\n const libreChatRuntime = createLibreChatAgentRuntime();\n const openClawRuntime = createOpenClawRuntime();\n jsonResponse(res, 200, {\n status: 'ok',\n service: 'dacheng-ai-backend',\n provider: libreChatRuntime.enabled ? libreChatRuntime.provider : 'deepseek',\n model: libreChatRuntime.enabled ? libreChatRuntime.model : deepseekModel,\n deepseekModel,\n deepseekAvailableModels,\n libreChatAgentChatEnabled: libreChatRuntime.enabled,\n libreChatAgentProvider: libreChatRuntime.provider,\n libreChatAgentReason: libreChatRuntime.enabled ? undefined : libreChatRuntime.reason,\n codexSdkAvailable: true,\n codexSdkChatEnabled: codexRuntime.enabled,\n codexSdkProvider: codexRuntime.provider,\n openClawAgentChatEnabled: openClawRuntime.enabled,\n openClawAgentProvider: openClawRuntime.provider,\n openClawAgentReason: openClawRuntime.enabled ? undefined : openClawRuntime.reason,\n requireMemberForAi,\n memberMonthlyLimit,\n freeMonthlyLimit,\n deepseekMaxCompletionTokens,\n openClawRuntimeUpdatesDir,\n openClawRuntimeManifestConfigured: fs.existsSync(openClawRuntimeManifestPath),\n cbetaApiRoot,\n timestamp: nowIso(),\n });\n});\n\napp.get(\n '/api/miniapps/registry',\n asyncHandler(async (req, res) => {\n const user = await resolveUser(req, { username: req.query.username });\n const official = officialMiniAppRegistry(req);\n const store = await readMiniAppStore();\n const visibleSandbox = store.apps.filter((appRecord) => {\n if (appRecord.reviewStatus === 'approved' && appRecord.source === 'marketplace') {\n return true;\n }\n return appRecord.ownerId === user.userId;\n });\n const bots = [\n ...official.bots,\n ...visibleSandbox.map((record) => miniAppRecordToBot(record)),\n ];\n const miniApps = [\n ...official.miniApps,\n ...visibleSandbox.map((record) => miniAppRecordToManifest(req, record)),\n ];\n const registry = {\n schemaVersion: 1,\n hostApiVersion: miniAppHostApiVersion,\n bots,\n miniApps,\n updatedAt: nowIso(),\n };\n registry.signature = sha256(JSON.stringify({ bots, miniApps }));\n jsonResponse(res, 200, { success: true, registry });\n }),\n);\n\napp.get(\n '/api/miniapps/dev/:miniAppId/index.html',\n asyncHandler(async (req, res) => {\n const miniAppId = safeMiniAppId(req.params.miniAppId);\n const store = await readMiniAppStore();\n const record = store.apps.find((appRecord) => appRecord.miniAppId === miniAppId);\n if (!record) {\n res.status(404).type('text/html').send('<h1>Mini app not found</h1>');\n return;\n }\n res.setHeader('Content-Type', 'text/html; charset=utf-8');\n res.setHeader('Cache-Control', 'no-store');\n res.send(record.sourceHtml || generatedMiniAppHtml(record));\n }),\n);\n\napp.post(\n '/api/miniapps/dev/create',\n asyncHandler(async (req, res) => {\n const user = await resolveUser(req, req.body || {});\n const title = safeUserText(req.body?.title, '个人小程序').slice(0, 80);\n const subtitle = safeUserText(req.body?.subtitle, '个人沙箱小程序').slice(0, 120);\n const permissions = sanitizeMiniAppPermissions(req.body?.permissions);\n const prompt = safeUserText(req.body?.prompt || title);\n const idSeed = crypto.randomUUID().replaceAll('-', '').slice(0, 12);\n const miniAppId = `sandbox.${idSeed}`;\n const botId = `bot.${miniAppId}`;\n const sourceHtml = generatedMiniAppHtml({ title, prompt });\n const scan = miniAppScan(permissions, sourceHtml);\n const createdAt = nowIso();\n const record = {\n ownerId: user.userId,\n miniAppId,\n botId,\n title,\n subtitle,\n initials: title.slice(0, 1) || '小',\n icon: 'code',\n avatarColor: '#3D8BFF',\n greeting: '你好,我是这个个人沙箱小程序的机器人。',\n inputHint: '输入消息,或打开小程序面板',\n version: '0.0.1',\n permissions,\n surfaces: ['chatPanel'],\n theme: 'telegramDark',\n source: 'sandbox',\n reviewStatus: 'sandbox',\n sourceHtml,\n scan,\n createdAt,\n updatedAt: createdAt,\n };\n const store = await readMiniAppStore();\n store.apps.push(record);\n await writeMiniAppStore(store);\n jsonResponse(res, 200, {\n success: true,\n miniApp: miniAppRecordToManifest(req, record),\n bot: miniAppRecordToBot(record),\n scan,\n });\n }),\n);\n\napp.post(\n '/api/miniapps/dev/:miniAppId/version',\n asyncHandler(async (req, res) => {\n const user = await resolveUser(req, req.body || {});\n const miniAppId = safeMiniAppId(req.params.miniAppId);\n const store = await readMiniAppStore();\n const index = store.apps.findIndex((appRecord) => appRecord.miniAppId === miniAppId);\n if (index < 0) {\n return jsonResponse(res, 404, { success: false, message: 'mini app not found' });\n }\n const record = store.apps[index];\n if (record.ownerId !== user.userId) {\n return jsonResponse(res, 403, { success: false, message: 'not your mini app' });\n }\n const title = safeUserText(req.body?.title, record.title).slice(0, 80);\n const subtitle = safeUserText(req.body?.subtitle, record.subtitle).slice(0, 120);\n const sourceHtml =\n safeUserText(req.body?.sourceHtml) ||\n generatedMiniAppHtml({ title, prompt: safeUserText(req.body?.prompt || title) });\n const permissions = sanitizeMiniAppPermissions(req.body?.permissions || record.permissions);\n const scan = miniAppScan(permissions, sourceHtml);\n store.apps[index] = {\n ...record,\n title,\n subtitle,\n sourceHtml,\n permissions,\n scan,\n reviewStatus: 'sandbox',\n version: safeUserText(req.body?.version, record.version || '0.0.1'),\n updatedAt: nowIso(),\n };\n await writeMiniAppStore(store);\n jsonResponse(res, 200, {\n success: true,\n miniApp: miniAppRecordToManifest(req, store.apps[index]),\n bot: miniAppRecordToBot(store.apps[index]),\n scan,\n });\n }),\n);\n\napp.post(\n '/api/miniapps/:miniAppId/submit-review',\n asyncHandler(async (req, res) => {\n const user = await resolveUser(req, req.body || {});\n const miniAppId = safeMiniAppId(req.params.miniAppId);\n const store = await readMiniAppStore();\n const index = store.apps.findIndex((appRecord) => appRecord.miniAppId === miniAppId);\n if (index < 0) {\n return jsonResponse(res, 404, { success: false, message: 'mini app not found' });\n }\n const record = store.apps[index];\n if (record.ownerId !== user.userId) {\n return jsonResponse(res, 403, { success: false, message: 'not your mini app' });\n }\n const scan = miniAppScan(record.permissions || [], record.sourceHtml || '');\n if (!scan.passed) {\n return jsonResponse(res, 400, {\n success: false,\n message: 'security scan failed',\n scan,\n });\n }\n store.apps[index] = {\n ...record,\n scan,\n reviewStatus: 'pending_review',\n updatedAt: nowIso(),\n };\n await writeMiniAppStore(store);\n jsonResponse(res, 200, {\n success: true,\n miniApp: miniAppRecordToManifest(req, store.apps[index]),\n scan,\n });\n }),\n);\n\napp.post(\n '/api/botfather/generate-miniapp',\n asyncHandler(async (req, res) => {\n const user = await resolveUser(req, req.body || {});\n const prompt = safeUserText(req.body?.prompt);\n if (!prompt) {\n return jsonResponse(res, 400, { success: false, message: 'prompt is required' });\n }\n const title = titleFromPromptForMiniApp(prompt);\n const subtitle = '机器人之父生成的个人沙箱小程序';\n const requestedPermissions = Array.isArray(req.body?.permissions) ? req.body.permissions : [];\n const generated = await generateBotFatherMiniAppHtml({ title, prompt, user });\n let sourceHtml = generated.sourceHtml;\n let generation = generated.generation;\n const permissions = sanitizeMiniAppPermissions([\n ...requestedPermissions,\n ...inferMiniAppPermissionsFromText(prompt, sourceHtml),\n ]);\n let scan = miniAppScan(permissions, sourceHtml);\n if (!scan.passed) {\n sourceHtml = generatedMiniAppHtml({ title, prompt });\n generation = {\n provider: 'template',\n fallbackReason: 'ai_output_failed_security_scan',\n };\n scan = miniAppScan(permissions, sourceHtml);\n }\n const idSeed = crypto.randomUUID().replaceAll('-', '').slice(0, 12);\n const miniAppId = `sandbox.${idSeed}`;\n const createdAt = nowIso();\n const record = {\n ownerId: user.userId,\n miniAppId,\n botId: `bot.${miniAppId}`,\n title,\n subtitle,\n initials: title.slice(0, 1) || '小',\n icon: 'code',\n avatarColor: '#3D8BFF',\n greeting: `我是「${title}」的机器人。`,\n inputHint: '继续描述修改需求,或打开小程序面板',\n version: '0.0.1',\n permissions,\n surfaces: ['chatPanel'],\n theme: 'telegramDark',\n source: 'sandbox',\n reviewStatus: 'sandbox',\n sourceHtml,\n scan,\n generation,\n prompt,\n createdAt,\n updatedAt: createdAt,\n };\n const store = await readMiniAppStore();\n store.apps.push(record);\n await writeMiniAppStore(store);\n jsonResponse(res, 200, {\n success: true,\n miniApp: miniAppRecordToManifest(req, record),\n bot: miniAppRecordToBot(record),\n scan,\n generation,\n actions: ['openMiniApp', 'continueEditing', 'submitReview'],\n });\n }),\n);\n\napp.get('/api/ai/models', (_req, res) => {\n jsonResponse(res, 200, {\n success: true,\n provider: 'deepseek',\n defaultModel: deepseekModel,\n models: deepseekAvailableModels.map((model) => ({\n id: model,\n label: model === 'deepseek-reasoner' ? 'DeepSeek Reasoner' : 'DeepSeek Chat',\n })),\n });\n});\napp.get(\n '/api/ai/quota',\n asyncHandler(async (req, res) => {\n const user = await resolveUser(req, { username: req.query.username });\n const budget = enforceTokenBudget(user, 0);\n const usage = usageFor(user.userId);\n jsonResponse(res, 200, {\n success: true,\n isMember: user.isMember,\n monthlyLimit: budget.limit,\n usedTokens: usage.used,\n remainingTokens: Math.max(0, budget.limit - usage.used),\n month: usage.month,\n });\n }),\n);\n\napp.post(\n '/api/agent/chat',\n asyncHandler(async (req, res) => {\n const message = safeUserText(req.body?.message);\n if (!message) {\n return jsonResponse(res, 400, { success: false, message: 'message is required' });\n }\n\n const user = await resolveUser(req, req.body || {});\n const promptEstimate = estimateTokens(message);\n const estimated = promptEstimate + 600;\n const budget = enforceTokenBudget(user, estimated);\n const maxCompletionTokens = completionBudgetFor(budget, promptEstimate);\n const createdAt = nowIso();\n const conversationId = safeUserText(req.body?.conversationId) || crypto.randomUUID();\n const mode = normalizeAgentMode(req.body?.mode);\n const stream = req.body?.stream === true;\n let conversation = statements.getConversation.get(conversationId, user.userId);\n const isNewConversation = !conversation;\n\n if (isNewConversation) {\n conversation = { id: conversationId, user_id: user.userId, title: titleFromMessage(message) };\n statements.insertConversation.run({\n id: conversationId,\n userId: user.userId,\n title: conversation.title,\n provider: 'agent',\n model: openClawAgentId,\n createdAt,\n updatedAt: createdAt,\n });\n }\n\n const messageId = safeUserText(req.body?.messageId) || crypto.randomUUID();\n statements.insertMessage.run({\n id: messageId,\n conversationId,\n role: 'user',\n content: message,\n promptTokens: 0,\n completionTokens: 0,\n totalTokens: 0,\n createdAt,\n });\n\n const runId = `run_${crypto.randomUUID().replaceAll('-', '')}`;\n const sessionKey = openClawSessionKey(user, conversationId);\n const openClawRuntime = createOpenClawRuntime();\n statements.insertAgentRun.run({\n id: runId,\n userId: user.userId,\n conversationId,\n messageId,\n openClawRunId: null,\n openClawSessionKey: sessionKey,\n status: stream ? 'queued' : 'running',\n mode,\n provider: openClawRuntime.enabled ? openClawRuntime.provider : 'dacheng-ai-fallback',\n model: openClawRuntime.enabled ? openClawAgentId : deepseekModel,\n startedAt: createdAt,\n });\n\n if (stream) {\n return jsonResponse(res, 200, {\n success: true,\n runId,\n conversationId,\n streamUrl: `/api/agent/runs/${runId}/events`,\n });\n }\n\n const controller = new AbortController();\n activeAgentRuns.set(runId, controller);\n try {\n const skillResult = await runResourceFinderSkill({ message, user });\n const skillContext = resourceContextMessage(skillResult);\n const historyRows = statements.listMessages.all(conversationId).slice(-14);\n const modelMessages = [\n { role: 'system', content: systemPrompt },\n ...(skillContext ? [{ role: 'system', content: skillContext }] : []),\n ...normalizeMessages(historyRows),\n ];\n\n const aiResult = await runAgentModel({\n messages: modelMessages,\n user,\n conversationId,\n mode,\n signal: controller.signal,\n maxCompletionTokens,\n });\n const usage = {\n promptTokens: aiResult.usage?.promptTokens || estimateTokens(JSON.stringify(modelMessages)),\n completionTokens: aiResult.usage?.completionTokens || estimateTokens(aiResult.message),\n totalTokens:\n aiResult.usage?.totalTokens ||\n estimateTokens(JSON.stringify(modelMessages)) + estimateTokens(aiResult.message),\n };\n recordUsage(user.userId, usage.totalTokens);\n const answeredAt = nowIso();\n const assistantMessageId = crypto.randomUUID();\n statements.insertMessage.run({\n id: assistantMessageId,\n conversationId,\n role: 'assistant',\n content: aiResult.message,\n promptTokens: usage.promptTokens,\n completionTokens: usage.completionTokens,\n totalTokens: usage.totalTokens,\n createdAt: answeredAt,\n });\n statements.updateConversation.run({\n id: conversationId,\n userId: user.userId,\n title: isNewConversation ? titleFromMessage(message) : conversation.title,\n updatedAt: answeredAt,\n });\n updateAgentRunStatus({ id: runId, user_id: user.userId }, 'completed', {\n openClawRunId: aiResult.runId,\n inputTokens: usage.promptTokens,\n outputTokens: usage.completionTokens,\n });\n const action = determineClientAction(skillResult, message);\n return jsonResponse(res, 200, {\n success: true,\n runId,\n conversationId,\n messageId: assistantMessageId,\n provider: aiResult.provider,\n model: aiResult.model,\n message: aiResult.message,\n usage: agentUsagePayload(usage, user, budget),\n ...(action ? { clientAction: action } : {}),\n });\n } catch (error) {\n const status = error.name === 'AbortError' ? 'cancelled' : 'failed';\n updateAgentRunStatus({ id: runId, user_id: user.userId }, status, {\n errorCode: error.name || 'AGENT_RUN_FAILED',\n errorMessage: publicAiErrorMessage(error),\n });\n if (status === 'cancelled') {\n return jsonResponse(res, 499, { success: false, runId, status: 'cancelled' });\n }\n throw error;\n } finally {\n activeAgentRuns.delete(runId);\n }\n }),\n);\n\napp.get(\n '/api/agent/runs/:runId/events',\n asyncHandler(async (req, res) => {\n const user = await resolveUser(req, {});\n const run = statements.getAgentRun.get(req.params.runId, user.userId);\n if (!run) {\n return jsonResponse(res, 404, { success: false, message: 'run not found' });\n }\n if (run.status === 'completed' || run.status === 'failed' || run.status === 'cancelled') {\n return jsonResponse(res, 409, { success: false, message: `run is already ${run.status}` });\n }\n\n sseHeaders(res);\n const controller = new AbortController();\n activeAgentRuns.set(run.id, controller);\n updateAgentRunStatus(run, 'running');\n writeSse(res, 'run.started', { runId: run.id, conversationId: run.conversation_id });\n\n try {\n let streamedText = '';\n const modelMessages = buildAgentMessages(run.conversation_id);\n const lastUserMsg = [...modelMessages].reverse().find(m => m.role === 'user');\n const userMsgText = lastUserMsg ? lastUserMsg.content : '';\n const skillResult = userMsgText ? await runResourceFinderSkill({\n message: userMsgText,\n user,\n onStep: (step) => writeSse(res, 'tool.call.completed', {\n tool: 'agent.step',\n displayName: step.title || 'Agent step',\n summary: step.message || '',\n }),\n }) : null;\n const skillContext = resourceContextMessage(skillResult);\n const finalMessages = skillContext ? [\n modelMessages[0],\n { role: 'system', content: skillContext },\n ...modelMessages.slice(1)\n ] : modelMessages;\n\n const promptEstimate = estimateTokens(JSON.stringify(finalMessages));\n const estimated = promptEstimate + 600;\n const budget = enforceTokenBudget(user, estimated);\n const maxCompletionTokens = completionBudgetFor(budget, promptEstimate);\n const aiResult = await runAgentModel({\n messages: finalMessages,\n user,\n conversationId: run.conversation_id,\n mode: run.mode,\n signal: controller.signal,\n maxCompletionTokens,\n callbacks: {\n onStep: (step) => writeSse(res, 'tool.call.completed', {\n tool: 'agent.step',\n displayName: step.title || 'Agent step',\n summary: step.message || '',\n }),\n onToken: (delta) => {\n streamedText += delta;\n writeSse(res, 'assistant.delta', { text: delta });\n },\n },\n });\n const message = aiResult.message || streamedText.trim();\n const usage = {\n promptTokens: aiResult.usage?.promptTokens || estimateTokens(JSON.stringify(finalMessages)),\n completionTokens: aiResult.usage?.completionTokens || estimateTokens(message),\n totalTokens:\n aiResult.usage?.totalTokens || estimateTokens(JSON.stringify(finalMessages)) + estimateTokens(message),\n };\n recordUsage(user.userId, usage.totalTokens);\n const assistantMessageId = crypto.randomUUID();\n const answeredAt = nowIso();\n statements.insertMessage.run({\n id: assistantMessageId,\n conversationId: run.conversation_id,\n role: 'assistant',\n content: message,\n promptTokens: usage.promptTokens,\n completionTokens: usage.completionTokens,\n totalTokens: usage.totalTokens,\n createdAt: answeredAt,\n });\n statements.updateConversation.run({\n id: run.conversation_id,\n userId: user.userId,\n title: titleFromMessage(message),\n updatedAt: answeredAt,\n });\n updateAgentRunStatus(run, 'completed', {\n openClawRunId: aiResult.runId,\n inputTokens: usage.promptTokens,\n outputTokens: usage.completionTokens,\n });\n const action = determineClientAction(skillResult, userMsgText);\n writeSse(res, 'assistant.message', { messageId: assistantMessageId, content: message });\n writeSse(res, 'run.completed', {\n usage: agentUsagePayload(usage, user, budget),\n ...(action ? { clientAction: action } : {}),\n });\n res.end();\n } catch (error) {\n const status = error.name === 'AbortError' ? 'cancelled' : 'failed';\n updateAgentRunStatus(run, status, {\n errorCode: error.name || 'AGENT_RUN_FAILED',\n errorMessage: publicAiErrorMessage(error),\n });\n writeSse(res, status === 'cancelled' ? 'run.cancelled' : 'run.failed', {\n runId: run.id,\n message: publicAiErrorMessage(error),\n });\n res.end();\n } finally {\n activeAgentRuns.delete(run.id);\n }\n }),\n);\n\napp.post(\n '/api/agent/runs/:runId/cancel',\n asyncHandler(async (req, res) => {\n const user = await resolveUser(req, req.body || {});\n const run = statements.getAgentRun.get(req.params.runId, user.userId);\n if (!run) {\n return jsonResponse(res, 404, { success: false, message: 'run not found' });\n }\n const controller = activeAgentRuns.get(run.id);\n if (controller) controller.abort();\n updateAgentRunStatus(run, 'cancelled');\n return jsonResponse(res, 200, { success: true, runId: run.id, status: 'cancelled' });\n }),\n);\n\napp.post(\n '/api/agent/messages/:messageId/feedback',\n asyncHandler(async (req, res) => {\n const user = await resolveUser(req, req.body || {});\n const rating = safeUserText(req.body?.rating);\n if (!['up', 'down'].includes(rating)) {\n return jsonResponse(res, 400, { success: false, message: 'rating must be up or down' });\n }\n statements.insertAgentFeedback.run({\n id: crypto.randomUUID(),\n messageId: req.params.messageId,\n userId: user.userId,\n rating,\n reason: safeUserText(req.body?.reason),\n comment: safeUserText(req.body?.comment),\n createdAt: nowIso(),\n });\n return jsonResponse(res, 200, { success: true });\n }),\n);\n\napp.post(\n '/api/ai/chat',\n asyncHandler(async (req, res) => {\n const message = safeUserText(req.body?.message);\n if (!message) {\n return jsonResponse(res, 400, { success: false, message: 'message is required' });\n }\n\n const user = await resolveUser(req, req.body || {});\n const promptEstimate = estimateTokens(message);\n const estimated = promptEstimate + 600;\n const budget = enforceTokenBudget(user, estimated);\n const maxCompletionTokens = completionBudgetFor(budget, promptEstimate);\n const createdAt = nowIso();\n const conversationId = safeUserText(req.body?.conversationId) || crypto.randomUUID();\n const requestedModel = normalizeDeepSeekModelName(req.body?.model);\n const clientContext = clientContextMessage(req.body?.client);\n let conversation = statements.getConversation.get(conversationId, user.userId);\n const isNewConversation = !conversation;\n\n if (isNewConversation) {\n conversation = {\n id: conversationId,\n user_id: user.userId,\n title: titleFromMessage(message),\n };\n statements.insertConversation.run({\n id: conversationId,\n userId: user.userId,\n title: conversation.title,\n provider: 'deepseek',\n model: requestedModel,\n createdAt,\n updatedAt: createdAt,\n });\n }\n\n statements.insertMessage.run({\n id: crypto.randomUUID(),\n conversationId,\n role: 'user',\n content: message,\n promptTokens: 0,\n completionTokens: 0,\n totalTokens: 0,\n createdAt,\n });\n\n const libreChatRuntime = createLibreChatAgentRuntime();\n const skillResult = await runResourceFinderSkill({ message, user });\n const skillContext = resourceContextMessage(skillResult);\n const historyRows = statements.listMessages.all(conversationId).slice(-14);\n const modelMessages = [\n { role: 'system', content: systemPrompt },\n ...(clientContext ? [{ role: 'system', content: clientContext }] : []),\n ...(skillContext ? [{ role: 'system', content: skillContext }] : []),\n ...normalizeMessages(historyRows),\n ];\n\n const openClawRuntime = createOpenClawRuntime();\n const codexRuntime = createCodexDeepSeekRuntime();\n let provider = openClawRuntime.enabled\n ? openClawRuntime.provider\n : libreChatRuntime.enabled\n ? libreChatRuntime.provider\n : 'deepseek';\n let responseModel = openClawRuntime.enabled\n ? openClawRuntime.model\n : libreChatRuntime.enabled\n ? libreChatRuntime.model\n : requestedModel;\n let aiResult;\n try {\n if (openClawRuntime.enabled) {\n aiResult = await callOpenClawAgent({\n messages: modelMessages,\n user,\n conversationId,\n mode: normalizeAgentMode(req.body?.mode),\n });\n provider = aiResult.provider || provider;\n responseModel = aiResult.model || responseModel;\n } else if (libreChatRuntime.enabled) {\n aiResult = await callLibreChatAgent(modelMessages);\n } else if (codexRuntime.enabled) {\n provider = codexRuntime.provider;\n aiResult = await callCodexSdkDeepSeek(modelMessages);\n } else {\n aiResult = await callDeepSeek(modelMessages, { model: requestedModel, maxCompletionTokens });\n }\n } catch (error) {\n if (libreChatRuntime.enabled) {\n logger.warn({ error: String(error) }, 'LibreChat Agent chat failed, falling back to Codex SDK or DeepSeek direct');\n provider = codexRuntime.enabled ? codexRuntime.provider : 'deepseek';\n responseModel = requestedModel;\n if (codexRuntime.enabled) {\n try {\n aiResult = await callCodexSdkDeepSeek(modelMessages);\n } catch (codexError) {\n if (isDeepSeekQuotaError(codexError)) throw codexError;\n logger.warn({ error: String(codexError) }, 'Codex SDK chat failed, falling back to DeepSeek direct');\n provider = 'deepseek';\n aiResult = await callDeepSeek(modelMessages, { model: requestedModel, maxCompletionTokens });\n }\n } else {\n aiResult = await callDeepSeek(modelMessages, { model: requestedModel, maxCompletionTokens });\n }\n } else {\n if (!codexRuntime.enabled || isDeepSeekQuotaError(error)) throw error;\n logger.warn({ error: String(error) }, 'Codex SDK chat failed, falling back to DeepSeek direct');\n provider = 'deepseek';\n responseModel = requestedModel;\n aiResult = await callDeepSeek(modelMessages, { model: requestedModel, maxCompletionTokens });\n }\n }\n const usage = {\n promptTokens: aiResult.usage.promptTokens || estimateTokens(JSON.stringify(modelMessages)),\n completionTokens: aiResult.usage.completionTokens || estimateTokens(aiResult.message),\n totalTokens:\n aiResult.usage.totalTokens ||\n estimateTokens(JSON.stringify(modelMessages)) + estimateTokens(aiResult.message),\n };\n recordUsage(user.userId, usage.totalTokens);\n\n const answeredAt = nowIso();\n statements.insertMessage.run({\n id: crypto.randomUUID(),\n conversationId,\n role: 'assistant',\n content: aiResult.message,\n promptTokens: usage.promptTokens,\n completionTokens: usage.completionTokens,\n totalTokens: usage.totalTokens,\n createdAt: answeredAt,\n });\n statements.updateConversation.run({\n id: conversationId,\n userId: user.userId,\n title: isNewConversation ? titleFromMessage(message) : conversation.title,\n updatedAt: answeredAt,\n });\n\n const latestUsage = usageFor(user.userId);\n const monthlyLimit = budget.limit;\n const action = determineClientAction(skillResult, message);\n jsonResponse(res, 200, {\n success: true,\n conversationId,\n provider,\n model: responseModel,\n message: aiResult.message,\n usage: {\n ...usage,\n monthlyLimit,\n remainingTokens: Math.max(0, monthlyLimit - latestUsage.used),\n },\n ...(action ? { clientAction: action } : {}),\n });\n }),\n);\n\napp.post('/api/ai/chat/stream', async (req, res) => {\n sseHeaders(res);\n\n try {\n const message = safeUserText(req.body?.message);\n if (!message) {\n writeSse(res, 'error', { message: 'message is required' });\n res.end();\n return;\n }\n\n const user = await resolveUser(req, req.body || {});\n const promptEstimate = estimateTokens(message);\n const estimated = promptEstimate + 600;\n const budget = enforceTokenBudget(user, estimated);\n const maxCompletionTokens = completionBudgetFor(budget, promptEstimate);\n const createdAt = nowIso();\n const conversationId = safeUserText(req.body?.conversationId) || crypto.randomUUID();\n const requestedModel = normalizeDeepSeekModelName(req.body?.model);\n const clientContext = clientContextMessage(req.body?.client);\n let conversation = statements.getConversation.get(conversationId, user.userId);\n const isNewConversation = !conversation;\n\n if (isNewConversation) {\n conversation = {\n id: conversationId,\n user_id: user.userId,\n title: titleFromMessage(message),\n };\n statements.insertConversation.run({\n id: conversationId,\n userId: user.userId,\n title: conversation.title,\n provider: 'deepseek',\n model: requestedModel,\n createdAt,\n updatedAt: createdAt,\n });\n }\n\n statements.insertMessage.run({\n id: crypto.randomUUID(),\n conversationId,\n role: 'user',\n content: message,\n promptTokens: 0,\n completionTokens: 0,\n totalTokens: 0,\n createdAt,\n });\n\n const codexRuntime = createCodexDeepSeekRuntime();\n const libreChatRuntime = createLibreChatAgentRuntime();\n const initialOpenClawRuntime = createOpenClawRuntime();\n let provider = initialOpenClawRuntime.enabled\n ? initialOpenClawRuntime.provider\n : libreChatRuntime.enabled\n ? libreChatRuntime.provider\n : codexRuntime.enabled\n ? codexRuntime.provider\n : 'deepseek';\n let responseModel = initialOpenClawRuntime.enabled\n ? initialOpenClawRuntime.model\n : libreChatRuntime.enabled\n ? libreChatRuntime.model\n : requestedModel;\n writeSse(res, 'meta', {\n conversationId,\n provider,\n model: responseModel,\n });\n const skillResult = await runResourceFinderSkill({\n message,\n user,\n onStep: (step) => writeSse(res, 'step', step),\n });\n const skillContext = resourceContextMessage(skillResult);\n const historyRows = statements.listMessages.all(conversationId).slice(-14);\n const modelMessages = [\n { role: 'system', content: systemPrompt },\n ...(clientContext ? [{ role: 'system', content: clientContext }] : []),\n ...(skillContext ? [{ role: 'system', content: skillContext }] : []),\n ...normalizeMessages(historyRows),\n ];\n\n let aiResult;\n const openClawRuntime = initialOpenClawRuntime;\n try {\n if (openClawRuntime.enabled) {\n aiResult = await callOpenClawAgent({\n messages: modelMessages,\n user,\n conversationId,\n mode: normalizeAgentMode(req.body?.mode),\n });\n provider = aiResult.provider || provider;\n responseModel = aiResult.model || responseModel;\n writeSse(res, 'delta', { text: aiResult.message });\n } else if (libreChatRuntime.enabled) {\n aiResult = await callLibreChatAgentStream(modelMessages, {\n onToken: (token) => writeSse(res, 'delta', { text: token }),\n });\n } else if (codexRuntime.enabled) {\n aiResult = await callCodexSdkDeepSeek(modelMessages, {\n onToken: (token) => writeSse(res, 'delta', { text: token }),\n onStep: (step) => writeSse(res, 'step', step),\n });\n } else {\n aiResult = await callDeepSeekStream(modelMessages, {\n model: requestedModel,\n maxCompletionTokens,\n onToken: (token) => writeSse(res, 'delta', { text: token }),\n });\n }\n } catch (error) {\n if (libreChatRuntime.enabled) {\n logger.warn({ error: String(error) }, 'LibreChat Agent streaming chat failed, falling back to Codex SDK or DeepSeek direct');\n provider = codexRuntime.enabled ? codexRuntime.provider : 'deepseek';\n responseModel = requestedModel;\n writeSse(res, 'meta', {\n conversationId,\n provider,\n model: responseModel,\n });\n if (codexRuntime.enabled) {\n try {\n aiResult = await callCodexSdkDeepSeek(modelMessages, {\n onToken: (token) => writeSse(res, 'delta', { text: token }),\n onStep: (step) => writeSse(res, 'step', step),\n });\n } catch (codexError) {\n if (isDeepSeekQuotaError(codexError)) throw codexError;\n logger.warn({ error: String(codexError) }, 'Codex SDK streaming chat failed, falling back to DeepSeek direct');\n provider = 'deepseek';\n responseModel = requestedModel;\n writeSse(res, 'meta', {\n conversationId,\n provider,\n model: responseModel,\n });\n aiResult = await callDeepSeekStream(modelMessages, {\n model: requestedModel,\n maxCompletionTokens,\n onToken: (token) => writeSse(res, 'delta', { text: token }),\n });\n }\n } else {\n aiResult = await callDeepSeekStream(modelMessages, {\n model: requestedModel,\n maxCompletionTokens,\n onToken: (token) => writeSse(res, 'delta', { text: token }),\n });\n }\n } else {\n if (!codexRuntime.enabled || isDeepSeekQuotaError(error)) throw error;\n logger.warn({ error: String(error) }, 'Codex SDK streaming chat failed, falling back to DeepSeek direct');\n provider = 'deepseek';\n responseModel = requestedModel;\n writeSse(res, 'meta', {\n conversationId,\n provider,\n model: responseModel,\n });\n aiResult = await callDeepSeekStream(modelMessages, {\n model: requestedModel,\n maxCompletionTokens,\n onToken: (token) => writeSse(res, 'delta', { text: token }),\n });\n }\n }\n\n const usage = {\n promptTokens: aiResult.usage.promptTokens || estimateTokens(JSON.stringify(modelMessages)),\n completionTokens: aiResult.usage.completionTokens || estimateTokens(aiResult.message),\n totalTokens:\n aiResult.usage.totalTokens ||\n estimateTokens(JSON.stringify(modelMessages)) + estimateTokens(aiResult.message),\n };\n recordUsage(user.userId, usage.totalTokens);\n\n const answeredAt = nowIso();\n statements.insertMessage.run({\n id: crypto.randomUUID(),\n conversationId,\n role: 'assistant',\n content: aiResult.message,\n promptTokens: usage.promptTokens,\n completionTokens: usage.completionTokens,\n totalTokens: usage.totalTokens,\n createdAt: answeredAt,\n });\n statements.updateConversation.run({\n id: conversationId,\n userId: user.userId,\n title: isNewConversation ? titleFromMessage(message) : conversation.title,\n updatedAt: answeredAt,\n });\n\n const latestUsage = usageFor(user.userId);\n const monthlyLimit = budget.limit;\n const action = determineClientAction(skillResult, message);\n writeSse(res, 'done', {\n conversationId,\n provider,\n model: responseModel,\n message: aiResult.message,\n usage: {\n ...usage,\n monthlyLimit,\n remainingTokens: Math.max(0, monthlyLimit - latestUsage.used),\n },\n ...(action ? { clientAction: action } : {}),\n });\n res.end();\n } catch (error) {\n logger.error({ error: error.stack || String(error) }, 'streaming chat failed');\n writeSse(res, 'error', {\n message: publicAiErrorMessage(error),\n details: error.details,\n });\n res.end();\n }\n});\n\napp.get(\n '/api/ai/conversations',\n asyncHandler(async (req, res) => {\n const user = await resolveUser(req, { username: req.query.username });\n const items = statements.listConversations.all(user.userId).map((item) => ({\n id: item.id,\n title: item.title,\n updatedAt: item.updatedAt,\n }));\n jsonResponse(res, 200, { success: true, items });\n }),\n);\n\napp.get(\n '/api/ai/conversations/:id',\n asyncHandler(async (req, res) => {\n const user = await resolveUser(req);\n const conversation = statements.getConversation.get(req.params.id, user.userId);\n if (!conversation) {\n return jsonResponse(res, 404, { success: false, message: 'conversation not found' });\n }\n const messages = statements.listMessages.all(req.params.id).map((item) => ({\n role: item.role,\n content: item.content,\n createdAt: item.createdAt,\n }));\n jsonResponse(res, 200, {\n success: true,\n conversation: {\n id: conversation.id,\n title: conversation.title,\n updatedAt: conversation.updated_at,\n },\n messages,\n });\n }),\n);\n\nfunction buildUrl(base, endpoint, params = {}) {\n const url = new URL(endpoint, `${base}/`);\n for (const [key, value] of Object.entries(params)) {\n if (Array.isArray(value)) {\n for (const item of value) {\n if (item !== undefined && item !== null && item !== '') {\n url.searchParams.append(key, String(item));\n }\n }\n } else if (value !== undefined && value !== null && value !== '') {\n url.searchParams.set(key, String(value));\n }\n }\n return url;\n}\n\nasync function fetchJson(url) {\n const response = await fetch(url, {\n headers: { Accept: 'application/json' },\n signal: AbortSignal.timeout(15_000),\n });\n if (!response.ok) {\n throw new Error(`Request failed: ${response.status}`);\n }\n return response.json();\n}\n\nfunction stripHtml(value) {\n return String(value || '')\n .replace(/<script[\\s\\S]*?<\\/script>/gi, ' ')\n .replace(/<style[\\s\\S]*?<\\/style>/gi, ' ')\n .replace(/<[^>]+>/g, ' ')\n .replace(/ /g, ' ')\n .replace(/</g, '<')\n .replace(/>/g, '>')\n .replace(/&/g, '&')\n .replace(/&#(\\d+);/g, (_match, code) => String.fromCharCode(Number(code)))\n .replace(/\\s+/g, ' ')\n .trim();\n}\n\nfunction cleanExtractedText(value) {\n let text = String(value || '')\n .replace(/\\b[A-Z]{1,3}\\d{2}n\\d{4,5}[a-z]?_p?\\d{4}[a-z]\\d{2}\\b/g, ' ')\n .replace(/\\uFFFD/g, '')\n .replace(/\\s+([,。;:!?、」』)》】])/g, '$1')\n .replace(/([「『《(【])\\s+/g, '$1');\n for (let i = 0; i < 3; i += 1) {\n text = text.replace(/([\\u3400-\\u9fff])\\s+([\\u3400-\\u9fff])/g, '$1$2');\n }\n return text.replace(/[ \\t]{2,}/g, ' ').trim();\n}\n\nfunction compactText(value, limit = 140) {\n const text = stripHtml(value);\n return text.length > limit ? `${text.slice(0, limit)}...` : text;\n}\n\nfunction collectSearchResults(data) {\n if (!data || typeof data !== 'object') return [];\n if (Array.isArray(data.results)) return data.results;\n if (Array.isArray(data?.results?.docs)) return data.results.docs;\n if (Array.isArray(data?.response?.docs)) return data.response.docs;\n if (Array.isArray(data.docs)) return data.docs;\n return [];\n}\n\nfunction firstText(item, keys) {\n for (const key of keys) {\n const value = item?.[key];\n if (Array.isArray(value)) {\n const first = value.map((entry) => safeUserText(String(entry))).find(Boolean);\n if (first) return first;\n }\n if (typeof value === 'string' && value.trim()) return value.trim();\n if (typeof value === 'number') return String(value);\n }\n return '';\n}\n\nfunction firstNumber(item, keys, fallback = undefined) {\n for (const key of keys) {\n const value = item?.[key];\n if (typeof value === 'number') return value;\n if (typeof value === 'string' && value.trim() && Number.isFinite(Number(value))) {\n return Number(value);\n }\n }\n return fallback;\n}\n\nasync function searchCbetaTitle(query, limit) {\n const url = buildUrl(cbetaApiRoot, 'search/title', { q: query, start: 0, rows: limit });\n const data = await fetchJson(url);\n return collectSearchResults(data).flatMap((item) => {\n const work = firstText(item, ['work', 'work_id', 'workId']);\n const title = firstText(item, ['content', 'title', 'work_title', 'workTitle']);\n if (!work || !title) return [];\n const juan = firstNumber(item, ['juan', 'juan_num', 'juanNum'], 1);\n return [\n {\n id: `cbeta-title-${work}-${juan}`,\n title,\n sourceName: 'CBETA 佛典',\n url: `cbeta:${work}:${juan}`,\n snippet: firstText(item, ['byline', 'creators', 'time_dynasty']) || '可下载经文正文并用于全球法布施。',\n resourceType: 'scripture',\n work,\n juan,\n },\n ];\n });\n}\n\nasync function searchCbetaContent(query, limit) {\n const endpoints = ['search/fulltext', 'search/content', 'search/all_in_one'];\n const settled = await Promise.allSettled(\n endpoints.map((endpoint) =>\n fetchJson(buildUrl(cbetaApiRoot, endpoint, { q: query, start: 0, rows: limit, field: 'content' })),\n ),\n );\n const results = [];\n for (const response of settled) {\n if (response.status !== 'fulfilled') continue;\n for (const item of collectSearchResults(response.value)) {\n const work = firstText(item, ['work', 'work_id', 'workId', 'sutra', 'id']);\n const snippet = compactText(firstText(item, ['kwic', 'highlight', 'snippet', 'content', 'text', 'body', 'p']));\n if (!work || !snippet) continue;\n const juan = firstNumber(item, ['juan', 'juan_num', 'juanNum', '卷'], 1);\n const title = firstText(item, ['title', 'work_title', 'workTitle', 'sutra_name', 'book', 'name']) || work;\n results.push({\n id: `cbeta-content-${work}-${juan}-${sha256(snippet).slice(0, 8)}`,\n title,\n sourceName: 'CBETA 佛典',\n url: `cbeta:${work}:${juan}`,\n snippet,\n resourceType: 'scripture',\n work,\n juan,\n });\n if (results.length >= limit) return results;\n }\n }\n return results;\n}\n\nfunction directUrlResource(query) {\n const url = safeUserText(query);\n let parsed;\n try {\n parsed = new URL(url);\n } catch {\n return null;\n }\n if (!['http:', 'https:'].includes(parsed.protocol)) return null;\n return {\n id: `url-${sha256(url).slice(0, 12)}`,\n title: parsed.hostname,\n sourceName: '网页资源',\n url,\n snippet: '直接下载并提取这个链接的可读正文。',\n resourceType: 'web',\n };\n}\n\nfunction looksLikeAudioQuery(query) {\n return /音乐|音樂|音频|音訊|mp3|audio|念诵|念誦|诵经|誦經|唱诵|唱誦|读诵|讀誦/.test(\n safeUserText(query).toLowerCase(),\n );\n}\n\nfunction archiveSearchTerm(query) {\n return (\n safeUserText(query)\n .replace(/下载|查找|寻找|找一?个|资源|音频|音訊|音乐|音樂|mp3|audio|念诵|念誦|诵经|誦經|唱诵|唱誦|读诵|讀誦/gi, ' ')\n .replace(/\\s+/g, ' ')\n .trim() || safeUserText(query)\n );\n}\n\nfunction solrPhrase(value) {\n return String(value).replace(/\\\\/g, '\\\\\\\\').replace(/\"/g, '\\\\\"');\n}\n\nasync function searchInternetArchiveAudio(query, limit) {\n if (!looksLikeAudioQuery(query)) return [];\n const term = archiveSearchTerm(query);\n if (!term) return [];\n const variants = Array.from(\n new Set([\n term,\n term.replaceAll('金刚经', '金剛經'),\n term.replaceAll('金剛經', '金刚经'),\n ].filter(Boolean)),\n );\n const phraseQuery = variants\n .flatMap((item) => [`title:\"${solrPhrase(item)}\"`, `description:\"${solrPhrase(item)}\"`])\n .join(' OR ');\n const url = buildUrl('https://archive.org', 'advancedsearch.php', {\n q: `(${phraseQuery}) AND mediatype:audio`,\n rows: Math.min(Math.max(limit, 1), 10),\n output: 'json',\n 'fl[]': ['identifier', 'title', 'description'],\n });\n const data = await fetchJson(url);\n const docs = Array.isArray(data?.response?.docs) ? data.response.docs : [];\n return docs\n .map((item) => {\n const identifier = firstText(item, ['identifier']);\n const title = firstText(item, ['title']) || identifier;\n if (!identifier || !title) return null;\n return {\n id: `ia-audio-${identifier}`,\n title,\n sourceName: 'Internet Archive 音频',\n url: `ia:${identifier}`,\n snippet: compactText(firstText(item, ['description'])) || '公开可访问的音频资源,将下载第一个可用 MP3 文件。',\n resourceType: 'audio',\n identifier,\n };\n })\n .filter(Boolean);\n}\n\nconst curatedCbetaWorks = [\n {\n work: 'T0235',\n juan: 1,\n title: '金剛般若波羅蜜經',\n keywords: ['金剛般若波羅蜜經', '金刚般若波罗蜜经', '金剛經', '金刚经'],\n snippet: '姚秦 鳩摩羅什譯。常用于读诵、共修与法布施的般若经典。',\n },\n {\n work: 'T0251',\n juan: 1,\n title: '般若波羅蜜多心經',\n keywords: ['般若波羅蜜多心經', '般若波罗蜜多心经', '心經', '心经'],\n snippet: '唐 玄奘譯。篇幅短,适合加入禅室功课本与日常持诵。',\n },\n {\n work: 'T0262',\n juan: 1,\n title: '妙法蓮華經',\n keywords: ['妙法蓮華經', '妙法莲华经', '法華經', '法华经'],\n snippet: '姚秦 鳩摩羅什譯。可按卷下载正文,用于长期功课与分享。',\n },\n {\n work: 'T0279',\n juan: 1,\n title: '大方廣佛華嚴經',\n keywords: ['大方廣佛華嚴經', '大方广佛华严经', '華嚴經', '华严经'],\n snippet: '唐 實叉難陀譯。可按卷下载正文,用于系统研读与法布施。',\n },\n {\n work: 'T0360',\n juan: 1,\n title: '佛說無量壽經',\n keywords: ['佛說無量壽經', '佛说无量寿经', '無量壽經', '无量寿经'],\n snippet: '净土经典。可下载正文,用于净土功课本与全球法布施。',\n },\n {\n work: 'T0365',\n juan: 1,\n title: '佛說觀無量壽佛經',\n keywords: ['佛說觀無量壽佛經', '佛说观无量寿佛经', '觀無量壽經', '观无量寿经'],\n snippet: '净土三经之一。适合查找下载后加入净土功课。',\n },\n {\n work: 'T0366',\n juan: 1,\n title: '佛說阿彌陀經',\n keywords: ['佛說阿彌陀經', '佛说阿弥陀经', '阿彌陀經', '阿弥陀经'],\n snippet: '姚秦 鳩摩羅什譯。篇幅适中,适合共修、持诵与分享。',\n },\n {\n work: 'T0475',\n juan: 1,\n title: '維摩詰所說經',\n keywords: ['維摩詰所說經', '维摩诘所说经', '維摩詰經', '维摩诘经'],\n snippet: '姚秦 鳩摩羅什譯。大乘经典,可按卷下载用于研读。',\n },\n {\n work: 'T0676',\n juan: 1,\n title: '解深密經',\n keywords: ['解深密經', '解深密经'],\n snippet: '唯识相关大乘经典。适合研读、整理笔记与法布施。',\n },\n {\n work: 'T1666',\n juan: 1,\n title: '大乘起信論',\n keywords: ['大乘起信論', '大乘起信论', '起信論', '起信论'],\n snippet: '大乘论典。适合下载后作为研习功课资料。',\n },\n];\n\nfunction localCbetaResults(query, limit) {\n const normalizedQuery = safeUserText(query).toLowerCase();\n if (!normalizedQuery) return [];\n return curatedCbetaWorks\n .filter((item) =>\n item.keywords.some((keyword) => {\n const normalizedKeyword = keyword.toLowerCase();\n return normalizedKeyword.includes(normalizedQuery) || normalizedQuery.includes(normalizedKeyword);\n }),\n )\n .slice(0, limit)\n .map((item) => ({\n id: `cbeta-curated-${item.work}-${item.juan}`,\n title: item.title,\n sourceName: 'CBETA 佛典',\n url: `cbeta:${item.work}:${item.juan}`,\n snippet: item.snippet,\n resourceType: 'scripture',\n work: item.work,\n juan: item.juan,\n }));\n}\n\nfunction uniqueResources(items, limit) {\n const seen = new Set();\n const results = [];\n for (const item of items) {\n const key = `${item.url}:${item.title}`;\n if (seen.has(key)) continue;\n seen.add(key);\n results.push(item);\n if (results.length >= limit) break;\n }\n return results;\n}\n\nasync function findResourceCandidates(query, limit) {\n const direct = directUrlResource(query);\n const results = [];\n if (direct) results.push(direct);\n try {\n results.push(...(await searchInternetArchiveAudio(query, limit)));\n } catch (error) {\n logger.warn({ error: String(error), query }, 'Internet Archive audio search skipped');\n }\n results.push(...localCbetaResults(query, limit));\n\n const [titleResults, contentResults] = await Promise.allSettled([\n searchCbetaTitle(query, limit),\n searchCbetaContent(query, limit),\n ]);\n if (titleResults.status === 'fulfilled') results.push(...titleResults.value);\n if (contentResults.status === 'fulfilled') results.push(...contentResults.value);\n\n return uniqueResources(results, limit);\n}\n\nfunction looksLikeResourceTask(message) {\n const text = safeUserText(message).toLowerCase();\n if (!text) return false;\n return /下载|查找|寻找|找一?个|资源|經文|经文|佛经|佛經|仪轨|儀軌|音乐|音樂|mp3|audio|念诵|念誦|诵经|誦經/.test(text);\n}\n\nfunction extractResourceQuery(message) {\n const text = safeUserText(message)\n .replace(/请|請|帮我|幫我|麻烦|麻煩|可以|能不能|需要|我要|想要/g, ' ')\n .replace(/下载|下載|查找|寻找|尋找|找一个|找一個|找|资源|資源|并|並|然后|然後|用于|用於|全球法布施|加入功课本|加入功課本/g, ' ')\n .replace(/\\s+/g, ' ')\n .trim();\n return text || safeUserText(message);\n}\n\nfunction resourceContextMessage(skillResult) {\n if (!skillResult) return '';\n const lines = [\n '本轮资源检索/下载已经由大乘 App 后端完成。',\n '不要再次调用 search_dharma_resources、download_dharma_resource 或其他资源 MCP 工具;请直接基于以下结果回答用户。',\n `后端已调用 skill: ${skillResult.skillName}`,\n `执行目标: ${skillResult.query}`,\n ];\n if (skillResult.selected) {\n lines.push(`候选资源: ${skillResult.selected.title}`);\n lines.push(`来源: ${skillResult.selected.sourceName}`);\n lines.push(`资源地址: ${skillResult.selected.url}`);\n }\n if (skillResult.downloaded) {\n lines.push(`下载标题: ${skillResult.downloaded.title}`);\n lines.push(`下载文件: ${skillResult.downloaded.fileName}`);\n const text = safeUserText(skillResult.downloaded.contentText);\n if (text) {\n lines.push('下载正文摘录:');\n lines.push(text.slice(0, 8000));\n }\n }\n return lines.join('\\n');\n}\n\nfunction determineClientAction(skillResult, message) {\n if (skillResult && skillResult.downloaded) {\n const text = safeUserText(message);\n if (/法布施|全球法布施|分享/.test(text)) {\n return {\n type: 'prepare_dharma_share_text',\n title: skillResult.downloaded.title,\n text: skillResult.downloaded.contentText || '',\n };\n }\n }\n return null;\n}\n\nasync function runResourceFinderSkill({ message, user, onStep }) {\n if (!looksLikeResourceTask(message)) return null;\n\n const skillName = 'resource-finder-downloader';\n const query = extractResourceQuery(message);\n const audioTask = looksLikeAudioQuery(message) || looksLikeAudioQuery(query);\n onStep?.({\n type: 'skill.step',\n skillName,\n stage: 'start',\n title: '调用资源查找下载 skill',\n message: `识别到资源任务: ${query}`,\n });\n\n onStep?.({\n type: 'skill.step',\n skillName,\n stage: 'search',\n title: '搜索可分享资源',\n message: audioTask\n ? '优先检索公开可访问音频资源,失败时再回落到佛典正文资源。'\n : '优先检索 CBETA 佛典正文接口,搜索索引不可用时使用内置经典映射。',\n });\n const candidates = await findResourceCandidates(query, 8);\n if (candidates.length === 0) {\n onStep?.({\n type: 'skill.step',\n skillName,\n stage: 'empty',\n title: '没有找到可下载资源',\n message: '后端没有找到明确可下载且可分享的资源。',\n });\n return {\n skillName,\n query,\n candidates: [],\n selected: null,\n downloaded: null,\n };\n }\n\n const selected = candidates[0];\n onStep?.({\n type: 'skill.step',\n skillName,\n stage: 'select',\n title: '选择可信候选资源',\n message: `${selected.title} / ${selected.sourceName}`,\n });\n\n onStep?.({\n type: 'skill.step',\n skillName,\n stage: 'download',\n title: '下载并提取资源',\n message: selected.url,\n });\n const cbeta = parseCbetaResource(selected);\n const internetArchive = parseInternetArchiveResource(selected);\n const downloaded = cbeta\n ? await downloadCbetaResource({ ...selected, ...cbeta })\n : internetArchive\n ? await downloadInternetArchiveAudio({ ...selected, ...internetArchive })\n : await downloadWebResource(selected);\n const persisted = persistDownloadedResource(user, downloaded);\n\n onStep?.({\n type: 'skill.step',\n skillName,\n stage: 'done',\n title: '资源已准备完成',\n message: downloaded.fileName,\n });\n\n return {\n skillName,\n query,\n candidates,\n selected,\n downloaded: {\n ...downloaded,\n downloadId: persisted.id,\n },\n };\n}\n\napp.post(\n '/api/resources/search',\n asyncHandler(async (req, res) => {\n const query = safeUserText(req.body?.query);\n const limit = Math.min(Math.max(Number(req.body?.limit || 12), 1), 20);\n if (query.length < 2) {\n return jsonResponse(res, 400, { success: false, message: 'query is required' });\n }\n\n const items = await findResourceCandidates(query, limit);\n\n jsonResponse(res, 200, {\n success: true,\n source: 'resource-index',\n items,\n });\n }),\n);\n\nfunction parseCbetaResource(input) {\n const work = safeUserText(input.work);\n const juan = Number(input.juan || 1);\n if (work) return { work, juan: Number.isFinite(juan) ? juan : 1 };\n\n const url = safeUserText(input.url);\n const match = /^cbeta:([^:]+):(\\d+)$/i.exec(url);\n if (!match) return null;\n return {\n work: match[1].toUpperCase(),\n juan: Number(match[2] || 1),\n };\n}\n\nfunction parseInternetArchiveResource(input) {\n const url = safeUserText(input.url);\n const match = /^ia:([^:]+)$/i.exec(url);\n const identifier = match?.[1] || safeUserText(input.identifier);\n if (!identifier) return null;\n return { identifier };\n}\n\nfunction safeFileName(value) {\n const normalized = String(value || 'dharma-resource')\n .replace(/[\\\\/:*?\"<>|]+/g, '-')\n .replace(/\\s+/g, '-')\n .replace(/-+/g, '-')\n .replace(/^-|-$/g, '')\n .slice(0, 80);\n return normalized || 'dharma-resource';\n}\n\nfunction extractCbetaHtml(data) {\n const first = Array.isArray(data?.results) ? data.results[0] : null;\n if (typeof first === 'string') return first;\n if (first && typeof first === 'object' && typeof first.html === 'string') return first.html;\n if (typeof data?.html === 'string') return data.html;\n return '';\n}\n\nasync function downloadCbetaResource(resource) {\n const url = buildUrl(cbetaApiRoot, 'juans', {\n work: resource.work,\n juan: resource.juan,\n work_info: 1,\n toc: 1,\n });\n const data = await fetchJson(url);\n const workInfo = data.work_info || {};\n const title = safeUserText(workInfo.title) || safeUserText(resource.title) || resource.work;\n const html = extractCbetaHtml(data);\n const text = cleanExtractedText(stripHtml(html));\n if (!text) throw new Error('CBETA resource returned empty content');\n return {\n title,\n sourceName: 'CBETA 佛典',\n url: `cbeta:${resource.work}:${resource.juan}`,\n contentText: text.slice(0, maxResourceTextChars),\n fileName: `${resource.work}_${resource.juan}_${safeFileName(title)}.txt`,\n };\n}\n\nasync function downloadInternetArchiveAudio(resource) {\n const identifier = safeUserText(resource.identifier);\n if (!identifier) throw new Error('Internet Archive identifier is required');\n const metadata = await fetchJson(`https://archive.org/metadata/${encodeURIComponent(identifier)}`);\n const title = safeUserText(metadata?.metadata?.title) || safeUserText(resource.title) || identifier;\n const files = Array.isArray(metadata?.files) ? metadata.files : [];\n const audioFile = files.find((file) => {\n const name = safeUserText(file?.name);\n const format = safeUserText(file?.format).toLowerCase();\n return /\\.(mp3|m4a|ogg)$/i.test(name) || format.includes('mp3');\n });\n if (!audioFile?.name) {\n throw new Error('Internet Archive audio item has no downloadable audio file');\n }\n const sourceFileName = audioFile.name;\n const fileUrl = `https://archive.org/download/${encodeURIComponent(identifier)}/${sourceFileName\n .split('/')\n .map((part) => encodeURIComponent(part))\n .join('/')}`;\n const response = await fetch(fileUrl, {\n headers: {\n Accept: 'audio/mpeg, audio/*;q=0.9, */*;q=0.5',\n 'User-Agent': 'DachengResourceDownloader/1.0 (+https://ombhrum.com)',\n },\n signal: AbortSignal.timeout(60_000),\n });\n if (!response.ok) {\n throw new Error(`Internet Archive audio download failed: ${response.status}`);\n }\n const binaryContent = Buffer.from(await response.arrayBuffer());\n if (!binaryContent.length) throw new Error('Internet Archive audio returned empty content');\n const sizeMb = (binaryContent.length / 1024 / 1024).toFixed(1);\n return {\n title,\n sourceName: 'Internet Archive 音频',\n url: fileUrl,\n contentText: `已下载音频资源: ${title}\\n文件名: ${sourceFileName}\\n大小: ${sizeMb} MB\\n来源: ${fileUrl}`,\n binaryContent,\n fileName: safeFileName(sourceFileName),\n };\n}\n\nasync function downloadWebResource(input) {\n const url = safeUserText(input.url);\n const parsed = new URL(url);\n if (!['http:', 'https:'].includes(parsed.protocol)) {\n throw new Error('Only http/https resources can be downloaded');\n }\n\n const response = await fetch(parsed, {\n headers: {\n Accept: 'text/html, text/plain, application/xhtml+xml, application/xml;q=0.8, */*;q=0.5',\n 'User-Agent': 'DachengResourceDownloader/1.0 (+https://ombhrum.com)',\n },\n signal: AbortSignal.timeout(25_000),\n });\n if (!response.ok) throw new Error(`Resource download failed: ${response.status}`);\n const contentType = response.headers.get('content-type') || '';\n const body = await response.text();\n const title = safeUserText(input.title) || parsed.hostname;\n const contentText = contentType.includes('html') ? cleanExtractedText(stripHtml(body)) : body.trim();\n if (!contentText) throw new Error('Resource returned empty content');\n return {\n title,\n sourceName: safeUserText(input.sourceName) || parsed.hostname,\n url,\n contentText: contentText.slice(0, maxResourceTextChars),\n fileName: `${safeFileName(title)}.txt`,\n };\n}\n\nfunction persistDownloadedResource(user, content) {\n const id = crypto.randomUUID();\n const fileName = `${id}-${safeFileName(content.fileName)}`;\n const filePath = path.join(resourcesDir, fileName);\n if (content.binaryContent) {\n fs.writeFileSync(filePath, content.binaryContent);\n } else {\n fs.writeFileSync(filePath, content.contentText, 'utf8');\n }\n statements.insertResourceDownload.run({\n id,\n userId: user.userId,\n title: content.title,\n sourceName: content.sourceName,\n sourceUrl: content.url,\n fileName: content.fileName,\n filePath,\n createdAt: nowIso(),\n });\n return { id, fileName, filePath };\n}\n\nfunction createLibreChatMcpServer() {\n const server = new McpServer({\n name: 'dacheng-ai-tools',\n version: '0.1.0',\n });\n\n server.registerTool(\n 'search_dharma_resources',\n {\n title: 'Search dharma resources',\n description:\n 'Search for legally shareable Buddhist scripture, text, web, or audio resources that can be prepared for Fabushi sharing.',\n inputSchema: {\n query: z.string().min(2).max(200),\n limit: z.number().int().min(1).max(12).optional(),\n },\n outputSchema: {\n query: z.string(),\n items: z.array(\n z.object({\n id: z.string(),\n title: z.string(),\n sourceName: z.string(),\n url: z.string(),\n snippet: z.string(),\n resourceType: z.string(),\n work: z.string().optional(),\n juan: z.number().optional(),\n }),\n ),\n },\n annotations: {\n readOnlyHint: true,\n destructiveHint: false,\n openWorldHint: true,\n },\n },\n async ({ query, limit }) => {\n const items = await findResourceCandidates(query, limit ?? 8);\n return textToolResult(`Found ${items.length} candidate resources for \"${query}\".`, {\n query,\n items,\n });\n },\n );\n\n server.registerTool(\n 'download_dharma_resource',\n {\n title: 'Download dharma resource',\n description:\n 'Download and extract a selected resource. Use the url/work/juan returned by search_dharma_resources whenever possible.',\n inputSchema: {\n url: z.string().min(1).max(1000),\n title: z.string().max(300).optional(),\n sourceName: z.string().max(120).optional(),\n work: z.string().max(40).optional(),\n juan: z.number().int().min(1).optional(),\n identifier: z.string().max(300).optional(),\n },\n outputSchema: {\n title: z.string(),\n sourceName: z.string(),\n url: z.string(),\n fileName: z.string(),\n downloadId: z.string(),\n contentText: z.string(),\n },\n annotations: {\n readOnlyHint: false,\n destructiveHint: false,\n openWorldHint: true,\n },\n },\n async (input) => {\n const user = { userId: 'librechat:mcp' };\n const cbeta = parseCbetaResource(input);\n const internetArchive = parseInternetArchiveResource(input);\n const content = cbeta\n ? await downloadCbetaResource({ ...input, ...cbeta })\n : internetArchive\n ? await downloadInternetArchiveAudio({ ...input, ...internetArchive })\n : await downloadWebResource(input);\n const persisted = persistDownloadedResource(user, content);\n const { binaryContent: _binaryContent, ...publicContent } = content;\n return textToolResult(`Downloaded ${publicContent.title} as ${publicContent.fileName}.`, {\n ...publicContent,\n downloadId: persisted.id,\n });\n },\n );\n\n server.registerTool(\n 'prepare_dharma_share_text',\n {\n title: 'Prepare dharma sharing text',\n description:\n 'Prepare a text payload for the Fabushi app to confirm and add to global dharma sharing. This returns a clientAction; it does not start sending.',\n inputSchema: {\n title: z.string().min(1).max(120),\n text: z.string().min(1).max(20000),\n },\n outputSchema: {\n clientAction: z.object({\n type: z.literal('prepare_dharma_share_text'),\n title: z.string(),\n text: z.string(),\n }),\n },\n annotations: {\n readOnlyHint: false,\n destructiveHint: false,\n openWorldHint: false,\n },\n },\n async ({ title, text }) =>\n textToolResult(`Prepared \"${title}\" for user confirmation in the Fabushi app.`, {\n clientAction: {\n type: 'prepare_dharma_share_text',\n title: safeUserText(title).slice(0, 120),\n text: String(text || '').trim().slice(0, 20000),\n },\n }),\n );\n\n server.registerTool(\n 'prepare_practice_book_item',\n {\n title: 'Prepare practice book item',\n description:\n 'Prepare a scripture, chant, or practice item for the Fabushi app practice book. This returns a clientAction; it does not mutate the app by itself.',\n inputSchema: {\n title: z.string().min(1).max(120),\n sourceName: z.string().max(120).optional(),\n text: z.string().min(1).max(20000),\n },\n outputSchema: {\n clientAction: z.object({\n type: z.literal('prepare_practice_book_item'),\n title: z.string(),\n sourceName: z.string(),\n text: z.string(),\n }),\n },\n annotations: {\n readOnlyHint: false,\n destructiveHint: false,\n openWorldHint: false,\n },\n },\n async ({ title, sourceName, text }) =>\n textToolResult(`Prepared \"${title}\" for the Fabushi practice book confirmation flow.`, {\n clientAction: {\n type: 'prepare_practice_book_item',\n title: safeUserText(title).slice(0, 120),\n sourceName: safeUserText(sourceName || '大乘 AI').slice(0, 120),\n text: String(text || '').trim().slice(0, 20000),\n },\n }),\n );\n\n return server;\n}\n\nasync function handleMcpRequest(req, res) {\n const server = createLibreChatMcpServer();\n const transport = new StreamableHTTPServerTransport({\n sessionIdGenerator: undefined,\n enableJsonResponse: true,\n });\n\n res.on('close', () => {\n transport.close();\n server.close();\n });\n\n await server.connect(transport);\n await transport.handleRequest(req, res, req.body);\n}\n\napp.all('/mcp', async (req, res) => {\n try {\n await handleMcpRequest(req, res);\n } catch (error) {\n logger.error({ error: error.stack || String(error) }, 'MCP request failed');\n if (!res.headersSent) {\n res.status(500).json({\n error: 'MCP request failed',\n message: publicAiErrorMessage(error),\n });\n }\n }\n});\n\napp.post(\n '/api/resources/download',\n asyncHandler(async (req, res) => {\n const user = await resolveUser(req, req.body || {});\n const cbeta = parseCbetaResource(req.body || {});\n const internetArchive = parseInternetArchiveResource(req.body || {});\n const content = cbeta\n ? await downloadCbetaResource({ ...req.body, ...cbeta })\n : internetArchive\n ? await downloadInternetArchiveAudio({ ...req.body, ...internetArchive })\n : await downloadWebResource(req.body || {});\n const persisted = persistDownloadedResource(user, content);\n const { binaryContent: _binaryContent, ...publicContent } = content;\n jsonResponse(res, 200, {\n success: true,\n downloadId: persisted.id,\n ...publicContent,\n });\n }),\n);\n\napp.post(\n '/api/codex/resource-task',\n asyncHandler(async (req, res) => {\n if (process.env.ENABLE_CODEX_SDK !== 'true') {\n return jsonResponse(res, 403, {\n success: false,\n message: 'Codex SDK task runner is disabled. Set ENABLE_CODEX_SDK=true on the server to enable it.',\n });\n }\n const prompt = safeUserText(req.body?.prompt);\n if (!prompt) {\n return jsonResponse(res, 400, { success: false, message: 'prompt is required' });\n }\n const { Codex } = await import('@openai/codex-sdk');\n const codexRuntime = createCodexDeepSeekRuntime();\n const codex = new Codex(codexRuntime.enabled ? codexRuntime.options : {});\n const thread = codex.startThread(\n codexRuntime.enabled\n ? codexRuntime.threadOptions\n : { skipGitRepoCheck: true, networkAccessEnabled: true },\n );\n const result = await thread.run(prompt);\n jsonResponse(res, 200, {\n success: true,\n threadId: thread.id,\n provider: codexRuntime.provider,\n model: deepseekModel,\n result,\n });\n }),\n);\n\nregisterPlatformApi({ app, db, resolveUser, asyncHandler });\n\napp.use((error, _req, res, _next) => {\n const status = Number(error.statusCode || error.status || 500);\n const safeStatus = status >= 400 && status < 600 ? status : 500;\n logger.error({ error: error.stack || String(error) }, 'request failed');\n jsonResponse(res, safeStatus, {\n success: false,\n message: error.message || 'Internal Server Error',\n details: error.details,\n });\n});\n\nconst port = Number(process.env.PORT || 8788);\napp.listen(port, '0.0.0.0', () => {\n logger.info(\n {\n port,\n dbPath,\n dataDir,\n deepseekModel,\n cbetaApiRoot,\n },\n 'Dacheng AI backend started',\n );\n});\n"}, "90a9f7458c39b3a13e16455cfbcc9604f8a35b76": {"t": 1785757966.099721, "n": 146082, "h": "c2b2311aead88025", "c": "import crypto from 'node:crypto';\nimport fs from 'node:fs';\nimport path from 'node:path';\nimport process from 'node:process';\nimport { fileURLToPath } from 'node:url';\n\nimport { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';\nimport { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';\nimport Database from 'better-sqlite3';\nimport cors from 'cors';\nimport dotenv from 'dotenv';\nimport express from 'express';\nimport rateLimit from 'express-rate-limit';\nimport helmet from 'helmet';\nimport pino from 'pino';\nimport { z } from 'zod';\n\nimport { registerPlatformApi } from './platform_api.js';\n\ndotenv.config();\n\nconst __dirname = path.dirname(fileURLToPath(import.meta.url));\nconst rootDir = path.resolve(__dirname, '..');\nconst dataDir = process.env.DATA_DIR || path.join(rootDir, 'data');\nconst resourcesDir = path.join(dataDir, 'resources');\nconst miniAppsDir = path.join(dataDir, 'miniapps');\nconst miniAppsStorePath = path.join(miniAppsDir, 'sandbox-miniapps.json');\nconst openClawRuntimeUpdatesDir =\n process.env.OPENCLAW_RUNTIME_UPDATES_DIR || path.join(dataDir, 'openclaw-runtime');\nconst openClawRuntimeEnableMacosUpdates =\n process.env.OPENCLAW_RUNTIME_ENABLE_MACOS_UPDATES === 'true';\nconst codexHomeDir = process.env.CODEX_HOME || path.join(dataDir, 'codex-home');\nconst codexTempDir = process.env.CODEX_TMPDIR || path.join(dataDir, 'codex-tmp');\nconst codexRuntimeDir = process.env.XDG_RUNTIME_DIR || path.join(dataDir, 'codex-runtime');\nconst dbPath = process.env.SQLITE_PATH || path.join(dataDir, 'dacheng-ai.sqlite');\n\nfs.mkdirSync(resourcesDir, { recursive: true });\nfs.mkdirSync(miniAppsDir, { recursive: true });\nfs.mkdirSync(openClawRuntimeUpdatesDir, { recursive: true });\nfs.mkdirSync(codexHomeDir, { recursive: true });\nfs.mkdirSync(codexTempDir, { recursive: true });\nfs.mkdirSync(codexRuntimeDir, { recursive: true });\n\nconst logger = pino({\n level: process.env.LOG_LEVEL || 'info',\n});\n\nconst app = express();\napp.disable('x-powered-by');\napp.set('trust proxy', Number(process.env.TRUST_PROXY_HOPS || 1));\napp.use(helmet());\nconst corsOrigin =\n !process.env.CORS_ORIGIN || process.env.CORS_ORIGIN.trim() === '*'\n ? true\n : process.env.CORS_ORIGIN.split(',').map((item) => item.trim()).filter(Boolean);\napp.use(cors({ origin: corsOrigin }));\napp.use(express.json({ limit: '1mb' }));\napp.use(\n rateLimit({\n windowMs: 60_000,\n limit: Number(process.env.RATE_LIMIT_PER_MINUTE || 90),\n standardHeaders: true,\n legacyHeaders: false,\n }),\n);\n\nconst db = new Database(dbPath);\ndb.pragma('journal_mode = WAL');\ndb.pragma('foreign_keys = ON');\n\ndb.exec(`\n CREATE TABLE IF NOT EXISTS conversations (\n id TEXT PRIMARY KEY,\n user_id TEXT NOT NULL,\n title TEXT NOT NULL,\n provider TEXT NOT NULL,\n model TEXT NOT NULL,\n created_at TEXT NOT NULL,\n updated_at TEXT NOT NULL\n );\n\n CREATE INDEX IF NOT EXISTS idx_conversations_user_updated\n ON conversations (user_id, updated_at DESC);\n\n CREATE TABLE IF NOT EXISTS messages (\n id TEXT PRIMARY KEY,\n conversation_id TEXT NOT NULL,\n role TEXT NOT NULL,\n content TEXT NOT NULL,\n prompt_tokens INTEGER NOT NULL DEFAULT 0,\n completion_tokens INTEGER NOT NULL DEFAULT 0,\n total_tokens INTEGER NOT NULL DEFAULT 0,\n created_at TEXT NOT NULL,\n FOREIGN KEY (conversation_id) REFERENCES conversations(id) ON DELETE CASCADE\n );\n\n CREATE INDEX IF NOT EXISTS idx_messages_conversation_created\n ON messages (conversation_id, created_at ASC);\n\n CREATE TABLE IF NOT EXISTS usage_months (\n user_id TEXT NOT NULL,\n month TEXT NOT NULL,\n total_tokens INTEGER NOT NULL DEFAULT 0,\n updated_at TEXT NOT NULL,\n PRIMARY KEY (user_id, month)\n );\n\n CREATE TABLE IF NOT EXISTS resource_downloads (\n id TEXT PRIMARY KEY,\n user_id TEXT NOT NULL,\n title TEXT NOT NULL,\n source_name TEXT NOT NULL,\n source_url TEXT NOT NULL,\n file_name TEXT NOT NULL,\n file_path TEXT NOT NULL,\n created_at TEXT NOT NULL\n );\n\n CREATE TABLE IF NOT EXISTS agent_runs (\n id TEXT PRIMARY KEY,\n user_id TEXT NOT NULL,\n conversation_id TEXT NOT NULL,\n message_id TEXT NOT NULL,\n openclaw_run_id TEXT,\n openclaw_session_key TEXT NOT NULL,\n status TEXT NOT NULL,\n mode TEXT NOT NULL,\n provider TEXT NOT NULL,\n model TEXT,\n input_tokens INTEGER NOT NULL DEFAULT 0,\n output_tokens INTEGER NOT NULL DEFAULT 0,\n tool_call_count INTEGER NOT NULL DEFAULT 0,\n cost_microusd INTEGER NOT NULL DEFAULT 0,\n started_at TEXT NOT NULL,\n completed_at TEXT,\n failed_at TEXT,\n error_code TEXT,\n error_message TEXT\n );\n\n CREATE INDEX IF NOT EXISTS idx_agent_runs_user_started\n ON agent_runs (user_id, started_at DESC);\n\n CREATE INDEX IF NOT EXISTS idx_agent_runs_status_started\n ON agent_runs (status, started_at DESC);\n\n CREATE TABLE IF NOT EXISTS agent_message_feedback (\n id TEXT PRIMARY KEY,\n message_id TEXT NOT NULL,\n user_id TEXT NOT NULL,\n rating TEXT NOT NULL,\n reason TEXT,\n comment TEXT,\n created_at TEXT NOT NULL\n );\n`);\n\nconst statements = {\n insertConversation: db.prepare(`\n INSERT INTO conversations (id, user_id, title, provider, model, created_at, updated_at)\n VALUES (@id, @userId, @title, @provider, @model, @createdAt, @updatedAt)\n `),\n updateConversation: db.prepare(`\n UPDATE conversations\n SET title = @title, updated_at = @updatedAt\n WHERE id = @id AND user_id = @userId\n `),\n getConversation: db.prepare(`\n SELECT * FROM conversations WHERE id = ? AND user_id = ? LIMIT 1\n `),\n listConversations: db.prepare(`\n SELECT id, title, updated_at AS updatedAt\n FROM conversations\n WHERE user_id = ?\n ORDER BY updated_at DESC\n LIMIT 30\n `),\n insertMessage: db.prepare(`\n INSERT INTO messages (\n id, conversation_id, role, content,\n prompt_tokens, completion_tokens, total_tokens, created_at\n )\n VALUES (\n @id, @conversationId, @role, @content,\n @promptTokens, @completionTokens, @totalTokens, @createdAt\n )\n `),\n listMessages: db.prepare(`\n SELECT role, content, prompt_tokens AS promptTokens,\n completion_tokens AS completionTokens, total_tokens AS totalTokens,\n created_at AS createdAt\n FROM messages\n WHERE conversation_id = ?\n ORDER BY created_at ASC\n `),\n getUsage: db.prepare(`\n SELECT total_tokens AS totalTokens FROM usage_months WHERE user_id = ? AND month = ?\n `),\n upsertUsage: db.prepare(`\n INSERT INTO usage_months (user_id, month, total_tokens, updated_at)\n VALUES (@userId, @month, @totalTokens, @updatedAt)\n ON CONFLICT(user_id, month)\n DO UPDATE SET total_tokens = usage_months.total_tokens + excluded.total_tokens,\n updated_at = excluded.updated_at\n `),\n insertResourceDownload: db.prepare(`\n INSERT INTO resource_downloads (\n id, user_id, title, source_name, source_url, file_name, file_path, created_at\n )\n VALUES (\n @id, @userId, @title, @sourceName, @sourceUrl, @fileName, @filePath, @createdAt\n )\n `),\n insertAgentRun: db.prepare(`\n INSERT INTO agent_runs (\n id, user_id, conversation_id, message_id, openclaw_run_id, openclaw_session_key,\n status, mode, provider, model, started_at\n )\n VALUES (\n @id, @userId, @conversationId, @messageId, @openClawRunId, @openClawSessionKey,\n @status, @mode, @provider, @model, @startedAt\n )\n `),\n getAgentRun: db.prepare(`\n SELECT * FROM agent_runs WHERE id = ? AND user_id = ? LIMIT 1\n `),\n updateAgentRunStatus: db.prepare(`\n UPDATE agent_runs\n SET status = @status,\n openclaw_run_id = COALESCE(@openClawRunId, openclaw_run_id),\n input_tokens = COALESCE(@inputTokens, input_tokens),\n output_tokens = COALESCE(@outputTokens, output_tokens),\n tool_call_count = COALESCE(@toolCallCount, tool_call_count),\n completed_at = @completedAt,\n failed_at = @failedAt,\n error_code = @errorCode,\n error_message = @errorMessage\n WHERE id = @id AND user_id = @userId\n `),\n insertAgentFeedback: db.prepare(`\n INSERT INTO agent_message_feedback (id, message_id, user_id, rating, reason, comment, created_at)\n VALUES (@id, @messageId, @userId, @rating, @reason, @comment, @createdAt)\n `),\n};\n\nconst deepseekApiKey = process.env.DEEPSEEK_API_KEY || '';\nconst deepseekBaseUrl = (process.env.DEEPSEEK_BASE_URL || 'https://api.deepseek.com').replace(/\\/+$/, '');\nconst deepseekModel = process.env.DEEPSEEK_MODEL || 'deepseek-chat';\nconst deepseekAvailableModels = Array.from(\n new Set(\n (process.env.DEEPSEEK_MODELS || `${deepseekModel},deepseek-chat,deepseek-reasoner`)\n .split(',')\n .map((item) => normalizeDeepSeekModelName(item))\n .filter(Boolean),\n ),\n);\nconst cbetaApiRoot = (process.env.CBETA_API_ROOT || 'https://api.ombhrum.com/api/cbeta').replace(/\\/+$/, '');\nconst fabushiApiBaseUrl = (process.env.FABUSHI_API_BASE_URL || 'https://api.ombhrum.com').replace(/\\/+$/, '');\nconst memberMonthlyLimit = Number(process.env.MEMBER_MONTHLY_TOKEN_LIMIT || 1_000_000);\nconst freeMonthlyLimit = Number(process.env.FREE_MONTHLY_TOKEN_LIMIT || 1_000);\nconst requireMemberForAi = process.env.REQUIRE_MEMBER_FOR_AI === 'true';\nconst deepseekMaxCompletionTokens = Math.max(1, Number(process.env.DEEPSEEK_MAX_COMPLETION_TOKENS || 1400));\nconst maxResourceTextChars = Number(process.env.MAX_RESOURCE_TEXT_CHARS || 80_000);\nconst openClawRuntimeManifestPath =\n process.env.OPENCLAW_RUNTIME_MANIFEST_PATH || path.join(openClawRuntimeUpdatesDir, 'manifest.json');\nconst openClawRuntimePublicBaseUrl = (process.env.OPENCLAW_RUNTIME_PUBLIC_BASE_URL || '').replace(/\\/+$/, '');\nconst enableLibreChatAgentChat = process.env.ENABLE_LIBRECHAT_AGENT_CHAT === 'true';\nconst libreChatAgentsBaseUrl = (\n process.env.LIBRECHAT_AGENTS_BASE_URL || 'http://127.0.0.1:3080/api/agents/v1'\n).replace(/\\/+$/, '');\nconst libreChatAgentApiKey = process.env.LIBRECHAT_AGENT_API_KEY || '';\nconst libreChatAgentId = process.env.LIBRECHAT_AGENT_ID || '';\nconst enableCodexSdkChat = process.env.ENABLE_CODEX_SDK_CHAT === 'true';\nconst enableOpenClawAgentChat = process.env.ENABLE_OPENCLAW_AGENT_CHAT === 'true';\nconst openClawGatewayUrl = (process.env.OPENCLAW_GATEWAY_URL || 'http://127.0.0.1:18789').replace(/\\/+$/, '');\nconst openClawGatewayToken = process.env.OPENCLAW_GATEWAY_TOKEN || '';\nconst openClawAgentId = process.env.OPENCLAW_AGENT_ID || 'fabushi-public-agent';\nconst openClawRunsEndpoint = process.env.OPENCLAW_RUNS_ENDPOINT || '';\nconst codexDeepSeekProviderId = 'deepseek-chat-completions';\nconst codexResponsesBaseUrl = (\n process.env.CODEX_DEEPSEEK_RESPONSES_BASE_URL ||\n `http://127.0.0.1:${process.env.PORT || 8788}/codex-deepseek/v1`\n).replace(/\\/+$/, '');\n\nconst systemPrompt = [\n '你是“大乘”App 的 AI 助手。',\n '你的核心任务是帮助用户查找、整理、理解并全球法布施合法可分享的佛法资源。',\n '回答要庄重、简洁、可执行;涉及经典、仪轨或资源时,提醒用户尊重版权、来源和当地法规。',\n '如果用户想找资源、下载经文、寻找音频或准备可分享资料,你必须先在后端自动执行搜索、验证和下载步骤;不要要求用户去前端 + 菜单手动查找。',\n '你可以把执行进度总结给用户,但不要输出隐藏推理原文。',\n].join('\\n');\n\nfunction nowIso() {\n return new Date().toISOString();\n}\n\nfunction monthKey(date = new Date()) {\n return `${date.getUTCFullYear()}-${String(date.getUTCMonth() + 1).padStart(2, '0')}`;\n}\n\nfunction jsonResponse(res, status, payload) {\n res.status(status).json(payload);\n}\n\nasync function readJsonFile(filePath) {\n const raw = await fs.promises.readFile(filePath, 'utf8');\n return JSON.parse(raw);\n}\n\nfunction requestOrigin(req) {\n if (openClawRuntimePublicBaseUrl) return openClawRuntimePublicBaseUrl;\n const proto = req.get('X-Forwarded-Proto') || req.protocol || 'https';\n const host = req.get('X-Forwarded-Host') || req.get('Host');\n return host ? `${proto}://${host}` : '';\n}\n\nfunction absolutizeOpenClawRuntimeUrl(req, value) {\n const text = readText(value);\n if (!text) return '';\n if (/^https?:\\/\\//i.test(text)) return text;\n const normalized = text.startsWith('/')\n ? text\n : `/api/openclaw/runtime/files/${text.replace(/^\\/+/, '')}`;\n const origin = requestOrigin(req);\n return origin ? new URL(normalized, `${origin}/`).toString() : normalized;\n}\n\nfunction textToolResult(text, structuredContent = {}) {\n return {\n structuredContent,\n content: [{ type: 'text', text }],\n };\n}\n\nfunction sseHeaders(res) {\n res.status(200);\n res.setHeader('Content-Type', 'text/event-stream; charset=utf-8');\n res.setHeader('Cache-Control', 'no-cache, no-transform');\n res.setHeader('Connection', 'keep-alive');\n res.setHeader('X-Accel-Buffering', 'no');\n if (typeof res.flushHeaders === 'function') res.flushHeaders();\n}\n\nfunction writeSse(res, event, payload) {\n res.write(`event: ${event}\\n`);\n res.write(`data: ${JSON.stringify(payload)}\\n\\n`);\n}\n\nfunction asyncHandler(fn) {\n return (req, res, next) => {\n Promise.resolve(fn(req, res, next)).catch(next);\n };\n}\n\nfunction bearerToken(req) {\n const delegated = readText(req.get('x-dacheng-auth-token'));\n if (\n delegated &&\n delegated !== 'DACHENG_AUTH_TOKEN' &&\n !delegated.startsWith('secretref-env:') &&\n delegated !== 'dacheng-openclaw-proxy'\n ) {\n return delegated;\n }\n const header = req.get('Authorization') || '';\n if (!header.startsWith('Bearer ')) return '';\n const token = header.slice('Bearer '.length).trim();\n return token === 'dacheng-openclaw-proxy' ? '' : token;\n}\n\nfunction sha256(value) {\n return crypto.createHash('sha256').update(value).digest('hex');\n}\n\nfunction safeUserText(value, fallback = '') {\n if (typeof value !== 'string') return fallback;\n return value.replace(/\\u0000/g, '').trim();\n}\n\nfunction estimateTokens(text) {\n const normalized = String(text || '').trim();\n if (!normalized) return 0;\n return Math.max(1, Math.ceil(normalized.length / 3));\n}\n\nfunction readText(value) {\n if (value === undefined || value === null) return '';\n return String(value).replace(/\\u0000/g, '').trim();\n}\n\nconst miniAppHostApiVersion = '2.0';\nconst miniAppHighRiskPermissions = new Set([\n 'auth.token',\n 'payments.alipay',\n 'payments.fudeGold',\n 'desktop.control',\n 'network.http',\n 'network.udp',\n 'network.interfaces',\n 'system.keepAwake',\n 'hotspot.settings',\n 'local.loopback',\n 'fs.readWrite',\n 'runtime.process',\n 'shell.execute',\n 'browser.external',\n 'files.write',\n 'projects.write',\n 'openclaw.restart',\n]);\n\nfunction officialMiniAppRegistry(req) {\n const webBase = process.env.MINIAPP_WEB_BASE_URL || 'https://fabushi.ombhrum.com';\n const official = [\n {\n botId: 'bot.global-dharma',\n miniAppId: 'official.global-dharma',\n title: '全球法布施',\n subtitle: '地区、循环、场能都在对话框上方设置',\n initials: '法',\n icon: 'public',\n avatarColor: '#4CAF7A',\n kind: 'global_dharma',\n entryUrl: process.env.GLOBAL_DHARMA_MINIAPP_URL,\n greeting: '把要分享的文字、链接或素材发给我,我会按上方选择的地区与模式启动全球法布施。',\n inputHint: '输入要法布施的文字/链接,或点 + 添加素材',\n permissions: [\n 'app.context',\n 'bot.chat',\n 'auth.session',\n 'wallet.balance',\n 'payments.entitlement',\n 'payments.fudeGold',\n 'payments.alipay',\n 'files.pick',\n 'network.http',\n 'network.udp',\n 'network.interfaces',\n 'system.keepAwake',\n 'hotspot.settings',\n 'local.loopback',\n 'openclaw.status',\n 'openclaw.chat',\n 'desktop.control',\n ],\n },\n {\n botId: 'bot.flashcards',\n miniAppId: 'official.flashcards',\n title: '背诵闪卡制作',\n subtitle: '随机挖空 / AI 制卡从顶部模式按钮选择',\n initials: '卡',\n icon: 'flashcards',\n avatarColor: '#7E57C2',\n kind: 'flashcards',\n entryUrl: process.env.FLASHCARDS_MINIAPP_URL,\n greeting: '粘贴经文、文章正文或链接即可制作背诵闪卡。制卡模式请在上方按钮切换。',\n inputHint: '粘贴链接或正文,发送后制作闪卡',\n permissions: ['app.context', 'bot.chat', 'flashcards.create', 'openclaw.status', 'openclaw.chat'],\n },\n {\n botId: 'bot.platform-publish',\n miniAppId: 'official.platform-publish',\n title: '法布施到平台',\n subtitle: '选择平台后生成发布草稿并打开入口',\n initials: '发',\n icon: 'publish',\n avatarColor: '#FF9F43',\n kind: 'platform_publish',\n entryUrl: process.env.PLATFORM_PUBLISH_MINIAPP_URL,\n greeting: '把要发布的正文或链接发给我,上方选择平台后,我会生成发布草稿并打开对应平台入口。',\n inputHint: '输入要发布到平台的正文/链接',\n permissions: [\n 'app.context',\n 'bot.chat',\n 'platform.publish',\n 'files.pick',\n 'fs.readWrite',\n 'runtime.process',\n 'shell.execute',\n 'browser.external',\n ],\n },\n {\n botId: 'bot.father',\n miniAppId: 'official.bot-father',\n title: '机器人之父',\n subtitle: '用对话生成个人沙箱小程序',\n initials: '父',\n icon: 'bot_father',\n avatarColor: '#3D8BFF',\n kind: 'bot_father',\n entryUrl: process.env.BOT_FATHER_MINIAPP_URL,\n greeting: '告诉我你想要的小程序,我会生成 manifest、界面和权限声明,并放进你的个人沙箱。',\n inputHint: '描述你想创建的小程序',\n permissions: ['app.context', 'bot.chat', 'miniapps.dev'],\n },\n ];\n return {\n bots: official.map((bot, index) => ({\n ...bot,\n destinationIndex: index,\n source: 'official',\n })),\n miniApps: official.map((bot) => ({\n miniAppId: bot.miniAppId,\n botId: bot.botId,\n title: bot.title,\n subtitle: bot.subtitle,\n entryUrl:\n bot.entryUrl ||\n `${webBase.replace(/\\/+$/, '')}/miniapps/${encodeURIComponent(bot.miniAppId)}`,\n version: '2.0.0',\n permissions: bot.permissions,\n surfaces: ['homePinned', 'chatPanel'],\n theme: 'telegramDark',\n signature: 'official',\n reviewStatus: 'trusted',\n source: 'official',\n })),\n };\n}\n\nasync function readMiniAppStore() {\n try {\n const raw = await fs.promises.readFile(miniAppsStorePath, 'utf8');\n const decoded = JSON.parse(raw);\n return {\n apps: Array.isArray(decoded.apps) ? decoded.apps : [],\n };\n } catch (error) {\n if (error?.code === 'ENOENT') return { apps: [] };\n throw error;\n }\n}\n\nasync function writeMiniAppStore(store) {\n await fs.promises.mkdir(miniAppsDir, { recursive: true });\n await fs.promises.writeFile(\n miniAppsStorePath,\n JSON.stringify({ apps: store.apps || [] }, null, 2),\n );\n}\n\nfunction miniAppEntryUrl(req, miniAppId) {\n const origin = requestOrigin(req) || '';\n const pathName = `/api/miniapps/dev/${encodeURIComponent(miniAppId)}/index.html`;\n return origin ? new URL(pathName, `${origin}/`).toString() : pathName;\n}\n\nfunction sanitizeMiniAppPermissions(value, { allowHighRisk = false } = {}) {\n const raw = Array.isArray(value) ? value : [];\n const normalized = raw\n .map((item) => readText(item).toLowerCase())\n .filter((item) => /^[a-z][a-z0-9_.:-]{0,64}$/.test(item));\n const deduped = [...new Set(['app.context', 'bot.chat', ...normalized])];\n return deduped.filter((permission) => allowHighRisk || !miniAppHighRiskPermissions.has(permission));\n}\n\nfunction miniAppScan(permissions, sourceHtml = '') {\n const highRisk = permissions.filter((permission) => miniAppHighRiskPermissions.has(permission));\n const blockedPatterns = [\n /authorization\\s*:\\s*['\"`]bearer/i,\n /desktop_control_bridge_token/i,\n /openclaw_gateway_token/i,\n /eval\\s*\\(/i,\n ];\n const blocked = blockedPatterns\n .filter((pattern) => pattern.test(sourceHtml))\n .map((pattern) => pattern.toString());\n return {\n passed: blocked.length === 0,\n highRiskPermissions: highRisk,\n blockedPatterns: blocked,\n };\n}\n\nfunction inferMiniAppPermissionsFromText(...values) {\n const text = values.map((value) => readText(value)).join('\\n').toLowerCase();\n const rules = [\n ['auth.session', [/auth\\.(getsession|requirelogin)/, /登录|用户信息|宿主账号/]],\n ['auth.token', [/auth\\.getaccesstoken/, /access token|访问 token/]],\n ['payments.alipay', [/payments\\.alipay/, /支付宝|支付|购买|订单/]],\n ['payments.entitlement', [/payments\\.checkentitlement|权益|解锁状态|购买状态/]],\n ['payments.fudeGold', [/payments\\.requestpayment|福德金|代币|钱包|余额/]],\n ['wallet.balance', [/wallet\\.getbalance|福德金|代币|钱包|余额/]],\n ['network.http', [/network\\.http|http\\.fetch|fetch\\s*\\(/, /链接正文|抓取网页|http 请求|网页内容/]],\n ['network.udp', [/network\\.udp|udp|datagram/, /全球法布施|法布施发送|数据报/]],\n ['network.interfaces', [/network\\.interfaces|网卡|网络接口|广播地址/]],\n ['system.keepAwake', [/system\\.keepawake|保持唤醒|后台任务|keep awake/]],\n ['hotspot.settings', [/hotspot\\.opensettings|热点|本地场能/]],\n ['local.loopback', [/localloopback\\./, /localhost|127\\.0\\.0\\.1|本地回环|本地转经轮/]],\n ['flashcards.create', [/flashcards\\./, /闪卡|背诵卡|挖空/]],\n ['platform.publish', [/platformpublish\\./, /发布草稿|发布到平台|公众号|小红书|知乎/]],\n ['files.pick', [/files\\.pick/, /选择文件|上传文件|本地素材/]],\n ['fs.readWrite', [/fs\\.(writefile|readfile)/, /本地文件|写文件|读文件|文件处理/]],\n ['runtime.process', [/runtime\\.process|process\\.execute/, /本地进程|执行程序|命令行|cli|playwright/]],\n ['shell.execute', [/shell\\.execute/, /shell/]],\n ['browser.external', [/browser\\.open/, /浏览器|打开网页|打开平台/]],\n ['desktop.control', [/desktopcontrol\\./, /桌面控制|点击|键盘|鼠标/]],\n ['openclaw.chat', [/openclaw\\.chat/, /openclaw/]],\n ];\n return rules\n .filter(([, patterns]) => patterns.some((pattern) => pattern.test(text)))\n .map(([permission]) => permission);\n}\n\nfunction miniAppRecordToBot(record) {\n return {\n botId: record.botId,\n miniAppId: record.miniAppId,\n title: record.title,\n subtitle: record.subtitle,\n initials: record.initials || '小',\n icon: record.icon || 'code',\n avatarColor: record.avatarColor || '#3D8BFF',\n kind: 'third_party',\n greeting: record.greeting || '你好,我是这个小程序的机器人。',\n inputHint: record.inputHint || '输入消息,或打开小程序面板',\n permissions: record.permissions || ['app.context', 'bot.chat'],\n source: record.source || 'sandbox',\n };\n}\n\nfunction miniAppRecordToManifest(req, record) {\n return {\n miniAppId: record.miniAppId,\n botId: record.botId,\n title: record.title,\n subtitle: record.subtitle,\n entryUrl: miniAppEntryUrl(req, record.miniAppId),\n version: record.version || '0.0.1',\n permissions: record.permissions || ['app.context', 'bot.chat'],\n surfaces: record.surfaces || ['chatPanel'],\n theme: record.theme || 'telegramDark',\n signature: record.signature || `sandbox:${record.ownerId}`,\n reviewStatus: record.reviewStatus || 'sandbox',\n source: record.source || 'sandbox',\n };\n}\n\nfunction safeMiniAppId(value) {\n return readText(value).replace(/[^a-zA-Z0-9_.-]/g, '-').slice(0, 80);\n}\n\nfunction titleFromPromptForMiniApp(prompt) {\n const text = readText(prompt).replace(/\\s+/g, ' ');\n if (!text) return '个人小程序';\n return text.length <= 18 ? text : text.slice(0, 18);\n}\n\nfunction escapeHtml(value) {\n return String(value || '')\n .replace(/&/g, '&')\n .replace(/</g, '<')\n .replace(/>/g, '>')\n .replace(/\"/g, '"')\n .replace(/'/g, ''');\n}\n\nfunction generatedMiniAppHtml({ title, prompt }) {\n const safeTitle = escapeHtml(title);\n const safePrompt = escapeHtml(prompt);\n const promptJson = JSON.stringify(readText(prompt));\n return `<!doctype html>\n<html lang=\"zh-Hans\">\n<head>\n <meta charset=\"utf-8\" />\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n <title>${safeTitle}</title>\n <style>\n body { margin:0; font-family:-apple-system,BlinkMacSystemFont,\"Segoe UI\",sans-serif; background:#0f1722; color:#fff; }\n main { padding:20px; display:grid; gap:14px; }\n .card { border:1px solid #263445; background:#17212b; border-radius:16px; padding:16px; }\n button { border:0; border-radius:999px; padding:10px 14px; background:#3390ec; color:white; font-weight:800; }\n pre { white-space:pre-wrap; color:#9ec7ff; }\n </style>\n</head>\n<body>\n <main>\n <section class=\"card\">\n <h1>${safeTitle}</h1>\n <p>这是机器人之父根据你的描述生成的个人沙箱小程序。</p>\n <pre>${safePrompt}</pre>\n </section>\n <section class=\"card\">\n <button id=\"context\">读取宿主上下文</button>\n <button id=\"spec\">读取宿主能力</button>\n <button id=\"chat\">让机器人解释这个小程序</button>\n <pre id=\"output\">等待操作...</pre>\n </section>\n </main>\n <script>\n const out = document.getElementById('output');\n async function invoke(method, params) {\n if (!window.FabushiMiniApp) throw new Error('FabushiMiniApp host SDK not ready');\n const result = await window.FabushiMiniApp.invoke(method, params || {});\n out.textContent = JSON.stringify(result, null, 2);\n }\n document.getElementById('context').onclick = () => invoke('app.getContext');\n document.getElementById('spec').onclick = () => invoke('app.getHostApiSpec');\n const prompt = ${promptJson};\n document.getElementById('chat').onclick = () => invoke('bot.sendMessage', {\n message: '请简要说明这个小程序能做什么:' + prompt\n });\n const seenCommands = new Set();\n function deliverMiniAppCommand(detail) {\n if (!detail || typeof detail !== 'object') return;\n const commandKey = detail.commandId || detail.id || [detail.createdAt, detail.command, detail.rawText || detail.text].filter(Boolean).join(':');\n if (commandKey) {\n if (seenCommands.has(commandKey)) return;\n seenCommands.add(commandKey);\n }\n handleMiniAppCommand(detail);\n }\n async function handleMiniAppCommand(detail) {\n out.textContent = '收到后台命令:' + JSON.stringify(detail, null, 2);\n try {\n await invoke('bot.postMessage', {\n commandId: detail.commandId || detail.id,\n message: '「${safeTitle}」已收到后台命令:' + (detail.args || detail.text || '')\n });\n await invoke('bot.reportCommandResult', {\n commandId: detail.commandId || detail.id,\n status: 'completed',\n message: '「${safeTitle}」后台命令已处理。'\n });\n } catch (error) {\n out.textContent = String(error && error.message || error);\n }\n }\n window.addEventListener('fabushi-miniapp-command', (event) => {\n deliverMiniAppCommand(event.detail || {});\n });\n if (window.__fabushiLastMiniAppCommand) {\n (window.queueMicrotask || ((fn) => setTimeout(fn, 0)))(() => {\n deliverMiniAppCommand(window.__fabushiLastMiniAppCommand);\n });\n }\n </script>\n</body>\n</html>`;\n}\n\nfunction extractMiniAppHtml(value) {\n const text = readText(value);\n if (!text) return '';\n const fenced = text.match(/```(?:html)?\\s*([\\s\\S]*?)```/i);\n const candidate = (fenced ? fenced[1] : text).trim();\n if (!/<html[\\s>]/i.test(candidate) || !/<script[\\s>]/i.test(candidate)) return '';\n return candidate.startsWith('<!doctype') || candidate.startsWith('<html')\n ? candidate\n : `<!doctype html>\\n${candidate}`;\n}\n\nasync function generateBotFatherMiniAppHtml({ title, prompt, user }) {\n const fallback = generatedMiniAppHtml({ title, prompt });\n try {\n const result = await runAgentModel({\n user,\n mode: 'miniapp_generation',\n maxCompletionTokens: deepseekMaxCompletionTokens,\n messages: [\n {\n role: 'system',\n content: [\n '你是 Fabushi App 的小程序生成器。',\n '只输出一个完整静态 HTML 文件,不要 Markdown,不要解释。',\n '小程序只能通过 window.FabushiMiniApp.invoke(method, params) 调用宿主能力。',\n '先调用 app.getHostApiSpec 或 app.getCapabilities 判断宿主版本和已授权权限,再显示对应 UI。',\n '聊天后台调用协议:优先使用 window.FabushiMiniApp.bot.onCommand(\"/start\", handler) 或 bot.onAnyCommand(handler);如果手写监听 window 的 fabushi-miniapp-command 事件,注册后也要读取 window.__fabushiLastMiniAppCommand 并按 commandId 去重补处理。detail.command 默认为 /start,detail.args 是用户文本;处理完成后调用 bot.postMessage 或 bot.reportCommandResult 把进度/结果写回聊天框。',\n '标准能力:auth.getSession/auth.requireLogin 复用宿主登录;payments.requestPayment 使用福德金支付并由宿主登记权益;payments.checkEntitlement 查询商品是否已解锁;payments.alipay.pay/createOrder 仅作为宿主官方现金支付兜底;network.http.fetch/network.udp/network.interfaces 提供 HTTP、UDP 和网卡原语;system.keepAwake/hotspot.openSettings 提供系统级辅助;flashcards.createDeck 接入背诵闪卡;platformPublish.createDraft 接入发布草稿;fs.writeFile/fs.readFile、runtime.process.execute、browser.open 用于本地文件、进程和浏览器。',\n '高危权限包括 auth.token、payments.alipay、payments.fudeGold、network.http、network.udp、network.interfaces、system.keepAwake、hotspot.settings、local.loopback、fs.readWrite、runtime.process、shell.execute、browser.external、desktop.control;只有用户需求明确需要时才使用。',\n '禁止外链脚本、eval、Authorization token、桌面桥 token、OpenClaw token。',\n '不要调用 auth.getAccessToken,除非用户明确要求对接需要 token 的受信服务。',\n '优先实现一个可点击、可立即使用的小面板,包含 app.getContext 或 bot.sendMessage 示例。',\n ].join('\\n'),\n },\n {\n role: 'user',\n content: [\n `小程序标题:${title}`,\n `用户需求:${prompt}`,\n '请生成 HTML/CSS/JS 单文件。',\n ].join('\\n'),\n },\n ],\n });\n const sourceHtml = extractMiniAppHtml(result.message);\n if (!sourceHtml) {\n return {\n sourceHtml: fallback,\n generation: { provider: 'template', fallbackReason: 'ai_output_not_html' },\n };\n }\n return {\n sourceHtml,\n generation: {\n provider: result.provider || 'ai',\n model: result.model || '',\n },\n };\n } catch (error) {\n return {\n sourceHtml: fallback,\n generation: {\n provider: 'template',\n fallbackReason: readText(error?.message || error),\n },\n };\n }\n}\n\nfunction clientContextMessage(client) {\n if (!client || typeof client !== 'object') return '';\n const selectedModel = readText(client.selectedModel || client.model);\n const project = client.project && typeof client.project === 'object' ? client.project : {};\n const projectName = readText(project.name);\n const projectPath = readText(project.path);\n const lines = ['桌面端上下文:'];\n if (selectedModel) lines.push(`用户选择的 DeepSeek 模型:${normalizeDeepSeekModelName(selectedModel)}`);\n if (projectPath) {\n lines.push(`当前项目名称:${projectName || projectPath}`);\n lines.push(`当前项目路径:${projectPath}`);\n lines.push('如果本轮任务涉及读取、修改、运行、分析或整理项目,请优先围绕当前项目路径工作;不要擅自操作其他目录。');\n }\n return lines.length > 1 ? lines.join('\\n') : '';\n}\nfunction unwrapPayload(payload) {\n if (!payload || typeof payload !== 'object') return {};\n return payload.data && typeof payload.data === 'object' ? payload.data : payload;\n}\n\nfunction readFirstText(values) {\n for (const value of values) {\n const text = readText(value);\n if (text) return text;\n }\n return '';\n}\n\nfunction activeFlag(value) {\n if (value === true) return true;\n if (value === 1) return true;\n if (typeof value !== 'string') return false;\n return ['true', '1', 'yes', 'active'].includes(value.trim().toLowerCase());\n}\n\nfunction futureDate(value) {\n const text = readText(value);\n if (!text) return false;\n const timestamp = Date.parse(text);\n return Number.isFinite(timestamp) && timestamp > Date.now();\n}\n\nfunction normalizeMembershipPayload(payload) {\n if (!payload || typeof payload !== 'object') return false;\n const data = unwrapPayload(payload);\n const membership = data.membership && typeof data.membership === 'object' ? data.membership : data;\n const type = readText(membership.type || membership.membershipType || data.membershipType).toLowerCase();\n const status = readText(membership.status || data.status).toLowerCase();\n const expiry = membership.expiresAt ||\n membership.expires_at ||\n membership.membershipExpiry ||\n membership.membership_expires_at ||\n data.membershipExpiry ||\n data.membership_expires_at;\n const hasExplicitActiveFlag =\n membership.isActive !== undefined ||\n membership.active !== undefined ||\n membership.hasPremiumAccess !== undefined ||\n membership.hasPremiumMembership !== undefined ||\n membership.isPremium !== undefined;\n\n if (hasExplicitActiveFlag) {\n return Boolean(\n activeFlag(membership.isActive) ||\n activeFlag(membership.active) ||\n activeFlag(membership.hasPremiumAccess) ||\n activeFlag(membership.hasPremiumMembership) ||\n activeFlag(membership.isPremium),\n );\n }\n\n return Boolean(\n status === 'active' ||\n type === 'paid' ||\n type === 'premium' ||\n type === 'trial' ||\n futureDate(expiry),\n );\n}\n\nfunction extractRemoteAccount(payload) {\n const data = unwrapPayload(payload);\n const nestedUser = data.user && typeof data.user === 'object' ? data.user : {};\n const id = readFirstText([\n data.userId,\n data.user_id,\n data.userNo,\n data.user_no,\n data.id,\n nestedUser.userId,\n nestedUser.user_id,\n nestedUser.userNo,\n nestedUser.user_no,\n nestedUser.id,\n ]);\n const username = readFirstText([data.username, data.name, nestedUser.username, nestedUser.name]);\n const email = readFirstText([data.email, nestedUser.email]);\n const membership = data.membership && typeof data.membership === 'object' ? data.membership : null;\n return {\n authenticated: Boolean(id || username || email || membership || data.success === true),\n id,\n username,\n email,\n isMember: normalizeMembershipPayload(payload),\n membership,\n };\n}\n\nasync function resolveRemoteAccount(token) {\n const fallback = {\n authenticated: false,\n id: '',\n username: '',\n email: '',\n isMember: false,\n membership: null,\n };\n if (!token || !fabushiApiBaseUrl) return fallback;\n\n const endpoints = [\n '/api/stripe/membership-status',\n '/api/alipay/check-membership',\n '/api/auth/user-info',\n ];\n\n for (const endpoint of endpoints) {\n try {\n const response = await fetch(`${fabushiApiBaseUrl}${endpoint}`, {\n headers: {\n Accept: 'application/json',\n Authorization: `Bearer ${token}`,\n },\n signal: AbortSignal.timeout(8_000),\n });\n if (response.status === 401 || response.status === 403) return fallback;\n if (!response.ok) continue;\n const payload = await response.json();\n const account = extractRemoteAccount(payload);\n if (account.authenticated || account.isMember) {\n return {\n ...fallback,\n ...account,\n authenticated: true,\n };\n }\n } catch (error) {\n logger.debug({ endpoint, error: String(error) }, 'membership lookup skipped');\n }\n }\n\n return fallback;\n}\n\nasync function resolveUser(req, body = {}) {\n const token = bearerToken(req);\n const usernameHint = safeUserText(body.username || req.query.username);\n\n const testToken = process.env.TEST_ACCOUNT_TOKEN || 'dacheng-test-token-bypass-limits';\n if (token && token === testToken) {\n return {\n userId: 'user:test_account',\n username: 'TestAccount',\n tokenHash: sha256(token).slice(0, 24),\n isAuthenticated: true,\n isMember: true,\n isTestAccount: true,\n membership: { type: 'lifetime', active: true },\n };\n }\n\n const tokenHash = token ? sha256(token).slice(0, 24) : '';\n const anonymousHash = sha256(`${req.ip}|${req.get('User-Agent') || ''}`).slice(0, 24);\n const remoteAccount = await resolveRemoteAccount(token);\n const remoteIdentity = remoteAccount.id ||\n remoteAccount.username ||\n remoteAccount.email ||\n (remoteAccount.authenticated ? tokenHash : '');\n const userId = remoteAccount.authenticated\n ? `user:${remoteIdentity}`\n : tokenHash\n ? `token:${tokenHash}`\n : `anon:${anonymousHash}`;\n return {\n userId,\n username: remoteAccount.username || usernameHint,\n tokenHash,\n isAuthenticated: remoteAccount.authenticated,\n isMember: remoteAccount.isMember,\n membership: remoteAccount.membership,\n };\n}\n\nfunction usageFor(userId) {\n const month = monthKey();\n const row = statements.getUsage.get(userId, month);\n return {\n month,\n used: Number(row?.totalTokens || 0),\n };\n}\n\nfunction enforceTokenBudget(user, estimatedTokensForRequest) {\n if (user && user.isTestAccount) {\n return { limit: 999999999, usage: { month: monthKey(), used: 0 } };\n }\n if (requireMemberForAi && !user.isMember) {\n const error = new Error('大乘 AI 需要开通会员后使用');\n error.statusCode = 403;\n error.code = 'AI_MEMBERSHIP_REQUIRED';\n error.details = {\n requireMemberForAi,\n monthlyLimit: memberMonthlyLimit,\n remainingTokens: 0,\n };\n throw error;\n }\n\n const limit = user.isMember ? memberMonthlyLimit : freeMonthlyLimit;\n const usage = usageFor(user.userId);\n if (usage.used + estimatedTokensForRequest > limit) {\n const error = new Error('本月 AI token 额度已不足');\n error.statusCode = 429;\n error.code = 'USER_TOKEN_QUOTA_EXCEEDED';\n error.details = {\n monthlyLimit: limit,\n usedTokens: usage.used,\n remainingTokens: Math.max(0, limit - usage.used),\n };\n throw error;\n }\n return { limit, usage };\n}\n\nfunction completionBudgetFor(budget, estimatedPromptTokens) {\n const remainingAfterPrompt = budget.limit - budget.usage.used - estimatedPromptTokens;\n return Math.max(1, Math.min(deepseekMaxCompletionTokens, Math.floor(remainingAfterPrompt)));\n}\n\nfunction recordUsage(userId, totalTokens) {\n if (!totalTokens || totalTokens < 1) return;\n statements.upsertUsage.run({\n userId,\n month: monthKey(),\n totalTokens,\n updatedAt: nowIso(),\n });\n}\n\nfunction titleFromMessage(message) {\n const compact = message.replace(/\\s+/g, ' ').trim();\n if (!compact) return '新对话';\n return compact.length > 22 ? `${compact.slice(0, 22)}...` : compact;\n}\n\nfunction normalizeMessages(rows) {\n return rows.map((row) => ({\n role: row.role,\n content: row.content,\n }));\n}\n\nfunction normalizeMaxCompletionTokens(value) {\n const numeric = Number(value || deepseekMaxCompletionTokens);\n if (!Number.isFinite(numeric) || numeric < 1) return deepseekMaxCompletionTokens;\n return Math.max(1, Math.min(deepseekMaxCompletionTokens, Math.floor(numeric)));\n}\n\nfunction normalizeDeepSeekModelName(value) {\n const raw = readText(value || deepseekModel);\n if (!raw) return deepseekModel;\n const modelId = raw.includes('/') ? raw.split('/').pop() : raw;\n return modelId || deepseekModel;\n}\n\nasync function callDeepSeek(messages, options = {}) {\n if (!deepseekApiKey) {\n const error = new Error('DeepSeek API key is not configured');\n error.statusCode = 503;\n throw error;\n }\n\n const model = normalizeDeepSeekModelName(options.model);\n const response = await fetch(`${deepseekBaseUrl}/chat/completions`, {\n method: 'POST',\n headers: {\n Accept: 'application/json',\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${deepseekApiKey}`,\n },\n body: JSON.stringify({\n model,\n messages,\n temperature: 0.4,\n max_tokens: normalizeMaxCompletionTokens(options.maxCompletionTokens),\n stream: false,\n }),\n signal: AbortSignal.timeout(60_000),\n });\n\n const bodyText = await response.text();\n let payload;\n try {\n payload = bodyText ? JSON.parse(bodyText) : {};\n } catch {\n payload = { error: bodyText };\n }\n\n if (!response.ok) {\n const upstreamMessage = payload?.error?.message || payload?.message || '';\n const friendlyMessage =\n response.status === 402 || /insufficient\\s+balance/i.test(upstreamMessage)\n ? '大乘 AI 服务额度暂不可用,请联系管理员充值 DeepSeek 账户后重试。'\n : upstreamMessage || `DeepSeek request failed: ${response.status}`;\n const error = new Error(friendlyMessage);\n error.statusCode = response.status;\n if (response.status === 402 || /insufficient\\s+balance/i.test(upstreamMessage)) {\n error.code = 'DEEPSEEK_PROVIDER_QUOTA';\n }\n throw error;\n }\n\n const message = payload?.choices?.[0]?.message?.content?.trim();\n if (!message) {\n const error = new Error('DeepSeek returned an empty response');\n error.statusCode = 502;\n throw error;\n }\n\n return {\n message,\n model,\n usage: {\n promptTokens: Number(payload?.usage?.prompt_tokens || 0),\n completionTokens: Number(payload?.usage?.completion_tokens || 0),\n totalTokens: Number(payload?.usage?.total_tokens || 0),\n },\n };\n}\n\nasync function callDeepSeekStream(messages, callbacks = {}) {\n if (!deepseekApiKey) {\n const error = new Error('DeepSeek API key is not configured');\n error.statusCode = 503;\n throw error;\n }\n\n const model = normalizeDeepSeekModelName(callbacks.model);\n const response = await fetch(`${deepseekBaseUrl}/chat/completions`, {\n method: 'POST',\n headers: {\n Accept: 'text/event-stream',\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${deepseekApiKey}`,\n },\n body: JSON.stringify({\n model,\n messages,\n temperature: 0.4,\n max_tokens: normalizeMaxCompletionTokens(callbacks.maxCompletionTokens),\n stream: true,\n stream_options: { include_usage: true },\n }),\n signal: callbacks.signal || AbortSignal.timeout(90_000),\n });\n\n if (!response.ok) {\n const bodyText = await response.text();\n let payload;\n try {\n payload = bodyText ? JSON.parse(bodyText) : {};\n } catch {\n payload = { error: bodyText };\n }\n const upstreamMessage = payload?.error?.message || payload?.message || '';\n const friendlyMessage =\n response.status === 402 || /insufficient\\s+balance/i.test(upstreamMessage)\n ? '大乘 AI 服务额度暂不可用,请联系管理员充值 DeepSeek 账户后重试。'\n : upstreamMessage || `DeepSeek request failed: ${response.status}`;\n const error = new Error(friendlyMessage);\n error.statusCode = response.status;\n if (response.status === 402 || /insufficient\\s+balance/i.test(upstreamMessage)) {\n error.code = 'DEEPSEEK_PROVIDER_QUOTA';\n }\n throw error;\n }\n\n let buffer = '';\n let message = '';\n let usage = {\n promptTokens: 0,\n completionTokens: 0,\n totalTokens: 0,\n };\n const decoder = new TextDecoder();\n\n for await (const chunk of response.body) {\n buffer += decoder.decode(chunk, { stream: true });\n const lines = buffer.split(/\\r?\\n/);\n buffer = lines.pop() || '';\n\n for (const rawLine of lines) {\n const line = rawLine.trim();\n if (!line.startsWith('data:')) continue;\n const data = line.slice('data:'.length).trim();\n if (!data || data === '[DONE]') continue;\n\n let payload;\n try {\n payload = JSON.parse(data);\n } catch {\n continue;\n }\n\n const delta = payload?.choices?.[0]?.delta?.content || '';\n if (delta) {\n message += delta;\n callbacks.onToken?.(delta);\n }\n\n if (payload?.usage) {\n usage = {\n promptTokens: Number(payload.usage.prompt_tokens || usage.promptTokens || 0),\n completionTokens: Number(payload.usage.completion_tokens || usage.completionTokens || 0),\n totalTokens: Number(payload.usage.total_tokens || usage.totalTokens || 0),\n };\n }\n }\n }\n\n message = message.trim();\n if (!message) {\n const error = new Error('DeepSeek returned an empty response');\n error.statusCode = 502;\n throw error;\n }\n\n return { message, model, usage };\n}\n\nfunction createCodexDeepSeekRuntime() {\n if (!enableCodexSdkChat || !deepseekApiKey) {\n return {\n enabled: false,\n provider: 'deepseek-direct',\n reason: enableCodexSdkChat ? 'DeepSeek API key is not configured' : 'ENABLE_CODEX_SDK_CHAT is not true',\n };\n }\n\n return {\n enabled: true,\n provider: 'codex-sdk-deepseek',\n options: {\n apiKey: deepseekApiKey,\n baseUrl: deepseekBaseUrl,\n env: {\n ...process.env,\n DEEPSEEK_API_KEY: deepseekApiKey,\n HOME: codexHomeDir,\n CODEX_HOME: codexHomeDir,\n TMPDIR: codexTempDir,\n XDG_RUNTIME_DIR: codexRuntimeDir,\n XDG_CACHE_HOME: path.join(codexHomeDir, '.cache'),\n XDG_CONFIG_HOME: path.join(codexHomeDir, '.config'),\n XDG_DATA_HOME: path.join(codexHomeDir, '.local', 'share'),\n PATH: process.env.PATH || '/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin',\n },\n config: {\n model_provider: codexDeepSeekProviderId,\n model_providers: {\n [codexDeepSeekProviderId]: {\n name: 'DeepSeek Chat Completions',\n base_url: codexResponsesBaseUrl,\n env_key: 'DEEPSEEK_API_KEY',\n wire_api: 'responses',\n query_params: {},\n },\n },\n },\n },\n threadOptions: {\n model: deepseekModel,\n approvalPolicy: 'never',\n sandboxMode: 'read-only',\n skipGitRepoCheck: true,\n networkAccessEnabled: true,\n },\n };\n}\n\nfunction codexPromptFromMessages(messages) {\n return messages\n .map((message) => {\n const role = String(message.role || 'user').toUpperCase();\n return `${role}:\\n${message.content}`;\n })\n .join('\\n\\n');\n}\n\nfunction responseContentText(content) {\n if (typeof content === 'string') return content;\n if (!Array.isArray(content)) return '';\n return content\n .map((part) => {\n if (typeof part === 'string') return part;\n if (!part || typeof part !== 'object') return '';\n return String(part.text || part.input_text || part.output_text || '');\n })\n .filter(Boolean)\n .join('\\n');\n}\n\nfunction codexResponsesPrompt(body) {\n const input = Array.isArray(body?.input) ? body.input : [];\n const userItems = input.filter((item) => item?.role === 'user');\n const source = userItems[userItems.length - 1] || input[input.length - 1];\n const prompt = responseContentText(source?.content).trim();\n if (prompt) return prompt;\n if (typeof body?.input === 'string') return body.input.trim();\n return '';\n}\n\nfunction writeResponsesEvent(res, event, data) {\n res.write(`event: ${event}\\n`);\n res.write(`data: ${JSON.stringify(data)}\\n\\n`);\n}\n\nfunction responseUsage(usage = {}) {\n return {\n input_tokens: Number(usage.promptTokens || usage.input_tokens || 0),\n cached_input_tokens: 0,\n output_tokens: Number(usage.completionTokens || usage.output_tokens || 0),\n reasoning_output_tokens: 0,\n total_tokens: Number(usage.totalTokens || usage.total_tokens || 0),\n };\n}\n\nfunction createLibreChatAgentRuntime() {\n if (!enableLibreChatAgentChat || !libreChatAgentApiKey || !libreChatAgentId) {\n return {\n enabled: false,\n provider: 'librechat-agent',\n reason: !enableLibreChatAgentChat\n ? 'ENABLE_LIBRECHAT_AGENT_CHAT is not true'\n : !libreChatAgentApiKey\n ? 'LIBRECHAT_AGENT_API_KEY is not configured'\n : 'LIBRECHAT_AGENT_ID is not configured',\n };\n }\n\n return {\n enabled: true,\n provider: 'librechat-agent',\n model: libreChatAgentId,\n chatCompletionsUrl: `${libreChatAgentsBaseUrl}/chat/completions`,\n };\n}\n\nfunction libreChatUsage(usage = {}) {\n return {\n promptTokens: Number(usage.prompt_tokens || usage.input_tokens || usage.promptTokens || 0),\n completionTokens: Number(usage.completion_tokens || usage.output_tokens || usage.completionTokens || 0),\n totalTokens: Number(usage.total_tokens || usage.totalTokens || 0),\n };\n}\n\nasync function callLibreChatAgent(messages) {\n const runtime = createLibreChatAgentRuntime();\n if (!runtime.enabled) {\n const error = new Error(runtime.reason || 'LibreChat Agent chat is not enabled');\n error.statusCode = 503;\n throw error;\n }\n\n const response = await fetch(runtime.chatCompletionsUrl, {\n method: 'POST',\n headers: {\n Accept: 'application/json',\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${libreChatAgentApiKey}`,\n },\n body: JSON.stringify({\n model: runtime.model,\n messages,\n temperature: 0.4,\n stream: false,\n }),\n signal: AbortSignal.timeout(120_000),\n });\n\n const bodyText = await response.text();\n let payload;\n try {\n payload = bodyText ? JSON.parse(bodyText) : {};\n } catch {\n payload = { error: bodyText };\n }\n\n if (!response.ok) {\n const upstreamMessage = payload?.error?.message || payload?.message || bodyText || '';\n const error = new Error(upstreamMessage || `LibreChat Agent request failed: ${response.status}`);\n error.statusCode = response.status;\n throw error;\n }\n\n const message = payload?.choices?.[0]?.message?.content?.trim();\n if (!message) {\n const error = new Error('LibreChat Agent returned an empty response');\n error.statusCode = 502;\n throw error;\n }\n\n return {\n message,\n usage: libreChatUsage(payload?.usage),\n };\n}\n\nasync function callLibreChatAgentStream(messages, callbacks = {}) {\n const runtime = createLibreChatAgentRuntime();\n if (!runtime.enabled) {\n const error = new Error(runtime.reason || 'LibreChat Agent chat is not enabled');\n error.statusCode = 503;\n throw error;\n }\n\n const response = await fetch(runtime.chatCompletionsUrl, {\n method: 'POST',\n headers: {\n Accept: 'text/event-stream',\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${libreChatAgentApiKey}`,\n },\n body: JSON.stringify({\n model: runtime.model,\n messages,\n temperature: 0.4,\n stream: true,\n }),\n signal: callbacks.signal || AbortSignal.timeout(180_000),\n });\n\n if (!response.ok) {\n const bodyText = await response.text();\n let payload;\n try {\n payload = bodyText ? JSON.parse(bodyText) : {};\n } catch {\n payload = { error: bodyText };\n }\n const upstreamMessage = payload?.error?.message || payload?.message || bodyText || '';\n const error = new Error(upstreamMessage || `LibreChat Agent request failed: ${response.status}`);\n error.statusCode = response.status;\n throw error;\n }\n\n let buffer = '';\n let message = '';\n let usage = { promptTokens: 0, completionTokens: 0, totalTokens: 0 };\n const decoder = new TextDecoder();\n\n for await (const chunk of response.body) {\n buffer += decoder.decode(chunk, { stream: true });\n const lines = buffer.split(/\\r?\\n/);\n buffer = lines.pop() || '';\n\n for (const rawLine of lines) {\n const line = rawLine.trim();\n if (!line.startsWith('data:')) continue;\n const data = line.slice('data:'.length).trim();\n if (!data || data === '[DONE]') continue;\n\n let payload;\n try {\n payload = JSON.parse(data);\n } catch {\n continue;\n }\n\n const delta = payload?.choices?.[0]?.delta?.content || '';\n if (delta) {\n message += delta;\n callbacks.onToken?.(delta);\n }\n\n if (payload?.usage) {\n usage = libreChatUsage(payload.usage);\n }\n }\n }\n\n message = message.trim();\n if (!message) {\n const error = new Error('LibreChat Agent returned an empty response');\n error.statusCode = 502;\n throw error;\n }\n\n return { message, usage };\n}\n\nfunction isDeepSeekQuotaError(error) {\n if (error?.code === 'DEEPSEEK_PROVIDER_QUOTA') return true;\n if (error?.code === 'USER_TOKEN_QUOTA_EXCEEDED' || error?.code === 'AI_MEMBERSHIP_REQUIRED') return false;\n const message = String(error?.message || error || '');\n return /insufficient\\s+balance|balance/i.test(message);\n}\n\nfunction publicAiErrorMessage(error) {\n if (isDeepSeekQuotaError(error)) {\n return '大乘 AI 服务额度暂不可用,请联系管理员充值 DeepSeek 账户后重试。';\n }\n return error?.message || 'Internal Server Error';\n}\n\nfunction responsesPayload({\n responseId,\n itemId,\n status,\n model,\n text = '',\n usage = null,\n error = null,\n}) {\n return {\n id: responseId,\n object: 'response',\n status,\n model,\n output:\n status === 'completed'\n ? [\n {\n id: itemId,\n type: 'message',\n role: 'assistant',\n status: 'completed',\n content: [{ type: 'output_text', text }],\n },\n ]\n : [],\n ...(usage ? { usage: responseUsage(usage) } : {}),\n ...(error\n ? {\n error: {\n message: error.message || String(error),\n type: 'server_error',\n },\n }\n : {}),\n };\n}\n\nasync function callCodexSdkDeepSeek(messages, callbacks = {}) {\n const codexRuntime = createCodexDeepSeekRuntime();\n if (!codexRuntime.enabled) {\n const error = new Error(codexRuntime.reason || 'Codex SDK chat is not enabled');\n error.statusCode = 503;\n throw error;\n }\n\n const { Codex } = await import('@openai/codex-sdk');\n const codex = new Codex(codexRuntime.options);\n const thread = codex.startThread(codexRuntime.threadOptions);\n const prompt = codexPromptFromMessages(messages);\n\n if (callbacks.onToken || callbacks.onStep) {\n const { events } = await thread.runStreamed(prompt, { signal: callbacks.signal });\n let finalResponse = '';\n let usage = null;\n\n for await (const event of events) {\n if (event.type === 'item.completed') {\n const item = event.item;\n if (item.type === 'agent_message') {\n finalResponse = item.text || finalResponse;\n callbacks.onToken?.(item.text || '');\n } else if (item.type === 'todo_list') {\n callbacks.onStep?.({\n title: 'Codex SDK 执行计划',\n message: item.items.map((todo) => `${todo.completed ? '✓' : '•'} ${todo.text}`).join('\\n'),\n });\n } else if (item.type === 'command_execution') {\n callbacks.onStep?.({\n title: 'Codex SDK 执行命令',\n message: item.command,\n });\n } else if (item.type === 'mcp_tool_call') {\n callbacks.onStep?.({\n title: 'Codex SDK 调用工具',\n message: `${item.server}.${item.tool}`,\n });\n }\n } else if (event.type === 'turn.completed') {\n usage = event.usage;\n } else if (event.type === 'turn.failed') {\n throw new Error(event.error?.message || 'Codex SDK turn failed');\n } else if (event.type === 'error') {\n throw new Error(event.message || 'Codex SDK stream failed');\n }\n }\n\n if (!finalResponse.trim()) {\n const error = new Error('Codex SDK returned an empty response');\n error.statusCode = 502;\n throw error;\n }\n\n return {\n message: finalResponse.trim(),\n usage: {\n promptTokens: Number(usage?.input_tokens || 0),\n completionTokens: Number(usage?.output_tokens || 0),\n totalTokens: Number((usage?.input_tokens || 0) + (usage?.output_tokens || 0)),\n },\n };\n }\n\n const turn = await thread.run(prompt);\n if (!turn.finalResponse?.trim()) {\n const error = new Error('Codex SDK returned an empty response');\n error.statusCode = 502;\n throw error;\n }\n\n return {\n message: turn.finalResponse.trim(),\n usage: {\n promptTokens: Number(turn.usage?.input_tokens || 0),\n completionTokens: Number(turn.usage?.output_tokens || 0),\n totalTokens: Number((turn.usage?.input_tokens || 0) + (turn.usage?.output_tokens || 0)),\n },\n };\n}\n\nconst activeAgentRuns = new Map();\n\nfunction normalizeAgentMode(value) {\n const mode = safeUserText(value || 'dharma_guide');\n return /^[a-z][a-z0-9_:-]{0,48}$/i.test(mode) ? mode : 'dharma_guide';\n}\n\nfunction openClawSessionKey(user, conversationId) {\n const env = process.env.NODE_ENV === 'production' ? 'prod' : 'dev';\n return `fabushi:env:${env}:user:${user.userId}:conversation:${conversationId}`;\n}\n\nfunction createOpenClawRuntime() {\n if (!enableOpenClawAgentChat || !openClawGatewayToken || !openClawGatewayUrl) {\n return {\n enabled: false,\n provider: 'openclaw-gateway',\n reason: !enableOpenClawAgentChat\n ? 'ENABLE_OPENCLAW_AGENT_CHAT is not true'\n : !openClawGatewayToken\n ? 'OPENCLAW_GATEWAY_TOKEN is not configured'\n : 'OPENCLAW_GATEWAY_URL is not configured',\n };\n }\n\n return {\n enabled: true,\n provider: 'openclaw-gateway',\n model: openClawAgentId,\n runsUrl: openClawRunsEndpoint || `${openClawGatewayUrl}/v1/agents/${encodeURIComponent(openClawAgentId)}/runs`,\n };\n}\n\nfunction extractOpenClawText(payload) {\n if (!payload || typeof payload !== 'object') return '';\n const candidates = [\n payload.message,\n payload.content,\n payload.output_text,\n payload.finalResponse,\n payload.final_response,\n payload.result?.message,\n payload.result?.content,\n payload.output?.message,\n payload.output?.content,\n payload.choices?.[0]?.message?.content,\n ];\n for (const candidate of candidates) {\n if (typeof candidate === 'string' && candidate.trim()) return candidate.trim();\n }\n return '';\n}\n\nfunction extractOpenClawUsage(payload) {\n const usage = payload?.usage || payload?.result?.usage || {};\n return {\n promptTokens: Number(usage.input_tokens || usage.prompt_tokens || usage.promptTokens || 0),\n completionTokens: Number(usage.output_tokens || usage.completion_tokens || usage.completionTokens || 0),\n totalTokens: Number(usage.total_tokens || usage.totalTokens || 0),\n };\n}\n\nasync function callOpenClawAgent({ messages, user, conversationId, mode, signal }) {\n const runtime = createOpenClawRuntime();\n if (!runtime.enabled) {\n const error = new Error(runtime.reason || 'OpenClaw Agent chat is not enabled');\n error.statusCode = 503;\n throw error;\n }\n\n const sessionKey = openClawSessionKey(user, conversationId);\n const response = await fetch(runtime.runsUrl, {\n method: 'POST',\n headers: {\n Accept: 'application/json',\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${openClawGatewayToken}`,\n },\n body: JSON.stringify({\n agentId: openClawAgentId,\n sessionKey,\n mode,\n stream: false,\n input: { messages },\n messages,\n metadata: {\n app: 'fabushi',\n userId: user.userId,\n conversationId,\n toolPolicy: 'public-user-read-mostly',\n },\n }),\n signal: signal || AbortSignal.timeout(180_000),\n });\n\n const bodyText = await response.text();\n let payload;\n try {\n payload = bodyText ? JSON.parse(bodyText) : {};\n } catch {\n payload = { message: bodyText };\n }\n\n if (!response.ok) {\n const error = new Error(payload?.error?.message || payload?.message || `OpenClaw request failed: ${response.status}`);\n error.statusCode = response.status;\n throw error;\n }\n\n const message = extractOpenClawText(payload);\n if (!message) {\n const error = new Error('OpenClaw returned an empty response');\n error.statusCode = 502;\n throw error;\n }\n\n return {\n message,\n runId: payload.runId || payload.id || payload.result?.runId || null,\n provider: runtime.provider,\n model: runtime.model,\n usage: extractOpenClawUsage(payload),\n };\n}\n\nasync function runAgentModel({ messages, user, conversationId, mode, callbacks = {}, signal, maxCompletionTokens }) {\n const openClawRuntime = createOpenClawRuntime();\n if (openClawRuntime.enabled && !callbacks.onToken) {\n return await callOpenClawAgent({ messages, user, conversationId, mode, signal });\n }\n\n const codexRuntime = createCodexDeepSeekRuntime();\n const libreChatRuntime = createLibreChatAgentRuntime();\n let provider = libreChatRuntime.enabled\n ? libreChatRuntime.provider\n : codexRuntime.enabled\n ? codexRuntime.provider\n : 'deepseek';\n let model = libreChatRuntime.enabled ? libreChatRuntime.model : deepseekModel;\n\n try {\n if (libreChatRuntime.enabled) {\n const result = callbacks.onToken\n ? await callLibreChatAgentStream(messages, { ...callbacks, signal })\n : await callLibreChatAgent(messages);\n return { ...result, provider, model };\n }\n if (codexRuntime.enabled) {\n const result = await callCodexSdkDeepSeek(messages, { ...callbacks, signal });\n return { ...result, provider, model };\n }\n const result = callbacks.onToken\n ? await callDeepSeekStream(messages, { ...callbacks, signal, maxCompletionTokens })\n : await callDeepSeek(messages, { maxCompletionTokens });\n return { ...result, provider, model };\n } catch (error) {\n if (!codexRuntime.enabled || isDeepSeekQuotaError(error)) throw error;\n logger.warn({ error: String(error) }, 'Agent model failed, falling back to DeepSeek direct');\n const result = callbacks.onToken\n ? await callDeepSeekStream(messages, { ...callbacks, signal, maxCompletionTokens })\n : await callDeepSeek(messages, { maxCompletionTokens });\n return { ...result, provider: 'deepseek', model: deepseekModel };\n }\n}\n\nfunction updateAgentRunStatus(run, status, fields = {}) {\n statements.updateAgentRunStatus.run({\n id: run.id,\n userId: run.user_id || run.userId,\n status,\n openClawRunId: fields.openClawRunId ?? null,\n inputTokens: fields.inputTokens ?? null,\n outputTokens: fields.outputTokens ?? null,\n toolCallCount: fields.toolCallCount ?? null,\n completedAt: status === 'completed' || status === 'cancelled' ? nowIso() : null,\n failedAt: status === 'failed' ? nowIso() : null,\n errorCode: fields.errorCode ?? null,\n errorMessage: fields.errorMessage ?? null,\n });\n}\n\nfunction buildAgentMessages(conversationId) {\n const historyRows = statements.listMessages.all(conversationId).slice(-14);\n return [{ role: 'system', content: systemPrompt }, ...normalizeMessages(historyRows)];\n}\n\nfunction agentUsagePayload(usage, user, budget) {\n const promptTokens = usage.promptTokens || usage.inputTokens || 0;\n const completionTokens = usage.completionTokens || usage.outputTokens || 0;\n const totalTokens = usage.totalTokens || promptTokens + completionTokens;\n const latestUsage = usageFor(user.userId);\n return {\n inputTokens: promptTokens,\n outputTokens: completionTokens,\n totalTokens,\n monthlyLimit: budget.limit,\n remainingTokens: Math.max(0, budget.limit - latestUsage.used),\n };\n}\n\nfunction openAiContentToText(content) {\n if (typeof content === 'string') return content;\n if (Array.isArray(content)) {\n return content\n .map((part) => {\n if (typeof part === 'string') return part;\n if (!part || typeof part !== 'object') return '';\n return readText(part.text || part.content || part.value);\n })\n .filter(Boolean)\n .join('\\n');\n }\n return readText(content);\n}\n\nfunction normalizeOpenAiMessages(value) {\n if (!Array.isArray(value)) return [];\n const allowedRoles = new Set(['system', 'user', 'assistant', 'tool']);\n return value\n .map((item) => {\n if (!item || typeof item !== 'object') return null;\n const role = allowedRoles.has(item.role) ? item.role : 'user';\n const content = openAiContentToText(item.content);\n if (!content) return null;\n return { role, content };\n })\n .filter(Boolean);\n}\n\nfunction truncateForDeepSeekMessage(text, maxChars = 12_000) {\n const value = readText(text);\n if (value.length <= maxChars) return value;\n return `${value.slice(0, maxChars)}\\n\\n[内容已截断]`;\n}\n\nfunction truncateTailForDeepSeekMessage(text, maxChars = 2400) {\n const value = readText(text);\n if (value.length <= maxChars) return value;\n return `[前文已省略]\\n\\n${value.slice(-maxChars)}`;\n}\n\nfunction stripOpenClawPromptNoise(text) {\n const value = readText(text);\n if (!value) return '';\n\n const finalPromptMatch = value.match(/finalPromptText[\"'\\s::]+([\\s\\S]{1,4000})$/i);\n if (finalPromptMatch?.[1]) {\n return readText(finalPromptMatch[1].replace(/^[\"'`]+|[\"'`,}\\]]+$/g, ''));\n }\n\n const markers = [\n 'final prompt:',\n 'final user prompt:',\n 'user prompt:',\n '用户问题:',\n '用户消息:',\n '用户输入:',\n ];\n const lower = value.toLowerCase();\n for (const marker of markers) {\n const index = lower.lastIndexOf(marker.toLowerCase());\n if (index >= 0) {\n const tail = value.slice(index + marker.length).trim();\n if (tail) return tail;\n }\n }\n\n return value;\n}\n\nfunction compactOpenClawMessagesForDeepSeek(messages) {\n const conversationalMessages = messages\n .filter((message) => message.role === 'user' || message.role === 'assistant')\n .map((message) => ({\n role: message.role,\n content: truncateTailForDeepSeekMessage(stripOpenClawPromptNoise(message.content)),\n }))\n .filter((message) => message.content);\n\n const recent = conversationalMessages.slice(-6);\n while (recent.length > 0 && recent[0].role !== 'user') {\n recent.shift();\n }\n\n if (recent.length === 0) {\n const fallback = messages\n .slice()\n .reverse()\n .map((message) => truncateTailForDeepSeekMessage(stripOpenClawPromptNoise(message.content), 2400))\n .find(Boolean);\n if (!fallback) return messages;\n recent.push({ role: 'user', content: fallback });\n }\n\n return [\n {\n role: 'system',\n content: '你是大乘 App 的桌面 AI 助手。请直接回答用户问题,默认使用简洁、准确的中文。',\n },\n ...recent,\n ];\n}\n\nfunction isDeepSeekBlockedError(error) {\n return (\n Number(error?.statusCode) === 403 &&\n /blocked|被拦截|forbidden/i.test(String(error?.message || error || ''))\n );\n}\n\nfunction shouldRetryWithCompactedOpenClawMessages(error, messages, compactedMessages) {\n if (!isDeepSeekBlockedError(error)) return false;\n return JSON.stringify(messages) !== JSON.stringify(compactedMessages);\n}\n\nfunction openClawBlockedFallbackText(messages) {\n const lastUser = messages\n .slice()\n .reverse()\n .find((message) => message.role === 'user')?.content || '';\n const localActionIntent =\n /(file|folder|directory|project|workspace|path|文件|文件夹|目录|项目|工程|路径|本地)/i.test(lastUser) &&\n /(create|make|mkdir|write|edit|modify|rename|delete|move|copy|save|download|read|list|生成|创建|新建|写入|编辑|修改|重命名|删除|移动|复制|保存|下载|读取|列出)/i.test(lastUser);\n\n if (localActionIntent) {\n return [\n '本机工具规划被模型服务拦截了,所以这次没有实际读取、创建、修改或下载本机文件。',\n '如果只是新建文件夹,请先在底部选择项目后重新发送“新建文件夹”;桌面端会直接在已选项目里创建。',\n '如果是更复杂的本地文件操作,请在“连应用”里重启本机 AI 后重试,或把要操作的文件路径和目标说得更具体。',\n ].join('\\n');\n }\n\n return '模型服务拦截了这次回复,未能生成结果。请换一种更具体、更短的表述后重试。';\n}\nfunction usageWithFallback(usage, messages, text) {\n const promptTokens = Number(usage?.promptTokens || usage?.inputTokens || 0);\n const completionTokens = Number(usage?.completionTokens || usage?.outputTokens || 0);\n const totalTokens = Number(usage?.totalTokens || 0);\n if (totalTokens > 0) {\n return {\n promptTokens,\n completionTokens,\n totalTokens,\n };\n }\n const estimatedPrompt = promptTokens || estimateTokens(JSON.stringify(messages));\n const estimatedCompletion = completionTokens || estimateTokens(text);\n return {\n promptTokens: estimatedPrompt,\n completionTokens: estimatedCompletion,\n totalTokens: estimatedPrompt + estimatedCompletion,\n };\n}\n\nfunction openAiUsagePayload(usage, user, budget) {\n const promptTokens = Number(usage.promptTokens || 0);\n const completionTokens = Number(usage.completionTokens || 0);\n const totalTokens = Number(usage.totalTokens || promptTokens + completionTokens);\n const latestUsage = usageFor(user.userId);\n const remainingTokens = Math.max(0, budget.limit - latestUsage.used);\n return {\n prompt_tokens: promptTokens,\n completion_tokens: completionTokens,\n total_tokens: totalTokens,\n promptTokens,\n completionTokens,\n totalTokens,\n monthlyLimit: budget.limit,\n remainingTokens,\n };\n}\n\nfunction writeOpenAiSse(res, payload) {\n res.write(`data: ${JSON.stringify(payload)}\\n\\n`);\n}\n\napp.post(\n '/api/openclaw/deepseek/v1/chat/completions',\n asyncHandler(async (req, res) => {\n const user = await resolveUser(req, req.body || {});\n const incomingMessages = normalizeOpenAiMessages(req.body?.messages);\n if (incomingMessages.length === 0) {\n res.status(400).json({ error: { message: 'messages is required' } });\n return;\n }\n\n const messages = compactOpenClawMessagesForDeepSeek(incomingMessages);\n const promptEstimate = estimateTokens(JSON.stringify(messages));\n const estimated = promptEstimate + 600;\n const budget = enforceTokenBudget(user, estimated);\n const maxCompletionTokens = Math.min(\n completionBudgetFor(budget, promptEstimate),\n normalizeMaxCompletionTokens(req.body?.max_tokens),\n );\n const model = normalizeDeepSeekModelName(req.body?.model);\n const wantsStream =\n req.body?.stream === true ||\n String(req.get('accept') || '').includes('text/event-stream');\n const id = `chatcmpl_${crypto.randomUUID().replaceAll('-', '')}`;\n const created = Math.floor(Date.now() / 1000);\n const compactedMessages = messages;\n\n if (!wantsStream) {\n let result;\n let usageMessages = messages;\n try {\n result = await callDeepSeek(messages, { model, maxCompletionTokens });\n } catch (error) {\n if (!shouldRetryWithCompactedOpenClawMessages(error, messages, compactedMessages)) {\n throw error;\n }\n logger.warn(\n {\n statusCode: error?.statusCode,\n messageCount: messages.length,\n compactedMessageCount: compactedMessages.length,\n },\n 'OpenClaw DeepSeek proxy retrying with compacted messages',\n );\n usageMessages = compactedMessages;\n result = await callDeepSeek(compactedMessages, { model, maxCompletionTokens });\n }\n const usage = usageWithFallback(result.usage, usageMessages, result.message);\n recordUsage(user.userId, usage.totalTokens);\n res.json({\n id,\n object: 'chat.completion',\n created,\n model: result.model || model,\n choices: [\n {\n index: 0,\n message: { role: 'assistant', content: result.message },\n finish_reason: 'stop',\n },\n ],\n usage: openAiUsagePayload(usage, user, budget),\n });\n return;\n }\n\n const abortController = new AbortController();\n req.on('close', () => abortController.abort());\n\n res.status(200);\n res.set({\n 'Content-Type': 'text/event-stream; charset=utf-8',\n 'Cache-Control': 'no-cache, no-transform',\n Connection: 'keep-alive',\n 'X-Accel-Buffering': 'no',\n });\n res.flushHeaders?.();\n\n const baseChunk = {\n id,\n object: 'chat.completion.chunk',\n created,\n model,\n system_fingerprint: 'fabushi-openclaw-deepseek-proxy',\n };\n writeOpenAiSse(res, {\n ...baseChunk,\n choices: [{ index: 0, delta: { role: 'assistant' }, finish_reason: null }],\n });\n\n let text = '';\n let usageMessages = messages;\n try {\n let result;\n try {\n result = await callDeepSeekStream(messages, {\n model,\n maxCompletionTokens,\n signal: abortController.signal,\n onToken: (delta) => {\n text += delta;\n writeOpenAiSse(res, {\n ...baseChunk,\n choices: [{ index: 0, delta: { content: delta }, finish_reason: null }],\n });\n },\n });\n } catch (error) {\n if (!shouldRetryWithCompactedOpenClawMessages(error, messages, compactedMessages)) {\n throw error;\n }\n logger.warn(\n {\n statusCode: error?.statusCode,\n messageCount: messages.length,\n compactedMessageCount: compactedMessages.length,\n },\n 'OpenClaw DeepSeek proxy stream retrying with compacted messages',\n );\n text = '';\n usageMessages = compactedMessages;\n result = await callDeepSeekStream(compactedMessages, {\n model,\n maxCompletionTokens,\n signal: abortController.signal,\n onToken: (delta) => {\n text += delta;\n writeOpenAiSse(res, {\n ...baseChunk,\n choices: [{ index: 0, delta: { content: delta }, finish_reason: null }],\n });\n },\n });\n }\n text = result.message || text;\n const usage = usageWithFallback(result.usage, usageMessages, text);\n recordUsage(user.userId, usage.totalTokens);\n writeOpenAiSse(res, {\n ...baseChunk,\n model: result.model || model,\n choices: [{ index: 0, delta: {}, finish_reason: 'stop' }],\n });\n writeOpenAiSse(res, {\n ...baseChunk,\n model: result.model || model,\n choices: [],\n usage: openAiUsagePayload(usage, user, budget),\n });\n res.write('data: [DONE]\\n\\n');\n } catch (error) {\n if (isDeepSeekBlockedError(error)) {\n const fallbackMessage = openClawBlockedFallbackText(usageMessages);\n const usage = usageWithFallback(null, usageMessages, fallbackMessage);\n recordUsage(user.userId, usage.totalTokens);\n logger.warn(\n { statusCode: error?.statusCode, code: error?.code },\n 'OpenClaw DeepSeek proxy stream blocked; returning local fallback message',\n );\n writeOpenAiSse(res, {\n ...baseChunk,\n choices: [{ index: 0, delta: { content: fallbackMessage }, finish_reason: null }],\n });\n writeOpenAiSse(res, {\n ...baseChunk,\n choices: [{ index: 0, delta: {}, finish_reason: 'stop' }],\n });\n writeOpenAiSse(res, {\n ...baseChunk,\n choices: [],\n usage: openAiUsagePayload(usage, user, budget),\n });\n res.write('data: [DONE]\\n\\n');\n return;\n }\n logger.error(\n { error: String(error), code: error?.code, statusCode: error?.statusCode },\n 'OpenClaw DeepSeek proxy stream failed',\n );\n writeOpenAiSse(res, {\n ...baseChunk,\n choices: [],\n error: {\n message: publicAiErrorMessage(error),\n code: error?.code || 'OPENCLAW_DEEPSEEK_PROXY_ERROR',\n },\n });\n res.write('data: [DONE]\\n\\n');\n } finally {\n res.end();\n }\n }),\n);\n\napp.post(\n '/codex-deepseek/v1/responses',\n asyncHandler(async (req, res) => {\n const bearer = bearerToken(req);\n if (!deepseekApiKey || bearer !== deepseekApiKey) {\n res.status(401).json({\n error: { message: 'Unauthorized Codex DeepSeek adapter request' },\n });\n return;\n }\n\n const prompt = codexResponsesPrompt(req.body);\n if (!prompt) {\n res.status(400).json({ error: { message: 'Responses input is required' } });\n return;\n }\n\n const model = String(req.body?.model || deepseekModel);\n const responseId = `resp_${crypto.randomUUID().replaceAll('-', '')}`;\n const itemId = `msg_${crypto.randomUUID().replaceAll('-', '')}`;\n const wantsStream =\n req.body?.stream !== false ||\n String(req.get('accept') || '').includes('text/event-stream');\n const messages = [{ role: 'user', content: prompt }];\n\n if (!wantsStream) {\n const result = await callDeepSeek(messages);\n res.json(\n responsesPayload({\n responseId,\n itemId,\n status: 'completed',\n model,\n text: result.message,\n usage: result.usage,\n }),\n );\n return;\n }\n\n res.status(200);\n res.set({\n 'Content-Type': 'text/event-stream; charset=utf-8',\n 'Cache-Control': 'no-cache, no-transform',\n Connection: 'keep-alive',\n 'X-Accel-Buffering': 'no',\n });\n res.flushHeaders?.();\n\n writeResponsesEvent(res, 'response.created', {\n type: 'response.created',\n response: responsesPayload({\n responseId,\n itemId,\n status: 'in_progress',\n model,\n }),\n });\n writeResponsesEvent(res, 'response.output_item.added', {\n type: 'response.output_item.added',\n output_index: 0,\n item: {\n id: itemId,\n type: 'message',\n role: 'assistant',\n status: 'in_progress',\n content: [],\n },\n });\n writeResponsesEvent(res, 'response.content_part.added', {\n type: 'response.content_part.added',\n item_id: itemId,\n output_index: 0,\n content_index: 0,\n part: { type: 'output_text', text: '' },\n });\n\n let text = '';\n try {\n const result = await callDeepSeekStream(messages, {\n onToken: (delta) => {\n text += delta;\n writeResponsesEvent(res, 'response.output_text.delta', {\n type: 'response.output_text.delta',\n item_id: itemId,\n output_index: 0,\n content_index: 0,\n delta,\n });\n },\n });\n text = result.message || text;\n writeResponsesEvent(res, 'response.output_text.done', {\n type: 'response.output_text.done',\n item_id: itemId,\n output_index: 0,\n content_index: 0,\n text,\n });\n writeResponsesEvent(res, 'response.content_part.done', {\n type: 'response.content_part.done',\n item_id: itemId,\n output_index: 0,\n content_index: 0,\n part: { type: 'output_text', text },\n });\n writeResponsesEvent(res, 'response.output_item.done', {\n type: 'response.output_item.done',\n output_index: 0,\n item: {\n id: itemId,\n type: 'message',\n role: 'assistant',\n status: 'completed',\n content: [{ type: 'output_text', text }],\n },\n });\n writeResponsesEvent(res, 'response.completed', {\n type: 'response.completed',\n response: responsesPayload({\n responseId,\n itemId,\n status: 'completed',\n model,\n text,\n usage: result.usage,\n }),\n });\n res.end();\n } catch (error) {\n writeResponsesEvent(res, 'response.failed', {\n type: 'response.failed',\n response: responsesPayload({\n responseId,\n itemId,\n status: 'failed',\n model,\n text,\n error,\n }),\n });\n res.end();\n }\n }),\n);\n\napp.use(\n '/api/openclaw/runtime/files',\n express.static(openClawRuntimeUpdatesDir, {\n fallthrough: false,\n immutable: true,\n maxAge: '7d',\n setHeaders(res) {\n res.setHeader('X-Content-Type-Options', 'nosniff');\n },\n }),\n);\n\napp.get(\n '/api/openclaw/runtime/manifest',\n asyncHandler(async (req, res) => {\n const platform = readText(req.query.platform);\n const currentVersion = readText(req.query.currentVersion);\n const appVersion = readText(req.query.appVersion);\n\n let manifest;\n try {\n manifest = await readJsonFile(openClawRuntimeManifestPath);\n } catch (error) {\n logger.info(\n { manifestPath: openClawRuntimeManifestPath, error: String(error) },\n 'OpenClaw runtime manifest unavailable',\n );\n return jsonResponse(res, 200, {\n success: true,\n schema: 1,\n updateAvailable: false,\n reason: 'manifest-unavailable',\n platform,\n currentVersion,\n appVersion,\n });\n }\n\n const latest = manifest.latest && typeof manifest.latest === 'object'\n ? manifest.latest\n : manifest;\n const platforms = latest.platforms && typeof latest.platforms === 'object'\n ? latest.platforms\n : {};\n const platformRelease = platform ? platforms[platform] : null;\n const version = readText(latest.version || manifest.version);\n\n if (!platform || !platformRelease || typeof platformRelease !== 'object') {\n return jsonResponse(res, 200, {\n success: true,\n schema: Number(manifest.schema || 1),\n channel: readText(manifest.channel || latest.channel || 'stable'),\n updateAvailable: false,\n reason: platform ? 'platform-unavailable' : 'platform-required',\n platform,\n currentVersion,\n appVersion,\n latestVersion: version,\n });\n }\n\n if (platform.startsWith('macos-') && !openClawRuntimeEnableMacosUpdates) {\n return jsonResponse(res, 200, {\n success: true,\n schema: Number(manifest.schema || 1),\n channel: readText(manifest.channel || latest.channel || 'stable'),\n updateAvailable: false,\n reason: 'macos-runtime-updates-disabled',\n platform,\n currentVersion,\n appVersion,\n latestVersion: version,\n });\n }\n\n const downloadUrl = absolutizeOpenClawRuntimeUrl(\n req,\n platformRelease.archiveUrl || platformRelease.url || platformRelease.fileName,\n );\n const release = {\n version,\n channel: readText(manifest.channel || latest.channel || 'stable'),\n minAppVersion: readText(latest.minAppVersion || platformRelease.minAppVersion),\n notes: readText(latest.notes || platformRelease.notes),\n nodeExecutable: readText(\n platformRelease.nodeExecutable || latest.nodeExecutable || 'node/bin/node',\n ),\n cliEntrypoint: readText(\n platformRelease.cliEntrypoint || latest.cliEntrypoint || 'openclaw/openclaw.mjs',\n ),\n defaultPort: Number(latest.defaultPort || manifest.defaultPort || 18789),\n defaultModel: readText(latest.defaultModel || manifest.defaultModel || 'openclaw/default'),\n defaultModelOverride: readText(latest.defaultModelOverride || manifest.defaultModelOverride),\n gatewayArgs: Array.isArray(latest.gatewayArgs)\n ? latest.gatewayArgs.map((item) => String(item))\n : ['gateway', '--port', '{port}', '--force'],\n archiveUrl: downloadUrl,\n sha256: readText(platformRelease.sha256),\n size: Number(platformRelease.size || 0),\n };\n\n jsonResponse(res, 200, {\n success: true,\n schema: Number(manifest.schema || 1),\n updateAvailable: Boolean(version && downloadUrl && version !== currentVersion),\n platform,\n currentVersion,\n appVersion,\n latest: release,\n generatedAt: readText(manifest.generatedAt || latest.generatedAt),\n });\n }),\n);\n\napp.get('/health', (_req, res) => {\n const codexRuntime = createCodexDeepSeekRuntime();\n const libreChatRuntime = createLibreChatAgentRuntime();\n const openClawRuntime = createOpenClawRuntime();\n jsonResponse(res, 200, {\n status: 'ok',\n service: 'dacheng-ai-backend',\n provider: libreChatRuntime.enabled ? libreChatRuntime.provider : 'deepseek',\n model: libreChatRuntime.enabled ? libreChatRuntime.model : deepseekModel,\n deepseekModel,\n deepseekAvailableModels,\n libreChatAgentChatEnabled: libreChatRuntime.enabled,\n libreChatAgentProvider: libreChatRuntime.provider,\n libreChatAgentReason: libreChatRuntime.enabled ? undefined : libreChatRuntime.reason,\n codexSdkAvailable: true,\n codexSdkChatEnabled: codexRuntime.enabled,\n codexSdkProvider: codexRuntime.provider,\n openClawAgentChatEnabled: openClawRuntime.enabled,\n openClawAgentProvider: openClawRuntime.provider,\n openClawAgentReason: openClawRuntime.enabled ? undefined : openClawRuntime.reason,\n requireMemberForAi,\n memberMonthlyLimit,\n freeMonthlyLimit,\n deepseekMaxCompletionTokens,\n openClawRuntimeUpdatesDir,\n openClawRuntimeManifestConfigured: fs.existsSync(openClawRuntimeManifestPath),\n cbetaApiRoot,\n timestamp: nowIso(),\n });\n});\n\napp.get(\n '/api/miniapps/registry',\n asyncHandler(async (req, res) => {\n const user = await resolveUser(req, { username: req.query.username });\n const official = officialMiniAppRegistry(req);\n const store = await readMiniAppStore();\n const visibleSandbox = store.apps.filter((appRecord) => {\n if (appRecord.reviewStatus === 'approved' && appRecord.source === 'marketplace') {\n return true;\n }\n return appRecord.ownerId === user.userId;\n });\n const bots = [\n ...official.bots,\n ...visibleSandbox.map((record) => miniAppRecordToBot(record)),\n ];\n const miniApps = [\n ...official.miniApps,\n ...visibleSandbox.map((record) => miniAppRecordToManifest(req, record)),\n ];\n const registry = {\n schemaVersion: 1,\n hostApiVersion: miniAppHostApiVersion,\n bots,\n miniApps,\n updatedAt: nowIso(),\n };\n registry.signature = sha256(JSON.stringify({ bots, miniApps }));\n jsonResponse(res, 200, { success: true, registry });\n }),\n);\n\napp.get(\n '/api/miniapps/dev/:miniAppId/index.html',\n asyncHandler(async (req, res) => {\n const miniAppId = safeMiniAppId(req.params.miniAppId);\n const store = await readMiniAppStore();\n const record = store.apps.find((appRecord) => appRecord.miniAppId === miniAppId);\n if (!record) {\n res.status(404).type('text/html').send('<h1>Mini app not found</h1>');\n return;\n }\n res.setHeader('Content-Type', 'text/html; charset=utf-8');\n res.setHeader('Cache-Control', 'no-store');\n res.send(record.sourceHtml || generatedMiniAppHtml(record));\n }),\n);\n\napp.post(\n '/api/miniapps/dev/create',\n asyncHandler(async (req, res) => {\n const user = await resolveUser(req, req.body || {});\n const title = safeUserText(req.body?.title, '个人小程序').slice(0, 80);\n const subtitle = safeUserText(req.body?.subtitle, '个人沙箱小程序').slice(0, 120);\n const permissions = sanitizeMiniAppPermissions(req.body?.permissions);\n const prompt = safeUserText(req.body?.prompt || title);\n const idSeed = crypto.randomUUID().replaceAll('-', '').slice(0, 12);\n const miniAppId = `sandbox.${idSeed}`;\n const botId = `bot.${miniAppId}`;\n const sourceHtml = generatedMiniAppHtml({ title, prompt });\n const scan = miniAppScan(permissions, sourceHtml);\n const createdAt = nowIso();\n const record = {\n ownerId: user.userId,\n miniAppId,\n botId,\n title,\n subtitle,\n initials: title.slice(0, 1) || '小',\n icon: 'code',\n avatarColor: '#3D8BFF',\n greeting: '你好,我是这个个人沙箱小程序的机器人。',\n inputHint: '输入消息,或打开小程序面板',\n version: '0.0.1',\n permissions,\n surfaces: ['chatPanel'],\n theme: 'telegramDark',\n source: 'sandbox',\n reviewStatus: 'sandbox',\n sourceHtml,\n scan,\n createdAt,\n updatedAt: createdAt,\n };\n const store = await readMiniAppStore();\n store.apps.push(record);\n await writeMiniAppStore(store);\n jsonResponse(res, 200, {\n success: true,\n miniApp: miniAppRecordToManifest(req, record),\n bot: miniAppRecordToBot(record),\n scan,\n });\n }),\n);\n\napp.post(\n '/api/miniapps/dev/:miniAppId/version',\n asyncHandler(async (req, res) => {\n const user = await resolveUser(req, req.body || {});\n const miniAppId = safeMiniAppId(req.params.miniAppId);\n const store = await readMiniAppStore();\n const index = store.apps.findIndex((appRecord) => appRecord.miniAppId === miniAppId);\n if (index < 0) {\n return jsonResponse(res, 404, { success: false, message: 'mini app not found' });\n }\n const record = store.apps[index];\n if (record.ownerId !== user.userId) {\n return jsonResponse(res, 403, { success: false, message: 'not your mini app' });\n }\n const title = safeUserText(req.body?.title, record.title).slice(0, 80);\n const subtitle = safeUserText(req.body?.subtitle, record.subtitle).slice(0, 120);\n const sourceHtml =\n safeUserText(req.body?.sourceHtml) ||\n generatedMiniAppHtml({ title, prompt: safeUserText(req.body?.prompt || title) });\n const permissions = sanitizeMiniAppPermissions(req.body?.permissions || record.permissions);\n const scan = miniAppScan(permissions, sourceHtml);\n store.apps[index] = {\n ...record,\n title,\n subtitle,\n sourceHtml,\n permissions,\n scan,\n reviewStatus: 'sandbox',\n version: safeUserText(req.body?.version, record.version || '0.0.1'),\n updatedAt: nowIso(),\n };\n await writeMiniAppStore(store);\n jsonResponse(res, 200, {\n success: true,\n miniApp: miniAppRecordToManifest(req, store.apps[index]),\n bot: miniAppRecordToBot(store.apps[index]),\n scan,\n });\n }),\n);\n\napp.post(\n '/api/miniapps/:miniAppId/submit-review',\n asyncHandler(async (req, res) => {\n const user = await resolveUser(req, req.body || {});\n const miniAppId = safeMiniAppId(req.params.miniAppId);\n const store = await readMiniAppStore();\n const index = store.apps.findIndex((appRecord) => appRecord.miniAppId === miniAppId);\n if (index < 0) {\n return jsonResponse(res, 404, { success: false, message: 'mini app not found' });\n }\n const record = store.apps[index];\n if (record.ownerId !== user.userId) {\n return jsonResponse(res, 403, { success: false, message: 'not your mini app' });\n }\n const scan = miniAppScan(record.permissions || [], record.sourceHtml || '');\n if (!scan.passed) {\n return jsonResponse(res, 400, {\n success: false,\n message: 'security scan failed',\n scan,\n });\n }\n store.apps[index] = {\n ...record,\n scan,\n reviewStatus: 'pending_review',\n updatedAt: nowIso(),\n };\n await writeMiniAppStore(store);\n jsonResponse(res, 200, {\n success: true,\n miniApp: miniAppRecordToManifest(req, store.apps[index]),\n scan,\n });\n }),\n);\n\napp.post(\n '/api/botfather/generate-miniapp',\n asyncHandler(async (req, res) => {\n const user = await resolveUser(req, req.body || {});\n const prompt = safeUserText(req.body?.prompt);\n if (!prompt) {\n return jsonResponse(res, 400, { success: false, message: 'prompt is required' });\n }\n const title = titleFromPromptForMiniApp(prompt);\n const subtitle = '机器人之父生成的个人沙箱小程序';\n const requestedPermissions = Array.isArray(req.body?.permissions) ? req.body.permissions : [];\n const generated = await generateBotFatherMiniAppHtml({ title, prompt, user });\n let sourceHtml = generated.sourceHtml;\n let generation = generated.generation;\n const permissions = sanitizeMiniAppPermissions([\n ...requestedPermissions,\n ...inferMiniAppPermissionsFromText(prompt, sourceHtml),\n ]);\n let scan = miniAppScan(permissions, sourceHtml);\n if (!scan.passed) {\n sourceHtml = generatedMiniAppHtml({ title, prompt });\n generation = {\n provider: 'template',\n fallbackReason: 'ai_output_failed_security_scan',\n };\n scan = miniAppScan(permissions, sourceHtml);\n }\n const idSeed = crypto.randomUUID().replaceAll('-', '').slice(0, 12);\n const miniAppId = `sandbox.${idSeed}`;\n const createdAt = nowIso();\n const record = {\n ownerId: user.userId,\n miniAppId,\n botId: `bot.${miniAppId}`,\n title,\n subtitle,\n initials: title.slice(0, 1) || '小',\n icon: 'code',\n avatarColor: '#3D8BFF',\n greeting: `我是「${title}」的机器人。`,\n inputHint: '继续描述修改需求,或打开小程序面板',\n version: '0.0.1',\n permissions,\n surfaces: ['chatPanel'],\n theme: 'telegramDark',\n source: 'sandbox',\n reviewStatus: 'sandbox',\n sourceHtml,\n scan,\n generation,\n prompt,\n createdAt,\n updatedAt: createdAt,\n };\n const store = await readMiniAppStore();\n store.apps.push(record);\n await writeMiniAppStore(store);\n jsonResponse(res, 200, {\n success: true,\n miniApp: miniAppRecordToManifest(req, record),\n bot: miniAppRecordToBot(record),\n scan,\n generation,\n actions: ['openMiniApp', 'continueEditing', 'submitReview'],\n });\n }),\n);\n\napp.get('/api/ai/models', (_req, res) => {\n jsonResponse(res, 200, {\n success: true,\n provider: 'deepseek',\n defaultModel: deepseekModel,\n models: deepseekAvailableModels.map((model) => ({\n id: model,\n label: model === 'deepseek-reasoner' ? 'DeepSeek Reasoner' : 'DeepSeek Chat',\n })),\n });\n});\napp.get(\n '/api/ai/quota',\n asyncHandler(async (req, res) => {\n const user = await resolveUser(req, { username: req.query.username });\n const budget = enforceTokenBudget(user, 0);\n const usage = usageFor(user.userId);\n jsonResponse(res, 200, {\n success: true,\n isMember: user.isMember,\n monthlyLimit: budget.limit,\n usedTokens: usage.used,\n remainingTokens: Math.max(0, budget.limit - usage.used),\n month: usage.month,\n });\n }),\n);\n\napp.post(\n '/api/agent/chat',\n asyncHandler(async (req, res) => {\n const message = safeUserText(req.body?.message);\n if (!message) {\n return jsonResponse(res, 400, { success: false, message: 'message is required' });\n }\n\n const user = await resolveUser(req, req.body || {});\n const promptEstimate = estimateTokens(message);\n const estimated = promptEstimate + 600;\n const budget = enforceTokenBudget(user, estimated);\n const maxCompletionTokens = completionBudgetFor(budget, promptEstimate);\n const createdAt = nowIso();\n const conversationId = safeUserText(req.body?.conversationId) || crypto.randomUUID();\n const mode = normalizeAgentMode(req.body?.mode);\n const stream = req.body?.stream === true;\n let conversation = statements.getConversation.get(conversationId, user.userId);\n const isNewConversation = !conversation;\n\n if (isNewConversation) {\n conversation = { id: conversationId, user_id: user.userId, title: titleFromMessage(message) };\n statements.insertConversation.run({\n id: conversationId,\n userId: user.userId,\n title: conversation.title,\n provider: 'agent',\n model: openClawAgentId,\n createdAt,\n updatedAt: createdAt,\n });\n }\n\n const messageId = safeUserText(req.body?.messageId) || crypto.randomUUID();\n statements.insertMessage.run({\n id: messageId,\n conversationId,\n role: 'user',\n content: message,\n promptTokens: 0,\n completionTokens: 0,\n totalTokens: 0,\n createdAt,\n });\n\n const runId = `run_${crypto.randomUUID().replaceAll('-', '')}`;\n const sessionKey = openClawSessionKey(user, conversationId);\n const openClawRuntime = createOpenClawRuntime();\n statements.insertAgentRun.run({\n id: runId,\n userId: user.userId,\n conversationId,\n messageId,\n openClawRunId: null,\n openClawSessionKey: sessionKey,\n status: stream ? 'queued' : 'running',\n mode,\n provider: openClawRuntime.enabled ? openClawRuntime.provider : 'dacheng-ai-fallback',\n model: openClawRuntime.enabled ? openClawAgentId : deepseekModel,\n startedAt: createdAt,\n });\n\n if (stream) {\n return jsonResponse(res, 200, {\n success: true,\n runId,\n conversationId,\n streamUrl: `/api/agent/runs/${runId}/events`,\n });\n }\n\n const controller = new AbortController();\n activeAgentRuns.set(runId, controller);\n try {\n const skillResult = await runResourceFinderSkill({ message, user });\n const skillContext = resourceContextMessage(skillResult);\n const historyRows = statements.listMessages.all(conversationId).slice(-14);\n const modelMessages = [\n { role: 'system', content: systemPrompt },\n ...(skillContext ? [{ role: 'system', content: skillContext }] : []),\n ...normalizeMessages(historyRows),\n ];\n\n const aiResult = await runAgentModel({\n messages: modelMessages,\n user,\n conversationId,\n mode,\n signal: controller.signal,\n maxCompletionTokens,\n });\n const usage = {\n promptTokens: aiResult.usage?.promptTokens || estimateTokens(JSON.stringify(modelMessages)),\n completionTokens: aiResult.usage?.completionTokens || estimateTokens(aiResult.message),\n totalTokens:\n aiResult.usage?.totalTokens ||\n estimateTokens(JSON.stringify(modelMessages)) + estimateTokens(aiResult.message),\n };\n recordUsage(user.userId, usage.totalTokens);\n const answeredAt = nowIso();\n const assistantMessageId = crypto.randomUUID();\n statements.insertMessage.run({\n id: assistantMessageId,\n conversationId,\n role: 'assistant',\n content: aiResult.message,\n promptTokens: usage.promptTokens,\n completionTokens: usage.completionTokens,\n totalTokens: usage.totalTokens,\n createdAt: answeredAt,\n });\n statements.updateConversation.run({\n id: conversationId,\n userId: user.userId,\n title: isNewConversation ? titleFromMessage(message) : conversation.title,\n updatedAt: answeredAt,\n });\n updateAgentRunStatus({ id: runId, user_id: user.userId }, 'completed', {\n openClawRunId: aiResult.runId,\n inputTokens: usage.promptTokens,\n outputTokens: usage.completionTokens,\n });\n const action = determineClientAction(skillResult, message);\n return jsonResponse(res, 200, {\n success: true,\n runId,\n conversationId,\n messageId: assistantMessageId,\n provider: aiResult.provider,\n model: aiResult.model,\n message: aiResult.message,\n usage: agentUsagePayload(usage, user, budget),\n ...(action ? { clientAction: action } : {}),\n });\n } catch (error) {\n const status = error.name === 'AbortError' ? 'cancelled' : 'failed';\n updateAgentRunStatus({ id: runId, user_id: user.userId }, status, {\n errorCode: error.name || 'AGENT_RUN_FAILED',\n errorMessage: publicAiErrorMessage(error),\n });\n if (status === 'cancelled') {\n return jsonResponse(res, 499, { success: false, runId, status: 'cancelled' });\n }\n throw error;\n } finally {\n activeAgentRuns.delete(runId);\n }\n }),\n);\n\napp.get(\n '/api/agent/runs/:runId/events',\n asyncHandler(async (req, res) => {\n const user = await resolveUser(req, {});\n const run = statements.getAgentRun.get(req.params.runId, user.userId);\n if (!run) {\n return jsonResponse(res, 404, { success: false, message: 'run not found' });\n }\n if (run.status === 'completed' || run.status === 'failed' || run.status === 'cancelled') {\n return jsonResponse(res, 409, { success: false, message: `run is already ${run.status}` });\n }\n\n sseHeaders(res);\n const controller = new AbortController();\n activeAgentRuns.set(run.id, controller);\n updateAgentRunStatus(run, 'running');\n writeSse(res, 'run.started', { runId: run.id, conversationId: run.conversation_id });\n\n try {\n let streamedText = '';\n const modelMessages = buildAgentMessages(run.conversation_id);\n const lastUserMsg = [...modelMessages].reverse().find(m => m.role === 'user');\n const userMsgText = lastUserMsg ? lastUserMsg.content : '';\n const skillResult = userMsgText ? await runResourceFinderSkill({\n message: userMsgText,\n user,\n onStep: (step) => writeSse(res, 'tool.call.completed', {\n tool: 'agent.step',\n displayName: step.title || 'Agent step',\n summary: step.message || '',\n }),\n }) : null;\n const skillContext = resourceContextMessage(skillResult);\n const finalMessages = skillContext ? [\n modelMessages[0],\n { role: 'system', content: skillContext },\n ...modelMessages.slice(1)\n ] : modelMessages;\n\n const promptEstimate = estimateTokens(JSON.stringify(finalMessages));\n const estimated = promptEstimate + 600;\n const budget = enforceTokenBudget(user, estimated);\n const maxCompletionTokens = completionBudgetFor(budget, promptEstimate);\n const aiResult = await runAgentModel({\n messages: finalMessages,\n user,\n conversationId: run.conversation_id,\n mode: run.mode,\n signal: controller.signal,\n maxCompletionTokens,\n callbacks: {\n onStep: (step) => writeSse(res, 'tool.call.completed', {\n tool: 'agent.step',\n displayName: step.title || 'Agent step',\n summary: step.message || '',\n }),\n onToken: (delta) => {\n streamedText += delta;\n writeSse(res, 'assistant.delta', { text: delta });\n },\n },\n });\n const message = aiResult.message || streamedText.trim();\n const usage = {\n promptTokens: aiResult.usage?.promptTokens || estimateTokens(JSON.stringify(finalMessages)),\n completionTokens: aiResult.usage?.completionTokens || estimateTokens(message),\n totalTokens:\n aiResult.usage?.totalTokens || estimateTokens(JSON.stringify(finalMessages)) + estimateTokens(message),\n };\n recordUsage(user.userId, usage.totalTokens);\n const assistantMessageId = crypto.randomUUID();\n const answeredAt = nowIso();\n statements.insertMessage.run({\n id: assistantMessageId,\n conversationId: run.conversation_id,\n role: 'assistant',\n content: message,\n promptTokens: usage.promptTokens,\n completionTokens: usage.completionTokens,\n totalTokens: usage.totalTokens,\n createdAt: answeredAt,\n });\n statements.updateConversation.run({\n id: run.conversation_id,\n userId: user.userId,\n title: titleFromMessage(message),\n updatedAt: answeredAt,\n });\n updateAgentRunStatus(run, 'completed', {\n openClawRunId: aiResult.runId,\n inputTokens: usage.promptTokens,\n outputTokens: usage.completionTokens,\n });\n const action = determineClientAction(skillResult, userMsgText);\n writeSse(res, 'assistant.message', { messageId: assistantMessageId, content: message });\n writeSse(res, 'run.completed', {\n usage: agentUsagePayload(usage, user, budget),\n ...(action ? { clientAction: action } : {}),\n });\n res.end();\n } catch (error) {\n const status = error.name === 'AbortError' ? 'cancelled' : 'failed';\n updateAgentRunStatus(run, status, {\n errorCode: error.name || 'AGENT_RUN_FAILED',\n errorMessage: publicAiErrorMessage(error),\n });\n writeSse(res, status === 'cancelled' ? 'run.cancelled' : 'run.failed', {\n runId: run.id,\n message: publicAiErrorMessage(error),\n });\n res.end();\n } finally {\n activeAgentRuns.delete(run.id);\n }\n }),\n);\n\napp.post(\n '/api/agent/runs/:runId/cancel',\n asyncHandler(async (req, res) => {\n const user = await resolveUser(req, req.body || {});\n const run = statements.getAgentRun.get(req.params.runId, user.userId);\n if (!run) {\n return jsonResponse(res, 404, { success: false, message: 'run not found' });\n }\n const controller = activeAgentRuns.get(run.id);\n if (controller) controller.abort();\n updateAgentRunStatus(run, 'cancelled');\n return jsonResponse(res, 200, { success: true, runId: run.id, status: 'cancelled' });\n }),\n);\n\napp.post(\n '/api/agent/messages/:messageId/feedback',\n asyncHandler(async (req, res) => {\n const user = await resolveUser(req, req.body || {});\n const rating = safeUserText(req.body?.rating);\n if (!['up', 'down'].includes(rating)) {\n return jsonResponse(res, 400, { success: false, message: 'rating must be up or down' });\n }\n statements.insertAgentFeedback.run({\n id: crypto.randomUUID(),\n messageId: req.params.messageId,\n userId: user.userId,\n rating,\n reason: safeUserText(req.body?.reason),\n comment: safeUserText(req.body?.comment),\n createdAt: nowIso(),\n });\n return jsonResponse(res, 200, { success: true });\n }),\n);\n\napp.post(\n '/api/ai/chat',\n asyncHandler(async (req, res) => {\n const message = safeUserText(req.body?.message);\n if (!message) {\n return jsonResponse(res, 400, { success: false, message: 'message is required' });\n }\n\n const user = await resolveUser(req, req.body || {});\n const promptEstimate = estimateTokens(message);\n const estimated = promptEstimate + 600;\n const budget = enforceTokenBudget(user, estimated);\n const maxCompletionTokens = completionBudgetFor(budget, promptEstimate);\n const createdAt = nowIso();\n const conversationId = safeUserText(req.body?.conversationId) || crypto.randomUUID();\n const requestedModel = normalizeDeepSeekModelName(req.body?.model);\n const clientContext = clientContextMessage(req.body?.client);\n let conversation = statements.getConversation.get(conversationId, user.userId);\n const isNewConversation = !conversation;\n\n if (isNewConversation) {\n conversation = {\n id: conversationId,\n user_id: user.userId,\n title: titleFromMessage(message),\n };\n statements.insertConversation.run({\n id: conversationId,\n userId: user.userId,\n title: conversation.title,\n provider: 'deepseek',\n model: requestedModel,\n createdAt,\n updatedAt: createdAt,\n });\n }\n\n statements.insertMessage.run({\n id: crypto.randomUUID(),\n conversationId,\n role: 'user',\n content: message,\n promptTokens: 0,\n completionTokens: 0,\n totalTokens: 0,\n createdAt,\n });\n\n const libreChatRuntime = createLibreChatAgentRuntime();\n const skillResult = await runResourceFinderSkill({ message, user });\n const skillContext = resourceContextMessage(skillResult);\n const historyRows = statements.listMessages.all(conversationId).slice(-14);\n const modelMessages = [\n { role: 'system', content: systemPrompt },\n ...(clientContext ? [{ role: 'system', content: clientContext }] : []),\n ...(skillContext ? [{ role: 'system', content: skillContext }] : []),\n ...normalizeMessages(historyRows),\n ];\n\n const openClawRuntime = createOpenClawRuntime();\n const codexRuntime = createCodexDeepSeekRuntime();\n let provider = openClawRuntime.enabled\n ? openClawRuntime.provider\n : libreChatRuntime.enabled\n ? libreChatRuntime.provider\n : 'deepseek';\n let responseModel = openClawRuntime.enabled\n ? openClawRuntime.model\n : libreChatRuntime.enabled\n ? libreChatRuntime.model\n : requestedModel;\n let aiResult;\n try {\n if (openClawRuntime.enabled) {\n aiResult = await callOpenClawAgent({\n messages: modelMessages,\n user,\n conversationId,\n mode: normalizeAgentMode(req.body?.mode),\n });\n provider = aiResult.provider || provider;\n responseModel = aiResult.model || responseModel;\n } else if (libreChatRuntime.enabled) {\n aiResult = await callLibreChatAgent(modelMessages);\n } else if (codexRuntime.enabled) {\n provider = codexRuntime.provider;\n aiResult = await callCodexSdkDeepSeek(modelMessages);\n } else {\n aiResult = await callDeepSeek(modelMessages, { model: requestedModel, maxCompletionTokens });\n }\n } catch (error) {\n if (libreChatRuntime.enabled) {\n logger.warn({ error: String(error) }, 'LibreChat Agent chat failed, falling back to Codex SDK or DeepSeek direct');\n provider = codexRuntime.enabled ? codexRuntime.provider : 'deepseek';\n responseModel = requestedModel;\n if (codexRuntime.enabled) {\n try {\n aiResult = await callCodexSdkDeepSeek(modelMessages);\n } catch (codexError) {\n if (isDeepSeekQuotaError(codexError)) throw codexError;\n logger.warn({ error: String(codexError) }, 'Codex SDK chat failed, falling back to DeepSeek direct');\n provider = 'deepseek';\n aiResult = await callDeepSeek(modelMessages, { model: requestedModel, maxCompletionTokens });\n }\n } else {\n aiResult = await callDeepSeek(modelMessages, { model: requestedModel, maxCompletionTokens });\n }\n } else {\n if (!codexRuntime.enabled || isDeepSeekQuotaError(error)) throw error;\n logger.warn({ error: String(error) }, 'Codex SDK chat failed, falling back to DeepSeek direct');\n provider = 'deepseek';\n responseModel = requestedModel;\n aiResult = await callDeepSeek(modelMessages, { model: requestedModel, maxCompletionTokens });\n }\n }\n const usage = {\n promptTokens: aiResult.usage.promptTokens || estimateTokens(JSON.stringify(modelMessages)),\n completionTokens: aiResult.usage.completionTokens || estimateTokens(aiResult.message),\n totalTokens:\n aiResult.usage.totalTokens ||\n estimateTokens(JSON.stringify(modelMessages)) + estimateTokens(aiResult.message),\n };\n recordUsage(user.userId, usage.totalTokens);\n\n const answeredAt = nowIso();\n statements.insertMessage.run({\n id: crypto.randomUUID(),\n conversationId,\n role: 'assistant',\n content: aiResult.message,\n promptTokens: usage.promptTokens,\n completionTokens: usage.completionTokens,\n totalTokens: usage.totalTokens,\n createdAt: answeredAt,\n });\n statements.updateConversation.run({\n id: conversationId,\n userId: user.userId,\n title: isNewConversation ? titleFromMessage(message) : conversation.title,\n updatedAt: answeredAt,\n });\n\n const latestUsage = usageFor(user.userId);\n const monthlyLimit = budget.limit;\n const action = determineClientAction(skillResult, message);\n jsonResponse(res, 200, {\n success: true,\n conversationId,\n provider,\n model: responseModel,\n message: aiResult.message,\n usage: {\n ...usage,\n monthlyLimit,\n remainingTokens: Math.max(0, monthlyLimit - latestUsage.used),\n },\n ...(action ? { clientAction: action } : {}),\n });\n }),\n);\n\napp.post('/api/ai/chat/stream', async (req, res) => {\n sseHeaders(res);\n\n try {\n const message = safeUserText(req.body?.message);\n if (!message) {\n writeSse(res, 'error', { message: 'message is required' });\n res.end();\n return;\n }\n\n const user = await resolveUser(req, req.body || {});\n const promptEstimate = estimateTokens(message);\n const estimated = promptEstimate + 600;\n const budget = enforceTokenBudget(user, estimated);\n const maxCompletionTokens = completionBudgetFor(budget, promptEstimate);\n const createdAt = nowIso();\n const conversationId = safeUserText(req.body?.conversationId) || crypto.randomUUID();\n const requestedModel = normalizeDeepSeekModelName(req.body?.model);\n const clientContext = clientContextMessage(req.body?.client);\n let conversation = statements.getConversation.get(conversationId, user.userId);\n const isNewConversation = !conversation;\n\n if (isNewConversation) {\n conversation = {\n id: conversationId,\n user_id: user.userId,\n title: titleFromMessage(message),\n };\n statements.insertConversation.run({\n id: conversationId,\n userId: user.userId,\n title: conversation.title,\n provider: 'deepseek',\n model: requestedModel,\n createdAt,\n updatedAt: createdAt,\n });\n }\n\n statements.insertMessage.run({\n id: crypto.randomUUID(),\n conversationId,\n role: 'user',\n content: message,\n promptTokens: 0,\n completionTokens: 0,\n totalTokens: 0,\n createdAt,\n });\n\n const codexRuntime = createCodexDeepSeekRuntime();\n const libreChatRuntime = createLibreChatAgentRuntime();\n const initialOpenClawRuntime = createOpenClawRuntime();\n let provider = initialOpenClawRuntime.enabled\n ? initialOpenClawRuntime.provider\n : libreChatRuntime.enabled\n ? libreChatRuntime.provider\n : codexRuntime.enabled\n ? codexRuntime.provider\n : 'deepseek';\n let responseModel = initialOpenClawRuntime.enabled\n ? initialOpenClawRuntime.model\n : libreChatRuntime.enabled\n ? libreChatRuntime.model\n : requestedModel;\n writeSse(res, 'meta', {\n conversationId,\n provider,\n model: responseModel,\n });\n const skillResult = await runResourceFinderSkill({\n message,\n user,\n onStep: (step) => writeSse(res, 'step', step),\n });\n const skillContext = resourceContextMessage(skillResult);\n const historyRows = statements.listMessages.all(conversationId).slice(-14);\n const modelMessages = [\n { role: 'system', content: systemPrompt },\n ...(clientContext ? [{ role: 'system', content: clientContext }] : []),\n ...(skillContext ? [{ role: 'system', content: skillContext }] : []),\n ...normalizeMessages(historyRows),\n ];\n\n let aiResult;\n const openClawRuntime = initialOpenClawRuntime;\n try {\n if (openClawRuntime.enabled) {\n aiResult = await callOpenClawAgent({\n messages: modelMessages,\n user,\n conversationId,\n mode: normalizeAgentMode(req.body?.mode),\n });\n provider = aiResult.provider || provider;\n responseModel = aiResult.model || responseModel;\n writeSse(res, 'delta', { text: aiResult.message });\n } else if (libreChatRuntime.enabled) {\n aiResult = await callLibreChatAgentStream(modelMessages, {\n onToken: (token) => writeSse(res, 'delta', { text: token }),\n });\n } else if (codexRuntime.enabled) {\n aiResult = await callCodexSdkDeepSeek(modelMessages, {\n onToken: (token) => writeSse(res, 'delta', { text: token }),\n onStep: (step) => writeSse(res, 'step', step),\n });\n } else {\n aiResult = await callDeepSeekStream(modelMessages, {\n model: requestedModel,\n maxCompletionTokens,\n onToken: (token) => writeSse(res, 'delta', { text: token }),\n });\n }\n } catch (error) {\n if (libreChatRuntime.enabled) {\n logger.warn({ error: String(error) }, 'LibreChat Agent streaming chat failed, falling back to Codex SDK or DeepSeek direct');\n provider = codexRuntime.enabled ? codexRuntime.provider : 'deepseek';\n responseModel = requestedModel;\n writeSse(res, 'meta', {\n conversationId,\n provider,\n model: responseModel,\n });\n if (codexRuntime.enabled) {\n try {\n aiResult = await callCodexSdkDeepSeek(modelMessages, {\n onToken: (token) => writeSse(res, 'delta', { text: token }),\n onStep: (step) => writeSse(res, 'step', step),\n });\n } catch (codexError) {\n if (isDeepSeekQuotaError(codexError)) throw codexError;\n logger.warn({ error: String(codexError) }, 'Codex SDK streaming chat failed, falling back to DeepSeek direct');\n provider = 'deepseek';\n responseModel = requestedModel;\n writeSse(res, 'meta', {\n conversationId,\n provider,\n model: responseModel,\n });\n aiResult = await callDeepSeekStream(modelMessages, {\n model: requestedModel,\n maxCompletionTokens,\n onToken: (token) => writeSse(res, 'delta', { text: token }),\n });\n }\n } else {\n aiResult = await callDeepSeekStream(modelMessages, {\n model: requestedModel,\n maxCompletionTokens,\n onToken: (token) => writeSse(res, 'delta', { text: token }),\n });\n }\n } else {\n if (!codexRuntime.enabled || isDeepSeekQuotaError(error)) throw error;\n logger.warn({ error: String(error) }, 'Codex SDK streaming chat failed, falling back to DeepSeek direct');\n provider = 'deepseek';\n responseModel = requestedModel;\n writeSse(res, 'meta', {\n conversationId,\n provider,\n model: responseModel,\n });\n aiResult = await callDeepSeekStream(modelMessages, {\n model: requestedModel,\n maxCompletionTokens,\n onToken: (token) => writeSse(res, 'delta', { text: token }),\n });\n }\n }\n\n const usage = {\n promptTokens: aiResult.usage.promptTokens || estimateTokens(JSON.stringify(modelMessages)),\n completionTokens: aiResult.usage.completionTokens || estimateTokens(aiResult.message),\n totalTokens:\n aiResult.usage.totalTokens ||\n estimateTokens(JSON.stringify(modelMessages)) + estimateTokens(aiResult.message),\n };\n recordUsage(user.userId, usage.totalTokens);\n\n const answeredAt = nowIso();\n statements.insertMessage.run({\n id: crypto.randomUUID(),\n conversationId,\n role: 'assistant',\n content: aiResult.message,\n promptTokens: usage.promptTokens,\n completionTokens: usage.completionTokens,\n totalTokens: usage.totalTokens,\n createdAt: answeredAt,\n });\n statements.updateConversation.run({\n id: conversationId,\n userId: user.userId,\n title: isNewConversation ? titleFromMessage(message) : conversation.title,\n updatedAt: answeredAt,\n });\n\n const latestUsage = usageFor(user.userId);\n const monthlyLimit = budget.limit;\n const action = determineClientAction(skillResult, message);\n writeSse(res, 'done', {\n conversationId,\n provider,\n model: responseModel,\n message: aiResult.message,\n usage: {\n ...usage,\n monthlyLimit,\n remainingTokens: Math.max(0, monthlyLimit - latestUsage.used),\n },\n ...(action ? { clientAction: action } : {}),\n });\n res.end();\n } catch (error) {\n logger.error({ error: error.stack || String(error) }, 'streaming chat failed');\n writeSse(res, 'error', {\n message: publicAiErrorMessage(error),\n details: error.details,\n });\n res.end();\n }\n});\n\napp.get(\n '/api/ai/conversations',\n asyncHandler(async (req, res) => {\n const user = await resolveUser(req, { username: req.query.username });\n const items = statements.listConversations.all(user.userId).map((item) => ({\n id: item.id,\n title: item.title,\n updatedAt: item.updatedAt,\n }));\n jsonResponse(res, 200, { success: true, items });\n }),\n);\n\napp.get(\n '/api/ai/conversations/:id',\n asyncHandler(async (req, res) => {\n const user = await resolveUser(req);\n const conversation = statements.getConversation.get(req.params.id, user.userId);\n if (!conversation) {\n return jsonResponse(res, 404, { success: false, message: 'conversation not found' });\n }\n const messages = statements.listMessages.all(req.params.id).map((item) => ({\n role: item.role,\n content: item.content,\n createdAt: item.createdAt,\n }));\n jsonResponse(res, 200, {\n success: true,\n conversation: {\n id: conversation.id,\n title: conversation.title,\n updatedAt: conversation.updated_at,\n },\n messages,\n });\n }),\n);\n\nfunction buildUrl(base, endpoint, params = {}) {\n const url = new URL(endpoint, `${base}/`);\n for (const [key, value] of Object.entries(params)) {\n if (Array.isArray(value)) {\n for (const item of value) {\n if (item !== undefined && item !== null && item !== '') {\n url.searchParams.append(key, String(item));\n }\n }\n } else if (value !== undefined && value !== null && value !== '') {\n url.searchParams.set(key, String(value));\n }\n }\n return url;\n}\n\nasync function fetchJson(url) {\n const response = await fetch(url, {\n headers: { Accept: 'application/json' },\n signal: AbortSignal.timeout(15_000),\n });\n if (!response.ok) {\n throw new Error(`Request failed: ${response.status}`);\n }\n return response.json();\n}\n\nfunction stripHtml(value) {\n return String(value || '')\n .replace(/<script[\\s\\S]*?<\\/script>/gi, ' ')\n .replace(/<style[\\s\\S]*?<\\/style>/gi, ' ')\n .replace(/<[^>]+>/g, ' ')\n .replace(/ /g, ' ')\n .replace(/</g, '<')\n .replace(/>/g, '>')\n .replace(/&/g, '&')\n .replace(/&#(\\d+);/g, (_match, code) => String.fromCharCode(Number(code)))\n .replace(/\\s+/g, ' ')\n .trim();\n}\n\nfunction cleanExtractedText(value) {\n let text = String(value || '')\n .replace(/\\b[A-Z]{1,3}\\d{2}n\\d{4,5}[a-z]?_p?\\d{4}[a-z]\\d{2}\\b/g, ' ')\n .replace(/\\uFFFD/g, '')\n .replace(/\\s+([,。;:!?、」』)》】])/g, '$1')\n .replace(/([「『《(【])\\s+/g, '$1');\n for (let i = 0; i < 3; i += 1) {\n text = text.replace(/([\\u3400-\\u9fff])\\s+([\\u3400-\\u9fff])/g, '$1$2');\n }\n return text.replace(/[ \\t]{2,}/g, ' ').trim();\n}\n\nfunction compactText(value, limit = 140) {\n const text = stripHtml(value);\n return text.length > limit ? `${text.slice(0, limit)}...` : text;\n}\n\nfunction collectSearchResults(data) {\n if (!data || typeof data !== 'object') return [];\n if (Array.isArray(data.results)) return data.results;\n if (Array.isArray(data?.results?.docs)) return data.results.docs;\n if (Array.isArray(data?.response?.docs)) return data.response.docs;\n if (Array.isArray(data.docs)) return data.docs;\n return [];\n}\n\nfunction firstText(item, keys) {\n for (const key of keys) {\n const value = item?.[key];\n if (Array.isArray(value)) {\n const first = value.map((entry) => safeUserText(String(entry))).find(Boolean);\n if (first) return first;\n }\n if (typeof value === 'string' && value.trim()) return value.trim();\n if (typeof value === 'number') return String(value);\n }\n return '';\n}\n\nfunction firstNumber(item, keys, fallback = undefined) {\n for (const key of keys) {\n const value = item?.[key];\n if (typeof value === 'number') return value;\n if (typeof value === 'string' && value.trim() && Number.isFinite(Number(value))) {\n return Number(value);\n }\n }\n return fallback;\n}\n\nasync function searchCbetaTitle(query, limit) {\n const url = buildUrl(cbetaApiRoot, 'search/title', { q: query, start: 0, rows: limit });\n const data = await fetchJson(url);\n return collectSearchResults(data).flatMap((item) => {\n const work = firstText(item, ['work', 'work_id', 'workId']);\n const title = firstText(item, ['content', 'title', 'work_title', 'workTitle']);\n if (!work || !title) return [];\n const juan = firstNumber(item, ['juan', 'juan_num', 'juanNum'], 1);\n return [\n {\n id: `cbeta-title-${work}-${juan}`,\n title,\n sourceName: 'CBETA 佛典',\n url: `cbeta:${work}:${juan}`,\n snippet: firstText(item, ['byline', 'creators', 'time_dynasty']) || '可下载经文正文并用于全球法布施。',\n resourceType: 'scripture',\n work,\n juan,\n },\n ];\n });\n}\n\nasync function searchCbetaContent(query, limit) {\n const endpoints = ['search/fulltext', 'search/content', 'search/all_in_one'];\n const settled = await Promise.allSettled(\n endpoints.map((endpoint) =>\n fetchJson(buildUrl(cbetaApiRoot, endpoint, { q: query, start: 0, rows: limit, field: 'content' })),\n ),\n );\n const results = [];\n for (const response of settled) {\n if (response.status !== 'fulfilled') continue;\n for (const item of collectSearchResults(response.value)) {\n const work = firstText(item, ['work', 'work_id', 'workId', 'sutra', 'id']);\n const snippet = compactText(firstText(item, ['kwic', 'highlight', 'snippet', 'content', 'text', 'body', 'p']));\n if (!work || !snippet) continue;\n const juan = firstNumber(item, ['juan', 'juan_num', 'juanNum', '卷'], 1);\n const title = firstText(item, ['title', 'work_title', 'workTitle', 'sutra_name', 'book', 'name']) || work;\n results.push({\n id: `cbeta-content-${work}-${juan}-${sha256(snippet).slice(0, 8)}`,\n title,\n sourceName: 'CBETA 佛典',\n url: `cbeta:${work}:${juan}`,\n snippet,\n resourceType: 'scripture',\n work,\n juan,\n });\n if (results.length >= limit) return results;\n }\n }\n return results;\n}\n\nfunction directUrlResource(query) {\n const url = safeUserText(query);\n let parsed;\n try {\n parsed = new URL(url);\n } catch {\n return null;\n }\n if (!['http:', 'https:'].includes(parsed.protocol)) return null;\n return {\n id: `url-${sha256(url).slice(0, 12)}`,\n title: parsed.hostname,\n sourceName: '网页资源',\n url,\n snippet: '直接下载并提取这个链接的可读正文。',\n resourceType: 'web',\n };\n}\n\nfunction looksLikeAudioQuery(query) {\n return /音乐|音樂|音频|音訊|mp3|audio|念诵|念誦|诵经|誦經|唱诵|唱誦|读诵|讀誦/.test(\n safeUserText(query).toLowerCase(),\n );\n}\n\nfunction archiveSearchTerm(query) {\n return (\n safeUserText(query)\n .replace(/下载|查找|寻找|找一?个|资源|音频|音訊|音乐|音樂|mp3|audio|念诵|念誦|诵经|誦經|唱诵|唱誦|读诵|讀誦/gi, ' ')\n .replace(/\\s+/g, ' ')\n .trim() || safeUserText(query)\n );\n}\n\nfunction solrPhrase(value) {\n return String(value).replace(/\\\\/g, '\\\\\\\\').replace(/\"/g, '\\\\\"');\n}\n\nasync function searchInternetArchiveAudio(query, limit) {\n if (!looksLikeAudioQuery(query)) return [];\n const term = archiveSearchTerm(query);\n if (!term) return [];\n const variants = Array.from(\n new Set([\n term,\n term.replaceAll('金刚经', '金剛經'),\n term.replaceAll('金剛經', '金刚经'),\n ].filter(Boolean)),\n );\n const phraseQuery = variants\n .flatMap((item) => [`title:\"${solrPhrase(item)}\"`, `description:\"${solrPhrase(item)}\"`])\n .join(' OR ');\n const url = buildUrl('https://archive.org', 'advancedsearch.php', {\n q: `(${phraseQuery}) AND mediatype:audio`,\n rows: Math.min(Math.max(limit, 1), 10),\n output: 'json',\n 'fl[]': ['identifier', 'title', 'description'],\n });\n const data = await fetchJson(url);\n const docs = Array.isArray(data?.response?.docs) ? data.response.docs : [];\n return docs\n .map((item) => {\n const identifier = firstText(item, ['identifier']);\n const title = firstText(item, ['title']) || identifier;\n if (!identifier || !title) return null;\n return {\n id: `ia-audio-${identifier}`,\n title,\n sourceName: 'Internet Archive 音频',\n url: `ia:${identifier}`,\n snippet: compactText(firstText(item, ['description'])) || '公开可访问的音频资源,将下载第一个可用 MP3 文件。',\n resourceType: 'audio',\n identifier,\n };\n })\n .filter(Boolean);\n}\n\nconst curatedCbetaWorks = [\n {\n work: 'T0235',\n juan: 1,\n title: '金剛般若波羅蜜經',\n keywords: ['金剛般若波羅蜜經', '金刚般若波罗蜜经', '金剛經', '金刚经'],\n snippet: '姚秦 鳩摩羅什譯。常用于读诵、共修与法布施的般若经典。',\n },\n {\n work: 'T0251',\n juan: 1,\n title: '般若波羅蜜多心經',\n keywords: ['般若波羅蜜多心經', '般若波罗蜜多心经', '心經', '心经'],\n snippet: '唐 玄奘譯。篇幅短,适合加入禅室功课本与日常持诵。',\n },\n {\n work: 'T0262',\n juan: 1,\n title: '妙法蓮華經',\n keywords: ['妙法蓮華經', '妙法莲华经', '法華經', '法华经'],\n snippet: '姚秦 鳩摩羅什譯。可按卷下载正文,用于长期功课与分享。',\n },\n {\n work: 'T0279',\n juan: 1,\n title: '大方廣佛華嚴經',\n keywords: ['大方廣佛華嚴經', '大方广佛华严经', '華嚴經', '华严经'],\n snippet: '唐 實叉難陀譯。可按卷下载正文,用于系统研读与法布施。',\n },\n {\n work: 'T0360',\n juan: 1,\n title: '佛說無量壽經',\n keywords: ['佛說無量壽經', '佛说无量寿经', '無量壽經', '无量寿经'],\n snippet: '净土经典。可下载正文,用于净土功课本与全球法布施。',\n },\n {\n work: 'T0365',\n juan: 1,\n title: '佛說觀無量壽佛經',\n keywords: ['佛說觀無量壽佛經', '佛说观无量寿佛经', '觀無量壽經', '观无量寿经'],\n snippet: '净土三经之一。适合查找下载后加入净土功课。',\n },\n {\n work: 'T0366',\n juan: 1,\n title: '佛說阿彌陀經',\n keywords: ['佛說阿彌陀經', '佛说阿弥陀经', '阿彌陀經', '阿弥陀经'],\n snippet: '姚秦 鳩摩羅什譯。篇幅适中,适合共修、持诵与分享。',\n },\n {\n work: 'T0475',\n juan: 1,\n title: '維摩詰所說經',\n keywords: ['維摩詰所說經', '维摩诘所说经', '維摩詰經', '维摩诘经'],\n snippet: '姚秦 鳩摩羅什譯。大乘经典,可按卷下载用于研读。',\n },\n {\n work: 'T0676',\n juan: 1,\n title: '解深密經',\n keywords: ['解深密經', '解深密经'],\n snippet: '唯识相关大乘经典。适合研读、整理笔记与法布施。',\n },\n {\n work: 'T1666',\n juan: 1,\n title: '大乘起信論',\n keywords: ['大乘起信論', '大乘起信论', '起信論', '起信论'],\n snippet: '大乘论典。适合下载后作为研习功课资料。',\n },\n];\n\nfunction localCbetaResults(query, limit) {\n const normalizedQuery = safeUserText(query).toLowerCase();\n if (!normalizedQuery) return [];\n return curatedCbetaWorks\n .filter((item) =>\n item.keywords.some((keyword) => {\n const normalizedKeyword = keyword.toLowerCase();\n return normalizedKeyword.includes(normalizedQuery) || normalizedQuery.includes(normalizedKeyword);\n }),\n )\n .slice(0, limit)\n .map((item) => ({\n id: `cbeta-curated-${item.work}-${item.juan}`,\n title: item.title,\n sourceName: 'CBETA 佛典',\n url: `cbeta:${item.work}:${item.juan}`,\n snippet: item.snippet,\n resourceType: 'scripture',\n work: item.work,\n juan: item.juan,\n }));\n}\n\nfunction uniqueResources(items, limit) {\n const seen = new Set();\n const results = [];\n for (const item of items) {\n const key = `${item.url}:${item.title}`;\n if (seen.has(key)) continue;\n seen.add(key);\n results.push(item);\n if (results.length >= limit) break;\n }\n return results;\n}\n\nasync function findResourceCandidates(query, limit) {\n const direct = directUrlResource(query);\n const results = [];\n if (direct) results.push(direct);\n try {\n results.push(...(await searchInternetArchiveAudio(query, limit)));\n } catch (error) {\n logger.warn({ error: String(error), query }, 'Internet Archive audio search skipped');\n }\n results.push(...localCbetaResults(query, limit));\n\n const [titleResults, contentResults] = await Promise.allSettled([\n searchCbetaTitle(query, limit),\n searchCbetaContent(query, limit),\n ]);\n if (titleResults.status === 'fulfilled') results.push(...titleResults.value);\n if (contentResults.status === 'fulfilled') results.push(...contentResults.value);\n\n return uniqueResources(results, limit);\n}\n\nfunction looksLikeResourceTask(message) {\n const text = safeUserText(message).toLowerCase();\n if (!text) return false;\n return /下载|查找|寻找|找一?个|资源|經文|经文|佛经|佛經|仪轨|儀軌|音乐|音樂|mp3|audio|念诵|念誦|诵经|誦經/.test(text);\n}\n\nfunction extractResourceQuery(message) {\n const text = safeUserText(message)\n .replace(/请|請|帮我|幫我|麻烦|麻煩|可以|能不能|需要|我要|想要/g, ' ')\n .replace(/下载|下載|查找|寻找|尋找|找一个|找一個|找|资源|資源|并|並|然后|然後|用于|用於|全球法布施|加入功课本|加入功課本/g, ' ')\n .replace(/\\s+/g, ' ')\n .trim();\n return text || safeUserText(message);\n}\n\nfunction resourceContextMessage(skillResult) {\n if (!skillResult) return '';\n const lines = [\n '本轮资源检索/下载已经由大乘 App 后端完成。',\n '不要再次调用 search_dharma_resources、download_dharma_resource 或其他资源 MCP 工具;请直接基于以下结果回答用户。',\n `后端已调用 skill: ${skillResult.skillName}`,\n `执行目标: ${skillResult.query}`,\n ];\n if (skillResult.selected) {\n lines.push(`候选资源: ${skillResult.selected.title}`);\n lines.push(`来源: ${skillResult.selected.sourceName}`);\n lines.push(`资源地址: ${skillResult.selected.url}`);\n }\n if (skillResult.downloaded) {\n lines.push(`下载标题: ${skillResult.downloaded.title}`);\n lines.push(`下载文件: ${skillResult.downloaded.fileName}`);\n const text = safeUserText(skillResult.downloaded.contentText);\n if (text) {\n lines.push('下载正文摘录:');\n lines.push(text.slice(0, 8000));\n }\n }\n return lines.join('\\n');\n}\n\nfunction determineClientAction(skillResult, message) {\n if (skillResult && skillResult.downloaded) {\n const text = safeUserText(message);\n if (/法布施|全球法布施|分享/.test(text)) {\n return {\n type: 'prepare_dharma_share_text',\n title: skillResult.downloaded.title,\n text: skillResult.downloaded.contentText || '',\n };\n }\n }\n return null;\n}\n\nasync function runResourceFinderSkill({ message, user, onStep }) {\n if (!looksLikeResourceTask(message)) return null;\n\n const skillName = 'resource-finder-downloader';\n const query = extractResourceQuery(message);\n const audioTask = looksLikeAudioQuery(message) || looksLikeAudioQuery(query);\n onStep?.({\n type: 'skill.step',\n skillName,\n stage: 'start',\n title: '调用资源查找下载 skill',\n message: `识别到资源任务: ${query}`,\n });\n\n onStep?.({\n type: 'skill.step',\n skillName,\n stage: 'search',\n title: '搜索可分享资源',\n message: audioTask\n ? '优先检索公开可访问音频资源,失败时再回落到佛典正文资源。'\n : '优先检索 CBETA 佛典正文接口,搜索索引不可用时使用内置经典映射。',\n });\n const candidates = await findResourceCandidates(query, 8);\n if (candidates.length === 0) {\n onStep?.({\n type: 'skill.step',\n skillName,\n stage: 'empty',\n title: '没有找到可下载资源',\n message: '后端没有找到明确可下载且可分享的资源。',\n });\n return {\n skillName,\n query,\n candidates: [],\n selected: null,\n downloaded: null,\n };\n }\n\n const selected = candidates[0];\n onStep?.({\n type: 'skill.step',\n skillName,\n stage: 'select',\n title: '选择可信候选资源',\n message: `${selected.title} / ${selected.sourceName}`,\n });\n\n onStep?.({\n type: 'skill.step',\n skillName,\n stage: 'download',\n title: '下载并提取资源',\n message: selected.url,\n });\n const cbeta = parseCbetaResource(selected);\n const internetArchive = parseInternetArchiveResource(selected);\n const downloaded = cbeta\n ? await downloadCbetaResource({ ...selected, ...cbeta })\n : internetArchive\n ? await downloadInternetArchiveAudio({ ...selected, ...internetArchive })\n : await downloadWebResource(selected);\n const persisted = persistDownloadedResource(user, downloaded);\n\n onStep?.({\n type: 'skill.step',\n skillName,\n stage: 'done',\n title: '资源已准备完成',\n message: downloaded.fileName,\n });\n\n return {\n skillName,\n query,\n candidates,\n selected,\n downloaded: {\n ...downloaded,\n downloadId: persisted.id,\n },\n };\n}\n\napp.post(\n '/api/resources/search',\n asyncHandler(async (req, res) => {\n const query = safeUserText(req.body?.query);\n const limit = Math.min(Math.max(Number(req.body?.limit || 12), 1), 20);\n if (query.length < 2) {\n return jsonResponse(res, 400, { success: false, message: 'query is required' });\n }\n\n const items = await findResourceCandidates(query, limit);\n\n jsonResponse(res, 200, {\n success: true,\n source: 'resource-index',\n items,\n });\n }),\n);\n\nfunction parseCbetaResource(input) {\n const work = safeUserText(input.work);\n const juan = Number(input.juan || 1);\n if (work) return { work, juan: Number.isFinite(juan) ? juan : 1 };\n\n const url = safeUserText(input.url);\n const match = /^cbeta:([^:]+):(\\d+)$/i.exec(url);\n if (!match) return null;\n return {\n work: match[1].toUpperCase(),\n juan: Number(match[2] || 1),\n };\n}\n\nfunction parseInternetArchiveResource(input) {\n const url = safeUserText(input.url);\n const match = /^ia:([^:]+)$/i.exec(url);\n const identifier = match?.[1] || safeUserText(input.identifier);\n if (!identifier) return null;\n return { identifier };\n}\n\nfunction safeFileName(value) {\n const normalized = String(value || 'dharma-resource')\n .replace(/[\\\\/:*?\"<>|]+/g, '-')\n .replace(/\\s+/g, '-')\n .replace(/-+/g, '-')\n .replace(/^-|-$/g, '')\n .slice(0, 80);\n return normalized || 'dharma-resource';\n}\n\nfunction extractCbetaHtml(data) {\n const first = Array.isArray(data?.results) ? data.results[0] : null;\n if (typeof first === 'string') return first;\n if (first && typeof first === 'object' && typeof first.html === 'string') return first.html;\n if (typeof data?.html === 'string') return data.html;\n return '';\n}\n\nasync function downloadCbetaResource(resource) {\n const url = buildUrl(cbetaApiRoot, 'juans', {\n work: resource.work,\n juan: resource.juan,\n work_info: 1,\n toc: 1,\n });\n const data = await fetchJson(url);\n const workInfo = data.work_info || {};\n const title = safeUserText(workInfo.title) || safeUserText(resource.title) || resource.work;\n const html = extractCbetaHtml(data);\n const text = cleanExtractedText(stripHtml(html));\n if (!text) throw new Error('CBETA resource returned empty content');\n return {\n title,\n sourceName: 'CBETA 佛典',\n url: `cbeta:${resource.work}:${resource.juan}`,\n contentText: text.slice(0, maxResourceTextChars),\n fileName: `${resource.work}_${resource.juan}_${safeFileName(title)}.txt`,\n };\n}\n\nasync function downloadInternetArchiveAudio(resource) {\n const identifier = safeUserText(resource.identifier);\n if (!identifier) throw new Error('Internet Archive identifier is required');\n const metadata = await fetchJson(`https://archive.org/metadata/${encodeURIComponent(identifier)}`);\n const title = safeUserText(metadata?.metadata?.title) || safeUserText(resource.title) || identifier;\n const files = Array.isArray(metadata?.files) ? metadata.files : [];\n const audioFile = files.find((file) => {\n const name = safeUserText(file?.name);\n const format = safeUserText(file?.format).toLowerCase();\n return /\\.(mp3|m4a|ogg)$/i.test(name) || format.includes('mp3');\n });\n if (!audioFile?.name) {\n throw new Error('Internet Archive audio item has no downloadable audio file');\n }\n const sourceFileName = audioFile.name;\n const fileUrl = `https://archive.org/download/${encodeURIComponent(identifier)}/${sourceFileName\n .split('/')\n .map((part) => encodeURIComponent(part))\n .join('/')}`;\n const response = await fetch(fileUrl, {\n headers: {\n Accept: 'audio/mpeg, audio/*;q=0.9, */*;q=0.5',\n 'User-Agent': 'DachengResourceDownloader/1.0 (+https://ombhrum.com)',\n },\n signal: AbortSignal.timeout(60_000),\n });\n if (!response.ok) {\n throw new Error(`Internet Archive audio download failed: ${response.status}`);\n }\n const binaryContent = Buffer.from(await response.arrayBuffer());\n if (!binaryContent.length) throw new Error('Internet Archive audio returned empty content');\n const sizeMb = (binaryContent.length / 1024 / 1024).toFixed(1);\n return {\n title,\n sourceName: 'Internet Archive 音频',\n url: fileUrl,\n contentText: `已下载音频资源: ${title}\\n文件名: ${sourceFileName}\\n大小: ${sizeMb} MB\\n来源: ${fileUrl}`,\n binaryContent,\n fileName: safeFileName(sourceFileName),\n };\n}\n\nasync function downloadWebResource(input) {\n const url = safeUserText(input.url);\n const parsed = new URL(url);\n if (!['http:', 'https:'].includes(parsed.protocol)) {\n throw new Error('Only http/https resources can be downloaded');\n }\n\n const response = await fetch(parsed, {\n headers: {\n Accept: 'text/html, text/plain, application/xhtml+xml, application/xml;q=0.8, */*;q=0.5',\n 'User-Agent': 'DachengResourceDownloader/1.0 (+https://ombhrum.com)',\n },\n signal: AbortSignal.timeout(25_000),\n });\n if (!response.ok) throw new Error(`Resource download failed: ${response.status}`);\n const contentType = response.headers.get('content-type') || '';\n const body = await response.text();\n const title = safeUserText(input.title) || parsed.hostname;\n const contentText = contentType.includes('html') ? cleanExtractedText(stripHtml(body)) : body.trim();\n if (!contentText) throw new Error('Resource returned empty content');\n return {\n title,\n sourceName: safeUserText(input.sourceName) || parsed.hostname,\n url,\n contentText: contentText.slice(0, maxResourceTextChars),\n fileName: `${safeFileName(title)}.txt`,\n };\n}\n\nfunction persistDownloadedResource(user, content) {\n const id = crypto.randomUUID();\n const fileName = `${id}-${safeFileName(content.fileName)}`;\n const filePath = path.join(resourcesDir, fileName);\n if (content.binaryContent) {\n fs.writeFileSync(filePath, content.binaryContent);\n } else {\n fs.writeFileSync(filePath, content.contentText, 'utf8');\n }\n statements.insertResourceDownload.run({\n id,\n userId: user.userId,\n title: content.title,\n sourceName: content.sourceName,\n sourceUrl: content.url,\n fileName: content.fileName,\n filePath,\n createdAt: nowIso(),\n });\n return { id, fileName, filePath };\n}\n\nfunction createLibreChatMcpServer() {\n const server = new McpServer({\n name: 'dacheng-ai-tools',\n version: '0.1.0',\n });\n\n server.registerTool(\n 'search_dharma_resources',\n {\n title: 'Search dharma resources',\n description:\n 'Search for legally shareable Buddhist scripture, text, web, or audio resources that can be prepared for Fabushi sharing.',\n inputSchema: {\n query: z.string().min(2).max(200),\n limit: z.number().int().min(1).max(12).optional(),\n },\n outputSchema: {\n query: z.string(),\n items: z.array(\n z.object({\n id: z.string(),\n title: z.string(),\n sourceName: z.string(),\n url: z.string(),\n snippet: z.string(),\n resourceType: z.string(),\n work: z.string().optional(),\n juan: z.number().optional(),\n }),\n ),\n },\n annotations: {\n readOnlyHint: true,\n destructiveHint: false,\n openWorldHint: true,\n },\n },\n async ({ query, limit }) => {\n const items = await findResourceCandidates(query, limit ?? 8);\n return textToolResult(`Found ${items.length} candidate resources for \"${query}\".`, {\n query,\n items,\n });\n },\n );\n\n server.registerTool(\n 'download_dharma_resource',\n {\n title: 'Download dharma resource',\n description:\n 'Download and extract a selected resource. Use the url/work/juan returned by search_dharma_resources whenever possible.',\n inputSchema: {\n url: z.string().min(1).max(1000),\n title: z.string().max(300).optional(),\n sourceName: z.string().max(120).optional(),\n work: z.string().max(40).optional(),\n juan: z.number().int().min(1).optional(),\n identifier: z.string().max(300).optional(),\n },\n outputSchema: {\n title: z.string(),\n sourceName: z.string(),\n url: z.string(),\n fileName: z.string(),\n downloadId: z.string(),\n contentText: z.string(),\n },\n annotations: {\n readOnlyHint: false,\n destructiveHint: false,\n openWorldHint: true,\n },\n },\n async (input) => {\n const user = { userId: 'librechat:mcp' };\n const cbeta = parseCbetaResource(input);\n const internetArchive = parseInternetArchiveResource(input);\n const content = cbeta\n ? await downloadCbetaResource({ ...input, ...cbeta })\n : internetArchive\n ? await downloadInternetArchiveAudio({ ...input, ...internetArchive })\n : await downloadWebResource(input);\n const persisted = persistDownloadedResource(user, content);\n const { binaryContent: _binaryContent, ...publicContent } = content;\n return textToolResult(`Downloaded ${publicContent.title} as ${publicContent.fileName}.`, {\n ...publicContent,\n downloadId: persisted.id,\n });\n },\n );\n\n server.registerTool(\n 'prepare_dharma_share_text',\n {\n title: 'Prepare dharma sharing text',\n description:\n 'Prepare a text payload for the Fabushi app to confirm and add to global dharma sharing. This returns a clientAction; it does not start sending.',\n inputSchema: {\n title: z.string().min(1).max(120),\n text: z.string().min(1).max(20000),\n },\n outputSchema: {\n clientAction: z.object({\n type: z.literal('prepare_dharma_share_text'),\n title: z.string(),\n text: z.string(),\n }),\n },\n annotations: {\n readOnlyHint: false,\n destructiveHint: false,\n openWorldHint: false,\n },\n },\n async ({ title, text }) =>\n textToolResult(`Prepared \"${title}\" for user confirmation in the Fabushi app.`, {\n clientAction: {\n type: 'prepare_dharma_share_text',\n title: safeUserText(title).slice(0, 120),\n text: String(text || '').trim().slice(0, 20000),\n },\n }),\n );\n\n server.registerTool(\n 'prepare_practice_book_item',\n {\n title: 'Prepare practice book item',\n description:\n 'Prepare a scripture, chant, or practice item for the Fabushi app practice book. This returns a clientAction; it does not mutate the app by itself.',\n inputSchema: {\n title: z.string().min(1).max(120),\n sourceName: z.string().max(120).optional(),\n text: z.string().min(1).max(20000),\n },\n outputSchema: {\n clientAction: z.object({\n type: z.literal('prepare_practice_book_item'),\n title: z.string(),\n sourceName: z.string(),\n text: z.string(),\n }),\n },\n annotations: {\n readOnlyHint: false,\n destructiveHint: false,\n openWorldHint: false,\n },\n },\n async ({ title, sourceName, text }) =>\n textToolResult(`Prepared \"${title}\" for the Fabushi practice book confirmation flow.`, {\n clientAction: {\n type: 'prepare_practice_book_item',\n title: safeUserText(title).slice(0, 120),\n sourceName: safeUserText(sourceName || '大乘 AI').slice(0, 120),\n text: String(text || '').trim().slice(0, 20000),\n },\n }),\n );\n\n return server;\n}\n\nasync function handleMcpRequest(req, res) {\n const server = createLibreChatMcpServer();\n const transport = new StreamableHTTPServerTransport({\n sessionIdGenerator: undefined,\n enableJsonResponse: true,\n });\n\n res.on('close', () => {\n transport.close();\n server.close();\n });\n\n await server.connect(transport);\n await transport.handleRequest(req, res, req.body);\n}\n\napp.all('/mcp', async (req, res) => {\n try {\n await handleMcpRequest(req, res);\n } catch (error) {\n logger.error({ error: error.stack || String(error) }, 'MCP request failed');\n if (!res.headersSent) {\n res.status(500).json({\n error: 'MCP request failed',\n message: publicAiErrorMessage(error),\n });\n }\n }\n});\n\napp.post(\n '/api/resources/download',\n asyncHandler(async (req, res) => {\n const user = await resolveUser(req, req.body || {});\n const cbeta = parseCbetaResource(req.body || {});\n const internetArchive = parseInternetArchiveResource(req.body || {});\n const content = cbeta\n ? await downloadCbetaResource({ ...req.body, ...cbeta })\n : internetArchive\n ? await downloadInternetArchiveAudio({ ...req.body, ...internetArchive })\n : await downloadWebResource(req.body || {});\n const persisted = persistDownloadedResource(user, content);\n const { binaryContent: _binaryContent, ...publicContent } = content;\n jsonResponse(res, 200, {\n success: true,\n downloadId: persisted.id,\n ...publicContent,\n });\n }),\n);\n\napp.post(\n '/api/codex/resource-task',\n asyncHandler(async (req, res) => {\n if (process.env.ENABLE_CODEX_SDK !== 'true') {\n return jsonResponse(res, 403, {\n success: false,\n message: 'Codex SDK task runner is disabled. Set ENABLE_CODEX_SDK=true on the server to enable it.',\n });\n }\n const prompt = safeUserText(req.body?.prompt);\n if (!prompt) {\n return jsonResponse(res, 400, { success: false, message: 'prompt is required' });\n }\n const { Codex } = await import('@openai/codex-sdk');\n const codexRuntime = createCodexDeepSeekRuntime();\n const codex = new Codex(codexRuntime.enabled ? codexRuntime.options : {});\n const thread = codex.startThread(\n codexRuntime.enabled\n ? codexRuntime.threadOptions\n : { skipGitRepoCheck: true, networkAccessEnabled: true },\n );\n const result = await thread.run(prompt);\n jsonResponse(res, 200, {\n success: true,\n threadId: thread.id,\n provider: codexRuntime.provider,\n model: deepseekModel,\n result,\n });\n }),\n);\n\nregisterPlatformApi({ app, db, resolveUser, asyncHandler });\n\napp.use((error, _req, res, _next) => {\n const status = Number(error.statusCode || error.status || 500);\n const safeStatus = status >= 400 && status < 600 ? status : 500;\n logger.error({ error: error.stack || String(error) }, 'request failed');\n jsonResponse(res, safeStatus, {\n success: false,\n message: error.message || 'Internal Server Error',\n details: error.details,\n });\n});\n\nconst port = Number(process.env.PORT || 8788);\napp.listen(port, '0.0.0.0', () => {\n logger.info(\n {\n port,\n dbPath,\n dataDir,\n deepseekModel,\n cbetaApiRoot,\n },\n 'Dacheng AI backend started',\n );\n});\n"}, "10d6e599cf5d59afe6c4783e2072b0443e58da89": {"t": 1785757968.0142395, "n": 145741, "h": "ea59a5e7c1081efe", "c": "import crypto from 'node:crypto';\nimport fs from 'node:fs';\nimport path from 'node:path';\nimport process from 'node:process';\nimport { fileURLToPath } from 'node:url';\n\nimport { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';\nimport { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';\nimport Database from 'better-sqlite3';\nimport cors from 'cors';\nimport dotenv from 'dotenv';\nimport express from 'express';\nimport rateLimit from 'express-rate-limit';\nimport helmet from 'helmet';\nimport pino from 'pino';\nimport { z } from 'zod';\n\nimport { registerPlatformApi } from './platform_api.js';\n\ndotenv.config();\n\nconst __dirname = path.dirname(fileURLToPath(import.meta.url));\nconst rootDir = path.resolve(__dirname, '..');\nconst dataDir = process.env.DATA_DIR || path.join(rootDir, 'data');\nconst resourcesDir = path.join(dataDir, 'resources');\nconst miniAppsDir = path.join(dataDir, 'miniapps');\nconst miniAppsStorePath = path.join(miniAppsDir, 'sandbox-miniapps.json');\nconst openClawRuntimeUpdatesDir =\n process.env.OPENCLAW_RUNTIME_UPDATES_DIR || path.join(dataDir, 'openclaw-runtime');\nconst openClawRuntimeEnableMacosUpdates =\n process.env.OPENCLAW_RUNTIME_ENABLE_MACOS_UPDATES === 'true';\nconst codexHomeDir = process.env.CODEX_HOME || path.join(dataDir, 'codex-home');\nconst codexTempDir = process.env.CODEX_TMPDIR || path.join(dataDir, 'codex-tmp');\nconst codexRuntimeDir = process.env.XDG_RUNTIME_DIR || path.join(dataDir, 'codex-runtime');\nconst dbPath = process.env.SQLITE_PATH || path.join(dataDir, 'dacheng-ai.sqlite');\n\nfs.mkdirSync(resourcesDir, { recursive: true });\nfs.mkdirSync(miniAppsDir, { recursive: true });\nfs.mkdirSync(openClawRuntimeUpdatesDir, { recursive: true });\nfs.mkdirSync(codexHomeDir, { recursive: true });\nfs.mkdirSync(codexTempDir, { recursive: true });\nfs.mkdirSync(codexRuntimeDir, { recursive: true });\n\nconst logger = pino({\n level: process.env.LOG_LEVEL || 'info',\n});\n\nconst app = express();\napp.disable('x-powered-by');\napp.set('trust proxy', Number(process.env.TRUST_PROXY_HOPS || 1));\napp.use(helmet());\nconst corsOrigin =\n !process.env.CORS_ORIGIN || process.env.CORS_ORIGIN.trim() === '*'\n ? true\n : process.env.CORS_ORIGIN.split(',').map((item) => item.trim()).filter(Boolean);\napp.use(cors({ origin: corsOrigin }));\napp.use(express.json({ limit: '1mb' }));\napp.use(\n rateLimit({\n windowMs: 60_000,\n limit: Number(process.env.RATE_LIMIT_PER_MINUTE || 90),\n standardHeaders: true,\n legacyHeaders: false,\n }),\n);\n\nconst db = new Database(dbPath);\ndb.pragma('journal_mode = WAL');\ndb.pragma('foreign_keys = ON');\n\ndb.exec(`\n CREATE TABLE IF NOT EXISTS conversations (\n id TEXT PRIMARY KEY,\n user_id TEXT NOT NULL,\n title TEXT NOT NULL,\n provider TEXT NOT NULL,\n model TEXT NOT NULL,\n created_at TEXT NOT NULL,\n updated_at TEXT NOT NULL\n );\n\n CREATE INDEX IF NOT EXISTS idx_conversations_user_updated\n ON conversations (user_id, updated_at DESC);\n\n CREATE TABLE IF NOT EXISTS messages (\n id TEXT PRIMARY KEY,\n conversation_id TEXT NOT NULL,\n role TEXT NOT NULL,\n content TEXT NOT NULL,\n prompt_tokens INTEGER NOT NULL DEFAULT 0,\n completion_tokens INTEGER NOT NULL DEFAULT 0,\n total_tokens INTEGER NOT NULL DEFAULT 0,\n created_at TEXT NOT NULL,\n FOREIGN KEY (conversation_id) REFERENCES conversations(id) ON DELETE CASCADE\n );\n\n CREATE INDEX IF NOT EXISTS idx_messages_conversation_created\n ON messages (conversation_id, created_at ASC);\n\n CREATE TABLE IF NOT EXISTS usage_months (\n user_id TEXT NOT NULL,\n month TEXT NOT NULL,\n total_tokens INTEGER NOT NULL DEFAULT 0,\n updated_at TEXT NOT NULL,\n PRIMARY KEY (user_id, month)\n );\n\n CREATE TABLE IF NOT EXISTS resource_downloads (\n id TEXT PRIMARY KEY,\n user_id TEXT NOT NULL,\n title TEXT NOT NULL,\n source_name TEXT NOT NULL,\n source_url TEXT NOT NULL,\n file_name TEXT NOT NULL,\n file_path TEXT NOT NULL,\n created_at TEXT NOT NULL\n );\n\n CREATE TABLE IF NOT EXISTS agent_runs (\n id TEXT PRIMARY KEY,\n user_id TEXT NOT NULL,\n conversation_id TEXT NOT NULL,\n message_id TEXT NOT NULL,\n openclaw_run_id TEXT,\n openclaw_session_key TEXT NOT NULL,\n status TEXT NOT NULL,\n mode TEXT NOT NULL,\n provider TEXT NOT NULL,\n model TEXT,\n input_tokens INTEGER NOT NULL DEFAULT 0,\n output_tokens INTEGER NOT NULL DEFAULT 0,\n tool_call_count INTEGER NOT NULL DEFAULT 0,\n cost_microusd INTEGER NOT NULL DEFAULT 0,\n started_at TEXT NOT NULL,\n completed_at TEXT,\n failed_at TEXT,\n error_code TEXT,\n error_message TEXT\n );\n\n CREATE INDEX IF NOT EXISTS idx_agent_runs_user_started\n ON agent_runs (user_id, started_at DESC);\n\n CREATE INDEX IF NOT EXISTS idx_agent_runs_status_started\n ON agent_runs (status, started_at DESC);\n\n CREATE TABLE IF NOT EXISTS agent_message_feedback (\n id TEXT PRIMARY KEY,\n message_id TEXT NOT NULL,\n user_id TEXT NOT NULL,\n rating TEXT NOT NULL,\n reason TEXT,\n comment TEXT,\n created_at TEXT NOT NULL\n );\n`);\n\nconst statements = {\n insertConversation: db.prepare(`\n INSERT INTO conversations (id, user_id, title, provider, model, created_at, updated_at)\n VALUES (@id, @userId, @title, @provider, @model, @createdAt, @updatedAt)\n `),\n updateConversation: db.prepare(`\n UPDATE conversations\n SET title = @title, updated_at = @updatedAt\n WHERE id = @id AND user_id = @userId\n `),\n getConversation: db.prepare(`\n SELECT * FROM conversations WHERE id = ? AND user_id = ? LIMIT 1\n `),\n listConversations: db.prepare(`\n SELECT id, title, updated_at AS updatedAt\n FROM conversations\n WHERE user_id = ?\n ORDER BY updated_at DESC\n LIMIT 30\n `),\n insertMessage: db.prepare(`\n INSERT INTO messages (\n id, conversation_id, role, content,\n prompt_tokens, completion_tokens, total_tokens, created_at\n )\n VALUES (\n @id, @conversationId, @role, @content,\n @promptTokens, @completionTokens, @totalTokens, @createdAt\n )\n `),\n listMessages: db.prepare(`\n SELECT role, content, prompt_tokens AS promptTokens,\n completion_tokens AS completionTokens, total_tokens AS totalTokens,\n created_at AS createdAt\n FROM messages\n WHERE conversation_id = ?\n ORDER BY created_at ASC\n `),\n getUsage: db.prepare(`\n SELECT total_tokens AS totalTokens FROM usage_months WHERE user_id = ? AND month = ?\n `),\n upsertUsage: db.prepare(`\n INSERT INTO usage_months (user_id, month, total_tokens, updated_at)\n VALUES (@userId, @month, @totalTokens, @updatedAt)\n ON CONFLICT(user_id, month)\n DO UPDATE SET total_tokens = usage_months.total_tokens + excluded.total_tokens,\n updated_at = excluded.updated_at\n `),\n insertResourceDownload: db.prepare(`\n INSERT INTO resource_downloads (\n id, user_id, title, source_name, source_url, file_name, file_path, created_at\n )\n VALUES (\n @id, @userId, @title, @sourceName, @sourceUrl, @fileName, @filePath, @createdAt\n )\n `),\n insertAgentRun: db.prepare(`\n INSERT INTO agent_runs (\n id, user_id, conversation_id, message_id, openclaw_run_id, openclaw_session_key,\n status, mode, provider, model, started_at\n )\n VALUES (\n @id, @userId, @conversationId, @messageId, @openClawRunId, @openClawSessionKey,\n @status, @mode, @provider, @model, @startedAt\n )\n `),\n getAgentRun: db.prepare(`\n SELECT * FROM agent_runs WHERE id = ? AND user_id = ? LIMIT 1\n `),\n updateAgentRunStatus: db.prepare(`\n UPDATE agent_runs\n SET status = @status,\n openclaw_run_id = COALESCE(@openClawRunId, openclaw_run_id),\n input_tokens = COALESCE(@inputTokens, input_tokens),\n output_tokens = COALESCE(@outputTokens, output_tokens),\n tool_call_count = COALESCE(@toolCallCount, tool_call_count),\n completed_at = @completedAt,\n failed_at = @failedAt,\n error_code = @errorCode,\n error_message = @errorMessage\n WHERE id = @id AND user_id = @userId\n `),\n insertAgentFeedback: db.prepare(`\n INSERT INTO agent_message_feedback (id, message_id, user_id, rating, reason, comment, created_at)\n VALUES (@id, @messageId, @userId, @rating, @reason, @comment, @createdAt)\n `),\n};\n\nconst deepseekApiKey = process.env.DEEPSEEK_API_KEY || '';\nconst deepseekBaseUrl = (process.env.DEEPSEEK_BASE_URL || 'https://api.deepseek.com').replace(/\\/+$/, '');\nconst deepseekModel = process.env.DEEPSEEK_MODEL || 'deepseek-chat';\nconst deepseekAvailableModels = Array.from(\n new Set(\n (process.env.DEEPSEEK_MODELS || `${deepseekModel},deepseek-chat,deepseek-reasoner`)\n .split(',')\n .map((item) => normalizeDeepSeekModelName(item))\n .filter(Boolean),\n ),\n);\nconst cbetaApiRoot = (process.env.CBETA_API_ROOT || 'https://api.ombhrum.com/api/cbeta').replace(/\\/+$/, '');\nconst fabushiApiBaseUrl = (process.env.FABUSHI_API_BASE_URL || 'https://api.ombhrum.com').replace(/\\/+$/, '');\nconst memberMonthlyLimit = Number(process.env.MEMBER_MONTHLY_TOKEN_LIMIT || 1_000_000);\nconst freeMonthlyLimit = Number(process.env.FREE_MONTHLY_TOKEN_LIMIT || 1_000);\nconst requireMemberForAi = process.env.REQUIRE_MEMBER_FOR_AI === 'true';\nconst deepseekMaxCompletionTokens = Math.max(1, Number(process.env.DEEPSEEK_MAX_COMPLETION_TOKENS || 1400));\nconst maxResourceTextChars = Number(process.env.MAX_RESOURCE_TEXT_CHARS || 80_000);\nconst openClawRuntimeManifestPath =\n process.env.OPENCLAW_RUNTIME_MANIFEST_PATH || path.join(openClawRuntimeUpdatesDir, 'manifest.json');\nconst openClawRuntimePublicBaseUrl = (process.env.OPENCLAW_RUNTIME_PUBLIC_BASE_URL || '').replace(/\\/+$/, '');\nconst enableLibreChatAgentChat = process.env.ENABLE_LIBRECHAT_AGENT_CHAT === 'true';\nconst libreChatAgentsBaseUrl = (\n process.env.LIBRECHAT_AGENTS_BASE_URL || 'http://127.0.0.1:3080/api/agents/v1'\n).replace(/\\/+$/, '');\nconst libreChatAgentApiKey = process.env.LIBRECHAT_AGENT_API_KEY || '';\nconst libreChatAgentId = process.env.LIBRECHAT_AGENT_ID || '';\nconst enableCodexSdkChat = process.env.ENABLE_CODEX_SDK_CHAT === 'true';\nconst enableOpenClawAgentChat = process.env.ENABLE_OPENCLAW_AGENT_CHAT === 'true';\nconst openClawGatewayUrl = (process.env.OPENCLAW_GATEWAY_URL || 'http://127.0.0.1:18789').replace(/\\/+$/, '');\nconst openClawGatewayToken = process.env.OPENCLAW_GATEWAY_TOKEN || '';\nconst openClawAgentId = process.env.OPENCLAW_AGENT_ID || 'fabushi-public-agent';\nconst openClawRunsEndpoint = process.env.OPENCLAW_RUNS_ENDPOINT || '';\nconst codexDeepSeekProviderId = 'deepseek-chat-completions';\nconst codexResponsesBaseUrl = (\n process.env.CODEX_DEEPSEEK_RESPONSES_BASE_URL ||\n `http://127.0.0.1:${process.env.PORT || 8788}/codex-deepseek/v1`\n).replace(/\\/+$/, '');\n\nconst systemPrompt = [\n '你是“大乘”App 的 AI 助手。',\n '你的核心任务是帮助用户查找、整理、理解并全球法布施合法可分享的佛法资源。',\n '回答要庄重、简洁、可执行;涉及经典、仪轨或资源时,提醒用户尊重版权、来源和当地法规。',\n '如果用户想找资源、下载经文、寻找音频或准备可分享资料,你必须先在后端自动执行搜索、验证和下载步骤;不要要求用户去前端 + 菜单手动查找。',\n '你可以把执行进度总结给用户,但不要输出隐藏推理原文。',\n].join('\\n');\n\nfunction nowIso() {\n return new Date().toISOString();\n}\n\nfunction monthKey(date = new Date()) {\n return `${date.getUTCFullYear()}-${String(date.getUTCMonth() + 1).padStart(2, '0')}`;\n}\n\nfunction jsonResponse(res, status, payload) {\n res.status(status).json(payload);\n}\n\nasync function readJsonFile(filePath) {\n const raw = await fs.promises.readFile(filePath, 'utf8');\n return JSON.parse(raw);\n}\n\nfunction requestOrigin(req) {\n if (openClawRuntimePublicBaseUrl) return openClawRuntimePublicBaseUrl;\n const proto = req.get('X-Forwarded-Proto') || req.protocol || 'https';\n const host = req.get('X-Forwarded-Host') || req.get('Host');\n return host ? `${proto}://${host}` : '';\n}\n\nfunction absolutizeOpenClawRuntimeUrl(req, value) {\n const text = readText(value);\n if (!text) return '';\n if (/^https?:\\/\\//i.test(text)) return text;\n const normalized = text.startsWith('/')\n ? text\n : `/api/openclaw/runtime/files/${text.replace(/^\\/+/, '')}`;\n const origin = requestOrigin(req);\n return origin ? new URL(normalized, `${origin}/`).toString() : normalized;\n}\n\nfunction textToolResult(text, structuredContent = {}) {\n return {\n structuredContent,\n content: [{ type: 'text', text }],\n };\n}\n\nfunction sseHeaders(res) {\n res.status(200);\n res.setHeader('Content-Type', 'text/event-stream; charset=utf-8');\n res.setHeader('Cache-Control', 'no-cache, no-transform');\n res.setHeader('Connection', 'keep-alive');\n res.setHeader('X-Accel-Buffering', 'no');\n if (typeof res.flushHeaders === 'function') res.flushHeaders();\n}\n\nfunction writeSse(res, event, payload) {\n res.write(`event: ${event}\\n`);\n res.write(`data: ${JSON.stringify(payload)}\\n\\n`);\n}\n\nfunction asyncHandler(fn) {\n return (req, res, next) => {\n Promise.resolve(fn(req, res, next)).catch(next);\n };\n}\n\nfunction bearerToken(req) {\n const delegated = readText(req.get('x-dacheng-auth-token'));\n if (\n delegated &&\n delegated !== 'DACHENG_AUTH_TOKEN' &&\n !delegated.startsWith('secretref-env:') &&\n delegated !== 'dacheng-openclaw-proxy'\n ) {\n return delegated;\n }\n const header = req.get('Authorization') || '';\n if (!header.startsWith('Bearer ')) return '';\n const token = header.slice('Bearer '.length).trim();\n return token === 'dacheng-openclaw-proxy' ? '' : token;\n}\n\nfunction sha256(value) {\n return crypto.createHash('sha256').update(value).digest('hex');\n}\n\nfunction safeUserText(value, fallback = '') {\n if (typeof value !== 'string') return fallback;\n return value.replace(/\\u0000/g, '').trim();\n}\n\nfunction estimateTokens(text) {\n const normalized = String(text || '').trim();\n if (!normalized) return 0;\n return Math.max(1, Math.ceil(normalized.length / 3));\n}\n\nfunction readText(value) {\n if (value === undefined || value === null) return '';\n return String(value).replace(/\\u0000/g, '').trim();\n}\n\nconst miniAppHostApiVersion = '1.5';\nconst miniAppHighRiskPermissions = new Set([\n 'auth.token',\n 'payments.alipay',\n 'payments.fudeGold',\n 'desktop.control',\n 'network.udp',\n 'network.interfaces',\n 'system.keepAwake',\n 'hotspot.settings',\n 'local.loopback',\n 'fs.readWrite',\n 'shell.execute',\n 'browser.external',\n 'files.write',\n 'projects.write',\n 'openclaw.restart',\n]);\n\nfunction officialMiniAppRegistry(req) {\n const webBase = process.env.MINIAPP_WEB_BASE_URL || 'https://fabushi.ombhrum.com';\n const official = [\n {\n botId: 'bot.global-dharma',\n miniAppId: 'official.global-dharma',\n title: '全球法布施',\n subtitle: '地区、循环、场能都在对话框上方设置',\n initials: '法',\n icon: 'public',\n avatarColor: '#4CAF7A',\n kind: 'global_dharma',\n entryUrl: process.env.GLOBAL_DHARMA_MINIAPP_URL,\n greeting: '把要分享的文字、链接或素材发给我,我会按上方选择的地区与模式启动全球法布施。',\n inputHint: '输入要法布施的文字/链接,或点 + 添加素材',\n permissions: [\n 'app.context',\n 'bot.chat',\n 'auth.session',\n 'wallet.balance',\n 'payments.entitlement',\n 'payments.fudeGold',\n 'payments.alipay',\n 'files.pick',\n 'network.udp',\n 'network.interfaces',\n 'system.keepAwake',\n 'hotspot.settings',\n 'openclaw.status',\n 'openclaw.chat',\n 'desktop.control',\n ],\n },\n {\n botId: 'bot.flashcards',\n miniAppId: 'official.flashcards',\n title: '背诵闪卡制作',\n subtitle: '随机挖空 / AI 制卡从顶部模式按钮选择',\n initials: '卡',\n icon: 'flashcards',\n avatarColor: '#7E57C2',\n kind: 'flashcards',\n entryUrl: process.env.FLASHCARDS_MINIAPP_URL,\n greeting: '粘贴经文、文章正文或链接即可制作背诵闪卡。制卡模式请在上方按钮切换。',\n inputHint: '粘贴链接或正文,发送后制作闪卡',\n permissions: ['app.context', 'bot.chat', 'flashcards.create', 'openclaw.status', 'openclaw.chat'],\n },\n {\n botId: 'bot.platform-publish',\n miniAppId: 'official.platform-publish',\n title: '法布施到平台',\n subtitle: '选择平台后生成发布草稿并打开入口',\n initials: '发',\n icon: 'publish',\n avatarColor: '#FF9F43',\n kind: 'platform_publish',\n entryUrl: process.env.PLATFORM_PUBLISH_MINIAPP_URL,\n greeting: '把要发布的正文或链接发给我,上方选择平台后,我会生成发布草稿并打开对应平台入口。',\n inputHint: '输入要发布到平台的正文/链接',\n permissions: [\n 'app.context',\n 'bot.chat',\n 'platform.publish',\n 'files.pick',\n 'fs.readWrite',\n 'shell.execute',\n 'browser.external',\n ],\n },\n {\n botId: 'bot.father',\n miniAppId: 'official.bot-father',\n title: '机器人之父',\n subtitle: '用对话生成个人沙箱小程序',\n initials: '父',\n icon: 'bot_father',\n avatarColor: '#3D8BFF',\n kind: 'bot_father',\n entryUrl: process.env.BOT_FATHER_MINIAPP_URL,\n greeting: '告诉我你想要的小程序,我会生成 manifest、界面和权限声明,并放进你的个人沙箱。',\n inputHint: '描述你想创建的小程序',\n permissions: ['app.context', 'bot.chat', 'miniapps.dev'],\n },\n ];\n return {\n bots: official.map((bot, index) => ({\n ...bot,\n destinationIndex: index,\n source: 'official',\n })),\n miniApps: official.map((bot) => ({\n miniAppId: bot.miniAppId,\n botId: bot.botId,\n title: bot.title,\n subtitle: bot.subtitle,\n entryUrl:\n bot.entryUrl ||\n `${webBase.replace(/\\/+$/, '')}/miniapps/${encodeURIComponent(bot.miniAppId)}`,\n version: '1.5.0',\n permissions: bot.permissions,\n surfaces: ['homePinned', 'chatPanel'],\n theme: 'telegramDark',\n signature: 'official',\n reviewStatus: 'trusted',\n source: 'official',\n })),\n };\n}\n\nasync function readMiniAppStore() {\n try {\n const raw = await fs.promises.readFile(miniAppsStorePath, 'utf8');\n const decoded = JSON.parse(raw);\n return {\n apps: Array.isArray(decoded.apps) ? decoded.apps : [],\n };\n } catch (error) {\n if (error?.code === 'ENOENT') return { apps: [] };\n throw error;\n }\n}\n\nasync function writeMiniAppStore(store) {\n await fs.promises.mkdir(miniAppsDir, { recursive: true });\n await fs.promises.writeFile(\n miniAppsStorePath,\n JSON.stringify({ apps: store.apps || [] }, null, 2),\n );\n}\n\nfunction miniAppEntryUrl(req, miniAppId) {\n const origin = requestOrigin(req) || '';\n const pathName = `/api/miniapps/dev/${encodeURIComponent(miniAppId)}/index.html`;\n return origin ? new URL(pathName, `${origin}/`).toString() : pathName;\n}\n\nfunction sanitizeMiniAppPermissions(value, { allowHighRisk = false } = {}) {\n const raw = Array.isArray(value) ? value : [];\n const normalized = raw\n .map((item) => readText(item).toLowerCase())\n .filter((item) => /^[a-z][a-z0-9_.:-]{0,64}$/.test(item));\n const deduped = [...new Set(['app.context', 'bot.chat', ...normalized])];\n return deduped.filter((permission) => allowHighRisk || !miniAppHighRiskPermissions.has(permission));\n}\n\nfunction miniAppScan(permissions, sourceHtml = '') {\n const highRisk = permissions.filter((permission) => miniAppHighRiskPermissions.has(permission));\n const blockedPatterns = [\n /authorization\\s*:\\s*['\"`]bearer/i,\n /desktop_control_bridge_token/i,\n /openclaw_gateway_token/i,\n /eval\\s*\\(/i,\n ];\n const blocked = blockedPatterns\n .filter((pattern) => pattern.test(sourceHtml))\n .map((pattern) => pattern.toString());\n return {\n passed: blocked.length === 0,\n highRiskPermissions: highRisk,\n blockedPatterns: blocked,\n };\n}\n\nfunction inferMiniAppPermissionsFromText(...values) {\n const text = values.map((value) => readText(value)).join('\\n').toLowerCase();\n const rules = [\n ['auth.session', [/auth\\.(getsession|requirelogin)/, /登录|用户信息|宿主账号/]],\n ['auth.token', [/auth\\.getaccesstoken/, /access token|访问 token/]],\n ['payments.alipay', [/payments\\.alipay/, /支付宝|支付|购买|订单/]],\n ['payments.entitlement', [/payments\\.checkentitlement|权益|解锁状态|购买状态/]],\n ['payments.fudeGold', [/payments\\.requestpayment|福德金|代币|钱包|余额/]],\n ['wallet.balance', [/wallet\\.getbalance|福德金|代币|钱包|余额/]],\n ['network.udp', [/network\\.udp|udp|datagram/, /全球法布施|法布施发送|数据报/]],\n ['network.interfaces', [/network\\.interfaces|网卡|网络接口|广播地址/]],\n ['system.keepAwake', [/system\\.keepawake|保持唤醒|后台任务|keep awake/]],\n ['hotspot.settings', [/hotspot\\.opensettings|热点|本地场能/]],\n ['local.loopback', [/localloopback\\./, /localhost|127\\.0\\.0\\.1|本地回环|本地转经轮/]],\n ['flashcards.create', [/flashcards\\./, /闪卡|背诵卡|挖空/]],\n ['platform.publish', [/platformpublish\\./, /发布草稿|发布到平台|公众号|小红书|知乎/]],\n ['files.pick', [/files\\.pick/, /选择文件|上传文件|本地素材/]],\n ['fs.readWrite', [/fs\\.(writefile|readfile)/, /本地文件|写文件|读文件|文件处理/]],\n ['shell.execute', [/shell\\.execute/, /终端|命令行|shell|cli|playwright/]],\n ['browser.external', [/browser\\.open/, /浏览器|打开网页|打开平台/]],\n ['desktop.control', [/desktopcontrol\\./, /桌面控制|点击|键盘|鼠标/]],\n ['openclaw.chat', [/openclaw\\.chat/, /openclaw/]],\n ];\n return rules\n .filter(([, patterns]) => patterns.some((pattern) => pattern.test(text)))\n .map(([permission]) => permission);\n}\n\nfunction miniAppRecordToBot(record) {\n return {\n botId: record.botId,\n miniAppId: record.miniAppId,\n title: record.title,\n subtitle: record.subtitle,\n initials: record.initials || '小',\n icon: record.icon || 'code',\n avatarColor: record.avatarColor || '#3D8BFF',\n kind: 'third_party',\n greeting: record.greeting || '你好,我是这个小程序的机器人。',\n inputHint: record.inputHint || '输入消息,或打开小程序面板',\n permissions: record.permissions || ['app.context', 'bot.chat'],\n source: record.source || 'sandbox',\n };\n}\n\nfunction miniAppRecordToManifest(req, record) {\n return {\n miniAppId: record.miniAppId,\n botId: record.botId,\n title: record.title,\n subtitle: record.subtitle,\n entryUrl: miniAppEntryUrl(req, record.miniAppId),\n version: record.version || '0.0.1',\n permissions: record.permissions || ['app.context', 'bot.chat'],\n surfaces: record.surfaces || ['chatPanel'],\n theme: record.theme || 'telegramDark',\n signature: record.signature || `sandbox:${record.ownerId}`,\n reviewStatus: record.reviewStatus || 'sandbox',\n source: record.source || 'sandbox',\n };\n}\n\nfunction safeMiniAppId(value) {\n return readText(value).replace(/[^a-zA-Z0-9_.-]/g, '-').slice(0, 80);\n}\n\nfunction titleFromPromptForMiniApp(prompt) {\n const text = readText(prompt).replace(/\\s+/g, ' ');\n if (!text) return '个人小程序';\n return text.length <= 18 ? text : text.slice(0, 18);\n}\n\nfunction escapeHtml(value) {\n return String(value || '')\n .replace(/&/g, '&')\n .replace(/</g, '<')\n .replace(/>/g, '>')\n .replace(/\"/g, '"')\n .replace(/'/g, ''');\n}\n\nfunction generatedMiniAppHtml({ title, prompt }) {\n const safeTitle = escapeHtml(title);\n const safePrompt = escapeHtml(prompt);\n const promptJson = JSON.stringify(readText(prompt));\n return `<!doctype html>\n<html lang=\"zh-Hans\">\n<head>\n <meta charset=\"utf-8\" />\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n <title>${safeTitle}</title>\n <style>\n body { margin:0; font-family:-apple-system,BlinkMacSystemFont,\"Segoe UI\",sans-serif; background:#0f1722; color:#fff; }\n main { padding:20px; display:grid; gap:14px; }\n .card { border:1px solid #263445; background:#17212b; border-radius:16px; padding:16px; }\n button { border:0; border-radius:999px; padding:10px 14px; background:#3390ec; color:white; font-weight:800; }\n pre { white-space:pre-wrap; color:#9ec7ff; }\n </style>\n</head>\n<body>\n <main>\n <section class=\"card\">\n <h1>${safeTitle}</h1>\n <p>这是机器人之父根据你的描述生成的个人沙箱小程序。</p>\n <pre>${safePrompt}</pre>\n </section>\n <section class=\"card\">\n <button id=\"context\">读取宿主上下文</button>\n <button id=\"spec\">读取宿主能力</button>\n <button id=\"chat\">让机器人解释这个小程序</button>\n <pre id=\"output\">等待操作...</pre>\n </section>\n </main>\n <script>\n const out = document.getElementById('output');\n async function invoke(method, params) {\n if (!window.FabushiMiniApp) throw new Error('FabushiMiniApp host SDK not ready');\n const result = await window.FabushiMiniApp.invoke(method, params || {});\n out.textContent = JSON.stringify(result, null, 2);\n }\n document.getElementById('context').onclick = () => invoke('app.getContext');\n document.getElementById('spec').onclick = () => invoke('app.getHostApiSpec');\n const prompt = ${promptJson};\n document.getElementById('chat').onclick = () => invoke('bot.sendMessage', {\n message: '请简要说明这个小程序能做什么:' + prompt\n });\n const seenCommands = new Set();\n function deliverMiniAppCommand(detail) {\n if (!detail || typeof detail !== 'object') return;\n const commandKey = detail.commandId || detail.id || [detail.createdAt, detail.command, detail.rawText || detail.text].filter(Boolean).join(':');\n if (commandKey) {\n if (seenCommands.has(commandKey)) return;\n seenCommands.add(commandKey);\n }\n handleMiniAppCommand(detail);\n }\n async function handleMiniAppCommand(detail) {\n out.textContent = '收到后台命令:' + JSON.stringify(detail, null, 2);\n try {\n await invoke('bot.postMessage', {\n commandId: detail.commandId || detail.id,\n message: '「${safeTitle}」已收到后台命令:' + (detail.args || detail.text || '')\n });\n await invoke('bot.reportCommandResult', {\n commandId: detail.commandId || detail.id,\n status: 'completed',\n message: '「${safeTitle}」后台命令已处理。'\n });\n } catch (error) {\n out.textContent = String(error && error.message || error);\n }\n }\n window.addEventListener('fabushi-miniapp-command', (event) => {\n deliverMiniAppCommand(event.detail || {});\n });\n if (window.__fabushiLastMiniAppCommand) {\n (window.queueMicrotask || ((fn) => setTimeout(fn, 0)))(() => {\n deliverMiniAppCommand(window.__fabushiLastMiniAppCommand);\n });\n }\n </script>\n</body>\n</html>`;\n}\n\nfunction extractMiniAppHtml(value) {\n const text = readText(value);\n if (!text) return '';\n const fenced = text.match(/```(?:html)?\\s*([\\s\\S]*?)```/i);\n const candidate = (fenced ? fenced[1] : text).trim();\n if (!/<html[\\s>]/i.test(candidate) || !/<script[\\s>]/i.test(candidate)) return '';\n return candidate.startsWith('<!doctype') || candidate.startsWith('<html')\n ? candidate\n : `<!doctype html>\\n${candidate}`;\n}\n\nasync function generateBotFatherMiniAppHtml({ title, prompt, user }) {\n const fallback = generatedMiniAppHtml({ title, prompt });\n try {\n const result = await runAgentModel({\n user,\n mode: 'miniapp_generation',\n maxCompletionTokens: deepseekMaxCompletionTokens,\n messages: [\n {\n role: 'system',\n content: [\n '你是 Fabushi App 的小程序生成器。',\n '只输出一个完整静态 HTML 文件,不要 Markdown,不要解释。',\n '小程序只能通过 window.FabushiMiniApp.invoke(method, params) 调用宿主能力。',\n '先调用 app.getHostApiSpec 或 app.getCapabilities 判断宿主版本和已授权权限,再显示对应 UI。',\n '聊天后台调用协议:优先使用 window.FabushiMiniApp.bot.onCommand(\"/start\", handler) 或 bot.onAnyCommand(handler);如果手写监听 window 的 fabushi-miniapp-command 事件,注册后也要读取 window.__fabushiLastMiniAppCommand 并按 commandId 去重补处理。detail.command 默认为 /start,detail.args 是用户文本;处理完成后调用 bot.postMessage 或 bot.reportCommandResult 把进度/结果写回聊天框。',\n '标准能力:auth.getSession/auth.requireLogin 复用宿主登录;payments.requestPayment 使用福德金支付并由宿主登记权益;payments.checkEntitlement 查询商品是否已解锁;payments.alipay.pay/createOrder 仅作为宿主官方现金支付兜底;network.udp/network.interfaces 提供 UDP 和网卡原语;system.keepAwake/hotspot.openSettings 提供系统级辅助;flashcards.createDeck 接入背诵闪卡;platformPublish.createDraft 接入发布草稿;fs.writeFile/fs.readFile、shell.execute、browser.open 用于本地文件处理、终端和浏览器。',\n '高危权限包括 auth.token、payments.alipay、payments.fudeGold、network.udp、network.interfaces、system.keepAwake、hotspot.settings、local.loopback、fs.readWrite、shell.execute、browser.external、desktop.control;只有用户需求明确需要时才使用。',\n '禁止外链脚本、eval、Authorization token、桌面桥 token、OpenClaw token。',\n '不要调用 auth.getAccessToken,除非用户明确要求对接需要 token 的受信服务。',\n '优先实现一个可点击、可立即使用的小面板,包含 app.getContext 或 bot.sendMessage 示例。',\n ].join('\\n'),\n },\n {\n role: 'user',\n content: [\n `小程序标题:${title}`,\n `用户需求:${prompt}`,\n '请生成 HTML/CSS/JS 单文件。',\n ].join('\\n'),\n },\n ],\n });\n const sourceHtml = extractMiniAppHtml(result.message);\n if (!sourceHtml) {\n return {\n sourceHtml: fallback,\n generation: { provider: 'template', fallbackReason: 'ai_output_not_html' },\n };\n }\n return {\n sourceHtml,\n generation: {\n provider: result.provider || 'ai',\n model: result.model || '',\n },\n };\n } catch (error) {\n return {\n sourceHtml: fallback,\n generation: {\n provider: 'template',\n fallbackReason: readText(error?.message || error),\n },\n };\n }\n}\n\nfunction clientContextMessage(client) {\n if (!client || typeof client !== 'object') return '';\n const selectedModel = readText(client.selectedModel || client.model);\n const project = client.project && typeof client.project === 'object' ? client.project : {};\n const projectName = readText(project.name);\n const projectPath = readText(project.path);\n const lines = ['桌面端上下文:'];\n if (selectedModel) lines.push(`用户选择的 DeepSeek 模型:${normalizeDeepSeekModelName(selectedModel)}`);\n if (projectPath) {\n lines.push(`当前项目名称:${projectName || projectPath}`);\n lines.push(`当前项目路径:${projectPath}`);\n lines.push('如果本轮任务涉及读取、修改、运行、分析或整理项目,请优先围绕当前项目路径工作;不要擅自操作其他目录。');\n }\n return lines.length > 1 ? lines.join('\\n') : '';\n}\nfunction unwrapPayload(payload) {\n if (!payload || typeof payload !== 'object') return {};\n return payload.data && typeof payload.data === 'object' ? payload.data : payload;\n}\n\nfunction readFirstText(values) {\n for (const value of values) {\n const text = readText(value);\n if (text) return text;\n }\n return '';\n}\n\nfunction activeFlag(value) {\n if (value === true) return true;\n if (value === 1) return true;\n if (typeof value !== 'string') return false;\n return ['true', '1', 'yes', 'active'].includes(value.trim().toLowerCase());\n}\n\nfunction futureDate(value) {\n const text = readText(value);\n if (!text) return false;\n const timestamp = Date.parse(text);\n return Number.isFinite(timestamp) && timestamp > Date.now();\n}\n\nfunction normalizeMembershipPayload(payload) {\n if (!payload || typeof payload !== 'object') return false;\n const data = unwrapPayload(payload);\n const membership = data.membership && typeof data.membership === 'object' ? data.membership : data;\n const type = readText(membership.type || membership.membershipType || data.membershipType).toLowerCase();\n const status = readText(membership.status || data.status).toLowerCase();\n const expiry = membership.expiresAt ||\n membership.expires_at ||\n membership.membershipExpiry ||\n membership.membership_expires_at ||\n data.membershipExpiry ||\n data.membership_expires_at;\n const hasExplicitActiveFlag =\n membership.isActive !== undefined ||\n membership.active !== undefined ||\n membership.hasPremiumAccess !== undefined ||\n membership.hasPremiumMembership !== undefined ||\n membership.isPremium !== undefined;\n\n if (hasExplicitActiveFlag) {\n return Boolean(\n activeFlag(membership.isActive) ||\n activeFlag(membership.active) ||\n activeFlag(membership.hasPremiumAccess) ||\n activeFlag(membership.hasPremiumMembership) ||\n activeFlag(membership.isPremium),\n );\n }\n\n return Boolean(\n status === 'active' ||\n type === 'paid' ||\n type === 'premium' ||\n type === 'trial' ||\n futureDate(expiry),\n );\n}\n\nfunction extractRemoteAccount(payload) {\n const data = unwrapPayload(payload);\n const nestedUser = data.user && typeof data.user === 'object' ? data.user : {};\n const id = readFirstText([\n data.userId,\n data.user_id,\n data.userNo,\n data.user_no,\n data.id,\n nestedUser.userId,\n nestedUser.user_id,\n nestedUser.userNo,\n nestedUser.user_no,\n nestedUser.id,\n ]);\n const username = readFirstText([data.username, data.name, nestedUser.username, nestedUser.name]);\n const email = readFirstText([data.email, nestedUser.email]);\n const membership = data.membership && typeof data.membership === 'object' ? data.membership : null;\n return {\n authenticated: Boolean(id || username || email || membership || data.success === true),\n id,\n username,\n email,\n isMember: normalizeMembershipPayload(payload),\n membership,\n };\n}\n\nasync function resolveRemoteAccount(token) {\n const fallback = {\n authenticated: false,\n id: '',\n username: '',\n email: '',\n isMember: false,\n membership: null,\n };\n if (!token || !fabushiApiBaseUrl) return fallback;\n\n const endpoints = [\n '/api/stripe/membership-status',\n '/api/alipay/check-membership',\n '/api/auth/user-info',\n ];\n\n for (const endpoint of endpoints) {\n try {\n const response = await fetch(`${fabushiApiBaseUrl}${endpoint}`, {\n headers: {\n Accept: 'application/json',\n Authorization: `Bearer ${token}`,\n },\n signal: AbortSignal.timeout(8_000),\n });\n if (response.status === 401 || response.status === 403) return fallback;\n if (!response.ok) continue;\n const payload = await response.json();\n const account = extractRemoteAccount(payload);\n if (account.authenticated || account.isMember) {\n return {\n ...fallback,\n ...account,\n authenticated: true,\n };\n }\n } catch (error) {\n logger.debug({ endpoint, error: String(error) }, 'membership lookup skipped');\n }\n }\n\n return fallback;\n}\n\nasync function resolveUser(req, body = {}) {\n const token = bearerToken(req);\n const usernameHint = safeUserText(body.username || req.query.username);\n\n const testToken = process.env.TEST_ACCOUNT_TOKEN || 'dacheng-test-token-bypass-limits';\n if (token && token === testToken) {\n return {\n userId: 'user:test_account',\n username: 'TestAccount',\n tokenHash: sha256(token).slice(0, 24),\n isAuthenticated: true,\n isMember: true,\n isTestAccount: true,\n membership: { type: 'lifetime', active: true },\n };\n }\n\n const tokenHash = token ? sha256(token).slice(0, 24) : '';\n const anonymousHash = sha256(`${req.ip}|${req.get('User-Agent') || ''}`).slice(0, 24);\n const remoteAccount = await resolveRemoteAccount(token);\n const remoteIdentity = remoteAccount.id ||\n remoteAccount.username ||\n remoteAccount.email ||\n (remoteAccount.authenticated ? tokenHash : '');\n const userId = remoteAccount.authenticated\n ? `user:${remoteIdentity}`\n : tokenHash\n ? `token:${tokenHash}`\n : `anon:${anonymousHash}`;\n return {\n userId,\n username: remoteAccount.username || usernameHint,\n tokenHash,\n isAuthenticated: remoteAccount.authenticated,\n isMember: remoteAccount.isMember,\n membership: remoteAccount.membership,\n };\n}\n\nfunction usageFor(userId) {\n const month = monthKey();\n const row = statements.getUsage.get(userId, month);\n return {\n month,\n used: Number(row?.totalTokens || 0),\n };\n}\n\nfunction enforceTokenBudget(user, estimatedTokensForRequest) {\n if (user && user.isTestAccount) {\n return { limit: 999999999, usage: { month: monthKey(), used: 0 } };\n }\n if (requireMemberForAi && !user.isMember) {\n const error = new Error('大乘 AI 需要开通会员后使用');\n error.statusCode = 403;\n error.code = 'AI_MEMBERSHIP_REQUIRED';\n error.details = {\n requireMemberForAi,\n monthlyLimit: memberMonthlyLimit,\n remainingTokens: 0,\n };\n throw error;\n }\n\n const limit = user.isMember ? memberMonthlyLimit : freeMonthlyLimit;\n const usage = usageFor(user.userId);\n if (usage.used + estimatedTokensForRequest > limit) {\n const error = new Error('本月 AI token 额度已不足');\n error.statusCode = 429;\n error.code = 'USER_TOKEN_QUOTA_EXCEEDED';\n error.details = {\n monthlyLimit: limit,\n usedTokens: usage.used,\n remainingTokens: Math.max(0, limit - usage.used),\n };\n throw error;\n }\n return { limit, usage };\n}\n\nfunction completionBudgetFor(budget, estimatedPromptTokens) {\n const remainingAfterPrompt = budget.limit - budget.usage.used - estimatedPromptTokens;\n return Math.max(1, Math.min(deepseekMaxCompletionTokens, Math.floor(remainingAfterPrompt)));\n}\n\nfunction recordUsage(userId, totalTokens) {\n if (!totalTokens || totalTokens < 1) return;\n statements.upsertUsage.run({\n userId,\n month: monthKey(),\n totalTokens,\n updatedAt: nowIso(),\n });\n}\n\nfunction titleFromMessage(message) {\n const compact = message.replace(/\\s+/g, ' ').trim();\n if (!compact) return '新对话';\n return compact.length > 22 ? `${compact.slice(0, 22)}...` : compact;\n}\n\nfunction normalizeMessages(rows) {\n return rows.map((row) => ({\n role: row.role,\n content: row.content,\n }));\n}\n\nfunction normalizeMaxCompletionTokens(value) {\n const numeric = Number(value || deepseekMaxCompletionTokens);\n if (!Number.isFinite(numeric) || numeric < 1) return deepseekMaxCompletionTokens;\n return Math.max(1, Math.min(deepseekMaxCompletionTokens, Math.floor(numeric)));\n}\n\nfunction normalizeDeepSeekModelName(value) {\n const raw = readText(value || deepseekModel);\n if (!raw) return deepseekModel;\n const modelId = raw.includes('/') ? raw.split('/').pop() : raw;\n return modelId || deepseekModel;\n}\n\nasync function callDeepSeek(messages, options = {}) {\n if (!deepseekApiKey) {\n const error = new Error('DeepSeek API key is not configured');\n error.statusCode = 503;\n throw error;\n }\n\n const model = normalizeDeepSeekModelName(options.model);\n const response = await fetch(`${deepseekBaseUrl}/chat/completions`, {\n method: 'POST',\n headers: {\n Accept: 'application/json',\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${deepseekApiKey}`,\n },\n body: JSON.stringify({\n model,\n messages,\n temperature: 0.4,\n max_tokens: normalizeMaxCompletionTokens(options.maxCompletionTokens),\n stream: false,\n }),\n signal: AbortSignal.timeout(60_000),\n });\n\n const bodyText = await response.text();\n let payload;\n try {\n payload = bodyText ? JSON.parse(bodyText) : {};\n } catch {\n payload = { error: bodyText };\n }\n\n if (!response.ok) {\n const upstreamMessage = payload?.error?.message || payload?.message || '';\n const friendlyMessage =\n response.status === 402 || /insufficient\\s+balance/i.test(upstreamMessage)\n ? '大乘 AI 服务额度暂不可用,请联系管理员充值 DeepSeek 账户后重试。'\n : upstreamMessage || `DeepSeek request failed: ${response.status}`;\n const error = new Error(friendlyMessage);\n error.statusCode = response.status;\n if (response.status === 402 || /insufficient\\s+balance/i.test(upstreamMessage)) {\n error.code = 'DEEPSEEK_PROVIDER_QUOTA';\n }\n throw error;\n }\n\n const message = payload?.choices?.[0]?.message?.content?.trim();\n if (!message) {\n const error = new Error('DeepSeek returned an empty response');\n error.statusCode = 502;\n throw error;\n }\n\n return {\n message,\n model,\n usage: {\n promptTokens: Number(payload?.usage?.prompt_tokens || 0),\n completionTokens: Number(payload?.usage?.completion_tokens || 0),\n totalTokens: Number(payload?.usage?.total_tokens || 0),\n },\n };\n}\n\nasync function callDeepSeekStream(messages, callbacks = {}) {\n if (!deepseekApiKey) {\n const error = new Error('DeepSeek API key is not configured');\n error.statusCode = 503;\n throw error;\n }\n\n const model = normalizeDeepSeekModelName(callbacks.model);\n const response = await fetch(`${deepseekBaseUrl}/chat/completions`, {\n method: 'POST',\n headers: {\n Accept: 'text/event-stream',\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${deepseekApiKey}`,\n },\n body: JSON.stringify({\n model,\n messages,\n temperature: 0.4,\n max_tokens: normalizeMaxCompletionTokens(callbacks.maxCompletionTokens),\n stream: true,\n stream_options: { include_usage: true },\n }),\n signal: callbacks.signal || AbortSignal.timeout(90_000),\n });\n\n if (!response.ok) {\n const bodyText = await response.text();\n let payload;\n try {\n payload = bodyText ? JSON.parse(bodyText) : {};\n } catch {\n payload = { error: bodyText };\n }\n const upstreamMessage = payload?.error?.message || payload?.message || '';\n const friendlyMessage =\n response.status === 402 || /insufficient\\s+balance/i.test(upstreamMessage)\n ? '大乘 AI 服务额度暂不可用,请联系管理员充值 DeepSeek 账户后重试。'\n : upstreamMessage || `DeepSeek request failed: ${response.status}`;\n const error = new Error(friendlyMessage);\n error.statusCode = response.status;\n if (response.status === 402 || /insufficient\\s+balance/i.test(upstreamMessage)) {\n error.code = 'DEEPSEEK_PROVIDER_QUOTA';\n }\n throw error;\n }\n\n let buffer = '';\n let message = '';\n let usage = {\n promptTokens: 0,\n completionTokens: 0,\n totalTokens: 0,\n };\n const decoder = new TextDecoder();\n\n for await (const chunk of response.body) {\n buffer += decoder.decode(chunk, { stream: true });\n const lines = buffer.split(/\\r?\\n/);\n buffer = lines.pop() || '';\n\n for (const rawLine of lines) {\n const line = rawLine.trim();\n if (!line.startsWith('data:')) continue;\n const data = line.slice('data:'.length).trim();\n if (!data || data === '[DONE]') continue;\n\n let payload;\n try {\n payload = JSON.parse(data);\n } catch {\n continue;\n }\n\n const delta = payload?.choices?.[0]?.delta?.content || '';\n if (delta) {\n message += delta;\n callbacks.onToken?.(delta);\n }\n\n if (payload?.usage) {\n usage = {\n promptTokens: Number(payload.usage.prompt_tokens || usage.promptTokens || 0),\n completionTokens: Number(payload.usage.completion_tokens || usage.completionTokens || 0),\n totalTokens: Number(payload.usage.total_tokens || usage.totalTokens || 0),\n };\n }\n }\n }\n\n message = message.trim();\n if (!message) {\n const error = new Error('DeepSeek returned an empty response');\n error.statusCode = 502;\n throw error;\n }\n\n return { message, model, usage };\n}\n\nfunction createCodexDeepSeekRuntime() {\n if (!enableCodexSdkChat || !deepseekApiKey) {\n return {\n enabled: false,\n provider: 'deepseek-direct',\n reason: enableCodexSdkChat ? 'DeepSeek API key is not configured' : 'ENABLE_CODEX_SDK_CHAT is not true',\n };\n }\n\n return {\n enabled: true,\n provider: 'codex-sdk-deepseek',\n options: {\n apiKey: deepseekApiKey,\n baseUrl: deepseekBaseUrl,\n env: {\n ...process.env,\n DEEPSEEK_API_KEY: deepseekApiKey,\n HOME: codexHomeDir,\n CODEX_HOME: codexHomeDir,\n TMPDIR: codexTempDir,\n XDG_RUNTIME_DIR: codexRuntimeDir,\n XDG_CACHE_HOME: path.join(codexHomeDir, '.cache'),\n XDG_CONFIG_HOME: path.join(codexHomeDir, '.config'),\n XDG_DATA_HOME: path.join(codexHomeDir, '.local', 'share'),\n PATH: process.env.PATH || '/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin',\n },\n config: {\n model_provider: codexDeepSeekProviderId,\n model_providers: {\n [codexDeepSeekProviderId]: {\n name: 'DeepSeek Chat Completions',\n base_url: codexResponsesBaseUrl,\n env_key: 'DEEPSEEK_API_KEY',\n wire_api: 'responses',\n query_params: {},\n },\n },\n },\n },\n threadOptions: {\n model: deepseekModel,\n approvalPolicy: 'never',\n sandboxMode: 'read-only',\n skipGitRepoCheck: true,\n networkAccessEnabled: true,\n },\n };\n}\n\nfunction codexPromptFromMessages(messages) {\n return messages\n .map((message) => {\n const role = String(message.role || 'user').toUpperCase();\n return `${role}:\\n${message.content}`;\n })\n .join('\\n\\n');\n}\n\nfunction responseContentText(content) {\n if (typeof content === 'string') return content;\n if (!Array.isArray(content)) return '';\n return content\n .map((part) => {\n if (typeof part === 'string') return part;\n if (!part || typeof part !== 'object') return '';\n return String(part.text || part.input_text || part.output_text || '');\n })\n .filter(Boolean)\n .join('\\n');\n}\n\nfunction codexResponsesPrompt(body) {\n const input = Array.isArray(body?.input) ? body.input : [];\n const userItems = input.filter((item) => item?.role === 'user');\n const source = userItems[userItems.length - 1] || input[input.length - 1];\n const prompt = responseContentText(source?.content).trim();\n if (prompt) return prompt;\n if (typeof body?.input === 'string') return body.input.trim();\n return '';\n}\n\nfunction writeResponsesEvent(res, event, data) {\n res.write(`event: ${event}\\n`);\n res.write(`data: ${JSON.stringify(data)}\\n\\n`);\n}\n\nfunction responseUsage(usage = {}) {\n return {\n input_tokens: Number(usage.promptTokens || usage.input_tokens || 0),\n cached_input_tokens: 0,\n output_tokens: Number(usage.completionTokens || usage.output_tokens || 0),\n reasoning_output_tokens: 0,\n total_tokens: Number(usage.totalTokens || usage.total_tokens || 0),\n };\n}\n\nfunction createLibreChatAgentRuntime() {\n if (!enableLibreChatAgentChat || !libreChatAgentApiKey || !libreChatAgentId) {\n return {\n enabled: false,\n provider: 'librechat-agent',\n reason: !enableLibreChatAgentChat\n ? 'ENABLE_LIBRECHAT_AGENT_CHAT is not true'\n : !libreChatAgentApiKey\n ? 'LIBRECHAT_AGENT_API_KEY is not configured'\n : 'LIBRECHAT_AGENT_ID is not configured',\n };\n }\n\n return {\n enabled: true,\n provider: 'librechat-agent',\n model: libreChatAgentId,\n chatCompletionsUrl: `${libreChatAgentsBaseUrl}/chat/completions`,\n };\n}\n\nfunction libreChatUsage(usage = {}) {\n return {\n promptTokens: Number(usage.prompt_tokens || usage.input_tokens || usage.promptTokens || 0),\n completionTokens: Number(usage.completion_tokens || usage.output_tokens || usage.completionTokens || 0),\n totalTokens: Number(usage.total_tokens || usage.totalTokens || 0),\n };\n}\n\nasync function callLibreChatAgent(messages) {\n const runtime = createLibreChatAgentRuntime();\n if (!runtime.enabled) {\n const error = new Error(runtime.reason || 'LibreChat Agent chat is not enabled');\n error.statusCode = 503;\n throw error;\n }\n\n const response = await fetch(runtime.chatCompletionsUrl, {\n method: 'POST',\n headers: {\n Accept: 'application/json',\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${libreChatAgentApiKey}`,\n },\n body: JSON.stringify({\n model: runtime.model,\n messages,\n temperature: 0.4,\n stream: false,\n }),\n signal: AbortSignal.timeout(120_000),\n });\n\n const bodyText = await response.text();\n let payload;\n try {\n payload = bodyText ? JSON.parse(bodyText) : {};\n } catch {\n payload = { error: bodyText };\n }\n\n if (!response.ok) {\n const upstreamMessage = payload?.error?.message || payload?.message || bodyText || '';\n const error = new Error(upstreamMessage || `LibreChat Agent request failed: ${response.status}`);\n error.statusCode = response.status;\n throw error;\n }\n\n const message = payload?.choices?.[0]?.message?.content?.trim();\n if (!message) {\n const error = new Error('LibreChat Agent returned an empty response');\n error.statusCode = 502;\n throw error;\n }\n\n return {\n message,\n usage: libreChatUsage(payload?.usage),\n };\n}\n\nasync function callLibreChatAgentStream(messages, callbacks = {}) {\n const runtime = createLibreChatAgentRuntime();\n if (!runtime.enabled) {\n const error = new Error(runtime.reason || 'LibreChat Agent chat is not enabled');\n error.statusCode = 503;\n throw error;\n }\n\n const response = await fetch(runtime.chatCompletionsUrl, {\n method: 'POST',\n headers: {\n Accept: 'text/event-stream',\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${libreChatAgentApiKey}`,\n },\n body: JSON.stringify({\n model: runtime.model,\n messages,\n temperature: 0.4,\n stream: true,\n }),\n signal: callbacks.signal || AbortSignal.timeout(180_000),\n });\n\n if (!response.ok) {\n const bodyText = await response.text();\n let payload;\n try {\n payload = bodyText ? JSON.parse(bodyText) : {};\n } catch {\n payload = { error: bodyText };\n }\n const upstreamMessage = payload?.error?.message || payload?.message || bodyText || '';\n const error = new Error(upstreamMessage || `LibreChat Agent request failed: ${response.status}`);\n error.statusCode = response.status;\n throw error;\n }\n\n let buffer = '';\n let message = '';\n let usage = { promptTokens: 0, completionTokens: 0, totalTokens: 0 };\n const decoder = new TextDecoder();\n\n for await (const chunk of response.body) {\n buffer += decoder.decode(chunk, { stream: true });\n const lines = buffer.split(/\\r?\\n/);\n buffer = lines.pop() || '';\n\n for (const rawLine of lines) {\n const line = rawLine.trim();\n if (!line.startsWith('data:')) continue;\n const data = line.slice('data:'.length).trim();\n if (!data || data === '[DONE]') continue;\n\n let payload;\n try {\n payload = JSON.parse(data);\n } catch {\n continue;\n }\n\n const delta = payload?.choices?.[0]?.delta?.content || '';\n if (delta) {\n message += delta;\n callbacks.onToken?.(delta);\n }\n\n if (payload?.usage) {\n usage = libreChatUsage(payload.usage);\n }\n }\n }\n\n message = message.trim();\n if (!message) {\n const error = new Error('LibreChat Agent returned an empty response');\n error.statusCode = 502;\n throw error;\n }\n\n return { message, usage };\n}\n\nfunction isDeepSeekQuotaError(error) {\n if (error?.code === 'DEEPSEEK_PROVIDER_QUOTA') return true;\n if (error?.code === 'USER_TOKEN_QUOTA_EXCEEDED' || error?.code === 'AI_MEMBERSHIP_REQUIRED') return false;\n const message = String(error?.message || error || '');\n return /insufficient\\s+balance|balance/i.test(message);\n}\n\nfunction publicAiErrorMessage(error) {\n if (isDeepSeekQuotaError(error)) {\n return '大乘 AI 服务额度暂不可用,请联系管理员充值 DeepSeek 账户后重试。';\n }\n return error?.message || 'Internal Server Error';\n}\n\nfunction responsesPayload({\n responseId,\n itemId,\n status,\n model,\n text = '',\n usage = null,\n error = null,\n}) {\n return {\n id: responseId,\n object: 'response',\n status,\n model,\n output:\n status === 'completed'\n ? [\n {\n id: itemId,\n type: 'message',\n role: 'assistant',\n status: 'completed',\n content: [{ type: 'output_text', text }],\n },\n ]\n : [],\n ...(usage ? { usage: responseUsage(usage) } : {}),\n ...(error\n ? {\n error: {\n message: error.message || String(error),\n type: 'server_error',\n },\n }\n : {}),\n };\n}\n\nasync function callCodexSdkDeepSeek(messages, callbacks = {}) {\n const codexRuntime = createCodexDeepSeekRuntime();\n if (!codexRuntime.enabled) {\n const error = new Error(codexRuntime.reason || 'Codex SDK chat is not enabled');\n error.statusCode = 503;\n throw error;\n }\n\n const { Codex } = await import('@openai/codex-sdk');\n const codex = new Codex(codexRuntime.options);\n const thread = codex.startThread(codexRuntime.threadOptions);\n const prompt = codexPromptFromMessages(messages);\n\n if (callbacks.onToken || callbacks.onStep) {\n const { events } = await thread.runStreamed(prompt, { signal: callbacks.signal });\n let finalResponse = '';\n let usage = null;\n\n for await (const event of events) {\n if (event.type === 'item.completed') {\n const item = event.item;\n if (item.type === 'agent_message') {\n finalResponse = item.text || finalResponse;\n callbacks.onToken?.(item.text || '');\n } else if (item.type === 'todo_list') {\n callbacks.onStep?.({\n title: 'Codex SDK 执行计划',\n message: item.items.map((todo) => `${todo.completed ? '✓' : '•'} ${todo.text}`).join('\\n'),\n });\n } else if (item.type === 'command_execution') {\n callbacks.onStep?.({\n title: 'Codex SDK 执行命令',\n message: item.command,\n });\n } else if (item.type === 'mcp_tool_call') {\n callbacks.onStep?.({\n title: 'Codex SDK 调用工具',\n message: `${item.server}.${item.tool}`,\n });\n }\n } else if (event.type === 'turn.completed') {\n usage = event.usage;\n } else if (event.type === 'turn.failed') {\n throw new Error(event.error?.message || 'Codex SDK turn failed');\n } else if (event.type === 'error') {\n throw new Error(event.message || 'Codex SDK stream failed');\n }\n }\n\n if (!finalResponse.trim()) {\n const error = new Error('Codex SDK returned an empty response');\n error.statusCode = 502;\n throw error;\n }\n\n return {\n message: finalResponse.trim(),\n usage: {\n promptTokens: Number(usage?.input_tokens || 0),\n completionTokens: Number(usage?.output_tokens || 0),\n totalTokens: Number((usage?.input_tokens || 0) + (usage?.output_tokens || 0)),\n },\n };\n }\n\n const turn = await thread.run(prompt);\n if (!turn.finalResponse?.trim()) {\n const error = new Error('Codex SDK returned an empty response');\n error.statusCode = 502;\n throw error;\n }\n\n return {\n message: turn.finalResponse.trim(),\n usage: {\n promptTokens: Number(turn.usage?.input_tokens || 0),\n completionTokens: Number(turn.usage?.output_tokens || 0),\n totalTokens: Number((turn.usage?.input_tokens || 0) + (turn.usage?.output_tokens || 0)),\n },\n };\n}\n\nconst activeAgentRuns = new Map();\n\nfunction normalizeAgentMode(value) {\n const mode = safeUserText(value || 'dharma_guide');\n return /^[a-z][a-z0-9_:-]{0,48}$/i.test(mode) ? mode : 'dharma_guide';\n}\n\nfunction openClawSessionKey(user, conversationId) {\n const env = process.env.NODE_ENV === 'production' ? 'prod' : 'dev';\n return `fabushi:env:${env}:user:${user.userId}:conversation:${conversationId}`;\n}\n\nfunction createOpenClawRuntime() {\n if (!enableOpenClawAgentChat || !openClawGatewayToken || !openClawGatewayUrl) {\n return {\n enabled: false,\n provider: 'openclaw-gateway',\n reason: !enableOpenClawAgentChat\n ? 'ENABLE_OPENCLAW_AGENT_CHAT is not true'\n : !openClawGatewayToken\n ? 'OPENCLAW_GATEWAY_TOKEN is not configured'\n : 'OPENCLAW_GATEWAY_URL is not configured',\n };\n }\n\n return {\n enabled: true,\n provider: 'openclaw-gateway',\n model: openClawAgentId,\n runsUrl: openClawRunsEndpoint || `${openClawGatewayUrl}/v1/agents/${encodeURIComponent(openClawAgentId)}/runs`,\n };\n}\n\nfunction extractOpenClawText(payload) {\n if (!payload || typeof payload !== 'object') return '';\n const candidates = [\n payload.message,\n payload.content,\n payload.output_text,\n payload.finalResponse,\n payload.final_response,\n payload.result?.message,\n payload.result?.content,\n payload.output?.message,\n payload.output?.content,\n payload.choices?.[0]?.message?.content,\n ];\n for (const candidate of candidates) {\n if (typeof candidate === 'string' && candidate.trim()) return candidate.trim();\n }\n return '';\n}\n\nfunction extractOpenClawUsage(payload) {\n const usage = payload?.usage || payload?.result?.usage || {};\n return {\n promptTokens: Number(usage.input_tokens || usage.prompt_tokens || usage.promptTokens || 0),\n completionTokens: Number(usage.output_tokens || usage.completion_tokens || usage.completionTokens || 0),\n totalTokens: Number(usage.total_tokens || usage.totalTokens || 0),\n };\n}\n\nasync function callOpenClawAgent({ messages, user, conversationId, mode, signal }) {\n const runtime = createOpenClawRuntime();\n if (!runtime.enabled) {\n const error = new Error(runtime.reason || 'OpenClaw Agent chat is not enabled');\n error.statusCode = 503;\n throw error;\n }\n\n const sessionKey = openClawSessionKey(user, conversationId);\n const response = await fetch(runtime.runsUrl, {\n method: 'POST',\n headers: {\n Accept: 'application/json',\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${openClawGatewayToken}`,\n },\n body: JSON.stringify({\n agentId: openClawAgentId,\n sessionKey,\n mode,\n stream: false,\n input: { messages },\n messages,\n metadata: {\n app: 'fabushi',\n userId: user.userId,\n conversationId,\n toolPolicy: 'public-user-read-mostly',\n },\n }),\n signal: signal || AbortSignal.timeout(180_000),\n });\n\n const bodyText = await response.text();\n let payload;\n try {\n payload = bodyText ? JSON.parse(bodyText) : {};\n } catch {\n payload = { message: bodyText };\n }\n\n if (!response.ok) {\n const error = new Error(payload?.error?.message || payload?.message || `OpenClaw request failed: ${response.status}`);\n error.statusCode = response.status;\n throw error;\n }\n\n const message = extractOpenClawText(payload);\n if (!message) {\n const error = new Error('OpenClaw returned an empty response');\n error.statusCode = 502;\n throw error;\n }\n\n return {\n message,\n runId: payload.runId || payload.id || payload.result?.runId || null,\n provider: runtime.provider,\n model: runtime.model,\n usage: extractOpenClawUsage(payload),\n };\n}\n\nasync function runAgentModel({ messages, user, conversationId, mode, callbacks = {}, signal, maxCompletionTokens }) {\n const openClawRuntime = createOpenClawRuntime();\n if (openClawRuntime.enabled && !callbacks.onToken) {\n return await callOpenClawAgent({ messages, user, conversationId, mode, signal });\n }\n\n const codexRuntime = createCodexDeepSeekRuntime();\n const libreChatRuntime = createLibreChatAgentRuntime();\n let provider = libreChatRuntime.enabled\n ? libreChatRuntime.provider\n : codexRuntime.enabled\n ? codexRuntime.provider\n : 'deepseek';\n let model = libreChatRuntime.enabled ? libreChatRuntime.model : deepseekModel;\n\n try {\n if (libreChatRuntime.enabled) {\n const result = callbacks.onToken\n ? await callLibreChatAgentStream(messages, { ...callbacks, signal })\n : await callLibreChatAgent(messages);\n return { ...result, provider, model };\n }\n if (codexRuntime.enabled) {\n const result = await callCodexSdkDeepSeek(messages, { ...callbacks, signal });\n return { ...result, provider, model };\n }\n const result = callbacks.onToken\n ? await callDeepSeekStream(messages, { ...callbacks, signal, maxCompletionTokens })\n : await callDeepSeek(messages, { maxCompletionTokens });\n return { ...result, provider, model };\n } catch (error) {\n if (!codexRuntime.enabled || isDeepSeekQuotaError(error)) throw error;\n logger.warn({ error: String(error) }, 'Agent model failed, falling back to DeepSeek direct');\n const result = callbacks.onToken\n ? await callDeepSeekStream(messages, { ...callbacks, signal, maxCompletionTokens })\n : await callDeepSeek(messages, { maxCompletionTokens });\n return { ...result, provider: 'deepseek', model: deepseekModel };\n }\n}\n\nfunction updateAgentRunStatus(run, status, fields = {}) {\n statements.updateAgentRunStatus.run({\n id: run.id,\n userId: run.user_id || run.userId,\n status,\n openClawRunId: fields.openClawRunId ?? null,\n inputTokens: fields.inputTokens ?? null,\n outputTokens: fields.outputTokens ?? null,\n toolCallCount: fields.toolCallCount ?? null,\n completedAt: status === 'completed' || status === 'cancelled' ? nowIso() : null,\n failedAt: status === 'failed' ? nowIso() : null,\n errorCode: fields.errorCode ?? null,\n errorMessage: fields.errorMessage ?? null,\n });\n}\n\nfunction buildAgentMessages(conversationId) {\n const historyRows = statements.listMessages.all(conversationId).slice(-14);\n return [{ role: 'system', content: systemPrompt }, ...normalizeMessages(historyRows)];\n}\n\nfunction agentUsagePayload(usage, user, budget) {\n const promptTokens = usage.promptTokens || usage.inputTokens || 0;\n const completionTokens = usage.completionTokens || usage.outputTokens || 0;\n const totalTokens = usage.totalTokens || promptTokens + completionTokens;\n const latestUsage = usageFor(user.userId);\n return {\n inputTokens: promptTokens,\n outputTokens: completionTokens,\n totalTokens,\n monthlyLimit: budget.limit,\n remainingTokens: Math.max(0, budget.limit - latestUsage.used),\n };\n}\n\nfunction openAiContentToText(content) {\n if (typeof content === 'string') return content;\n if (Array.isArray(content)) {\n return content\n .map((part) => {\n if (typeof part === 'string') return part;\n if (!part || typeof part !== 'object') return '';\n return readText(part.text || part.content || part.value);\n })\n .filter(Boolean)\n .join('\\n');\n }\n return readText(content);\n}\n\nfunction normalizeOpenAiMessages(value) {\n if (!Array.isArray(value)) return [];\n const allowedRoles = new Set(['system', 'user', 'assistant', 'tool']);\n return value\n .map((item) => {\n if (!item || typeof item !== 'object') return null;\n const role = allowedRoles.has(item.role) ? item.role : 'user';\n const content = openAiContentToText(item.content);\n if (!content) return null;\n return { role, content };\n })\n .filter(Boolean);\n}\n\nfunction truncateForDeepSeekMessage(text, maxChars = 12_000) {\n const value = readText(text);\n if (value.length <= maxChars) return value;\n return `${value.slice(0, maxChars)}\\n\\n[内容已截断]`;\n}\n\nfunction truncateTailForDeepSeekMessage(text, maxChars = 2400) {\n const value = readText(text);\n if (value.length <= maxChars) return value;\n return `[前文已省略]\\n\\n${value.slice(-maxChars)}`;\n}\n\nfunction stripOpenClawPromptNoise(text) {\n const value = readText(text);\n if (!value) return '';\n\n const finalPromptMatch = value.match(/finalPromptText[\"'\\s::]+([\\s\\S]{1,4000})$/i);\n if (finalPromptMatch?.[1]) {\n return readText(finalPromptMatch[1].replace(/^[\"'`]+|[\"'`,}\\]]+$/g, ''));\n }\n\n const markers = [\n 'final prompt:',\n 'final user prompt:',\n 'user prompt:',\n '用户问题:',\n '用户消息:',\n '用户输入:',\n ];\n const lower = value.toLowerCase();\n for (const marker of markers) {\n const index = lower.lastIndexOf(marker.toLowerCase());\n if (index >= 0) {\n const tail = value.slice(index + marker.length).trim();\n if (tail) return tail;\n }\n }\n\n return value;\n}\n\nfunction compactOpenClawMessagesForDeepSeek(messages) {\n const conversationalMessages = messages\n .filter((message) => message.role === 'user' || message.role === 'assistant')\n .map((message) => ({\n role: message.role,\n content: truncateTailForDeepSeekMessage(stripOpenClawPromptNoise(message.content)),\n }))\n .filter((message) => message.content);\n\n const recent = conversationalMessages.slice(-6);\n while (recent.length > 0 && recent[0].role !== 'user') {\n recent.shift();\n }\n\n if (recent.length === 0) {\n const fallback = messages\n .slice()\n .reverse()\n .map((message) => truncateTailForDeepSeekMessage(stripOpenClawPromptNoise(message.content), 2400))\n .find(Boolean);\n if (!fallback) return messages;\n recent.push({ role: 'user', content: fallback });\n }\n\n return [\n {\n role: 'system',\n content: '你是大乘 App 的桌面 AI 助手。请直接回答用户问题,默认使用简洁、准确的中文。',\n },\n ...recent,\n ];\n}\n\nfunction isDeepSeekBlockedError(error) {\n return (\n Number(error?.statusCode) === 403 &&\n /blocked|被拦截|forbidden/i.test(String(error?.message || error || ''))\n );\n}\n\nfunction shouldRetryWithCompactedOpenClawMessages(error, messages, compactedMessages) {\n if (!isDeepSeekBlockedError(error)) return false;\n return JSON.stringify(messages) !== JSON.stringify(compactedMessages);\n}\n\nfunction openClawBlockedFallbackText(messages) {\n const lastUser = messages\n .slice()\n .reverse()\n .find((message) => message.role === 'user')?.content || '';\n const localActionIntent =\n /(file|folder|directory|project|workspace|path|文件|文件夹|目录|项目|工程|路径|本地)/i.test(lastUser) &&\n /(create|make|mkdir|write|edit|modify|rename|delete|move|copy|save|download|read|list|生成|创建|新建|写入|编辑|修改|重命名|删除|移动|复制|保存|下载|读取|列出)/i.test(lastUser);\n\n if (localActionIntent) {\n return [\n '本机工具规划被模型服务拦截了,所以这次没有实际读取、创建、修改或下载本机文件。',\n '如果只是新建文件夹,请先在底部选择项目后重新发送“新建文件夹”;桌面端会直接在已选项目里创建。',\n '如果是更复杂的本地文件操作,请在“连应用”里重启本机 AI 后重试,或把要操作的文件路径和目标说得更具体。',\n ].join('\\n');\n }\n\n return '模型服务拦截了这次回复,未能生成结果。请换一种更具体、更短的表述后重试。';\n}\nfunction usageWithFallback(usage, messages, text) {\n const promptTokens = Number(usage?.promptTokens || usage?.inputTokens || 0);\n const completionTokens = Number(usage?.completionTokens || usage?.outputTokens || 0);\n const totalTokens = Number(usage?.totalTokens || 0);\n if (totalTokens > 0) {\n return {\n promptTokens,\n completionTokens,\n totalTokens,\n };\n }\n const estimatedPrompt = promptTokens || estimateTokens(JSON.stringify(messages));\n const estimatedCompletion = completionTokens || estimateTokens(text);\n return {\n promptTokens: estimatedPrompt,\n completionTokens: estimatedCompletion,\n totalTokens: estimatedPrompt + estimatedCompletion,\n };\n}\n\nfunction openAiUsagePayload(usage, user, budget) {\n const promptTokens = Number(usage.promptTokens || 0);\n const completionTokens = Number(usage.completionTokens || 0);\n const totalTokens = Number(usage.totalTokens || promptTokens + completionTokens);\n const latestUsage = usageFor(user.userId);\n const remainingTokens = Math.max(0, budget.limit - latestUsage.used);\n return {\n prompt_tokens: promptTokens,\n completion_tokens: completionTokens,\n total_tokens: totalTokens,\n promptTokens,\n completionTokens,\n totalTokens,\n monthlyLimit: budget.limit,\n remainingTokens,\n };\n}\n\nfunction writeOpenAiSse(res, payload) {\n res.write(`data: ${JSON.stringify(payload)}\\n\\n`);\n}\n\napp.post(\n '/api/openclaw/deepseek/v1/chat/completions',\n asyncHandler(async (req, res) => {\n const user = await resolveUser(req, req.body || {});\n const incomingMessages = normalizeOpenAiMessages(req.body?.messages);\n if (incomingMessages.length === 0) {\n res.status(400).json({ error: { message: 'messages is required' } });\n return;\n }\n\n const messages = compactOpenClawMessagesForDeepSeek(incomingMessages);\n const promptEstimate = estimateTokens(JSON.stringify(messages));\n const estimated = promptEstimate + 600;\n const budget = enforceTokenBudget(user, estimated);\n const maxCompletionTokens = Math.min(\n completionBudgetFor(budget, promptEstimate),\n normalizeMaxCompletionTokens(req.body?.max_tokens),\n );\n const model = normalizeDeepSeekModelName(req.body?.model);\n const wantsStream =\n req.body?.stream === true ||\n String(req.get('accept') || '').includes('text/event-stream');\n const id = `chatcmpl_${crypto.randomUUID().replaceAll('-', '')}`;\n const created = Math.floor(Date.now() / 1000);\n const compactedMessages = messages;\n\n if (!wantsStream) {\n let result;\n let usageMessages = messages;\n try {\n result = await callDeepSeek(messages, { model, maxCompletionTokens });\n } catch (error) {\n if (!shouldRetryWithCompactedOpenClawMessages(error, messages, compactedMessages)) {\n throw error;\n }\n logger.warn(\n {\n statusCode: error?.statusCode,\n messageCount: messages.length,\n compactedMessageCount: compactedMessages.length,\n },\n 'OpenClaw DeepSeek proxy retrying with compacted messages',\n );\n usageMessages = compactedMessages;\n result = await callDeepSeek(compactedMessages, { model, maxCompletionTokens });\n }\n const usage = usageWithFallback(result.usage, usageMessages, result.message);\n recordUsage(user.userId, usage.totalTokens);\n res.json({\n id,\n object: 'chat.completion',\n created,\n model: result.model || model,\n choices: [\n {\n index: 0,\n message: { role: 'assistant', content: result.message },\n finish_reason: 'stop',\n },\n ],\n usage: openAiUsagePayload(usage, user, budget),\n });\n return;\n }\n\n const abortController = new AbortController();\n req.on('close', () => abortController.abort());\n\n res.status(200);\n res.set({\n 'Content-Type': 'text/event-stream; charset=utf-8',\n 'Cache-Control': 'no-cache, no-transform',\n Connection: 'keep-alive',\n 'X-Accel-Buffering': 'no',\n });\n res.flushHeaders?.();\n\n const baseChunk = {\n id,\n object: 'chat.completion.chunk',\n created,\n model,\n system_fingerprint: 'fabushi-openclaw-deepseek-proxy',\n };\n writeOpenAiSse(res, {\n ...baseChunk,\n choices: [{ index: 0, delta: { role: 'assistant' }, finish_reason: null }],\n });\n\n let text = '';\n let usageMessages = messages;\n try {\n let result;\n try {\n result = await callDeepSeekStream(messages, {\n model,\n maxCompletionTokens,\n signal: abortController.signal,\n onToken: (delta) => {\n text += delta;\n writeOpenAiSse(res, {\n ...baseChunk,\n choices: [{ index: 0, delta: { content: delta }, finish_reason: null }],\n });\n },\n });\n } catch (error) {\n if (!shouldRetryWithCompactedOpenClawMessages(error, messages, compactedMessages)) {\n throw error;\n }\n logger.warn(\n {\n statusCode: error?.statusCode,\n messageCount: messages.length,\n compactedMessageCount: compactedMessages.length,\n },\n 'OpenClaw DeepSeek proxy stream retrying with compacted messages',\n );\n text = '';\n usageMessages = compactedMessages;\n result = await callDeepSeekStream(compactedMessages, {\n model,\n maxCompletionTokens,\n signal: abortController.signal,\n onToken: (delta) => {\n text += delta;\n writeOpenAiSse(res, {\n ...baseChunk,\n choices: [{ index: 0, delta: { content: delta }, finish_reason: null }],\n });\n },\n });\n }\n text = result.message || text;\n const usage = usageWithFallback(result.usage, usageMessages, text);\n recordUsage(user.userId, usage.totalTokens);\n writeOpenAiSse(res, {\n ...baseChunk,\n model: result.model || model,\n choices: [{ index: 0, delta: {}, finish_reason: 'stop' }],\n });\n writeOpenAiSse(res, {\n ...baseChunk,\n model: result.model || model,\n choices: [],\n usage: openAiUsagePayload(usage, user, budget),\n });\n res.write('data: [DONE]\\n\\n');\n } catch (error) {\n if (isDeepSeekBlockedError(error)) {\n const fallbackMessage = openClawBlockedFallbackText(usageMessages);\n const usage = usageWithFallback(null, usageMessages, fallbackMessage);\n recordUsage(user.userId, usage.totalTokens);\n logger.warn(\n { statusCode: error?.statusCode, code: error?.code },\n 'OpenClaw DeepSeek proxy stream blocked; returning local fallback message',\n );\n writeOpenAiSse(res, {\n ...baseChunk,\n choices: [{ index: 0, delta: { content: fallbackMessage }, finish_reason: null }],\n });\n writeOpenAiSse(res, {\n ...baseChunk,\n choices: [{ index: 0, delta: {}, finish_reason: 'stop' }],\n });\n writeOpenAiSse(res, {\n ...baseChunk,\n choices: [],\n usage: openAiUsagePayload(usage, user, budget),\n });\n res.write('data: [DONE]\\n\\n');\n return;\n }\n logger.error(\n { error: String(error), code: error?.code, statusCode: error?.statusCode },\n 'OpenClaw DeepSeek proxy stream failed',\n );\n writeOpenAiSse(res, {\n ...baseChunk,\n choices: [],\n error: {\n message: publicAiErrorMessage(error),\n code: error?.code || 'OPENCLAW_DEEPSEEK_PROXY_ERROR',\n },\n });\n res.write('data: [DONE]\\n\\n');\n } finally {\n res.end();\n }\n }),\n);\n\napp.post(\n '/codex-deepseek/v1/responses',\n asyncHandler(async (req, res) => {\n const bearer = bearerToken(req);\n if (!deepseekApiKey || bearer !== deepseekApiKey) {\n res.status(401).json({\n error: { message: 'Unauthorized Codex DeepSeek adapter request' },\n });\n return;\n }\n\n const prompt = codexResponsesPrompt(req.body);\n if (!prompt) {\n res.status(400).json({ error: { message: 'Responses input is required' } });\n return;\n }\n\n const model = String(req.body?.model || deepseekModel);\n const responseId = `resp_${crypto.randomUUID().replaceAll('-', '')}`;\n const itemId = `msg_${crypto.randomUUID().replaceAll('-', '')}`;\n const wantsStream =\n req.body?.stream !== false ||\n String(req.get('accept') || '').includes('text/event-stream');\n const messages = [{ role: 'user', content: prompt }];\n\n if (!wantsStream) {\n const result = await callDeepSeek(messages);\n res.json(\n responsesPayload({\n responseId,\n itemId,\n status: 'completed',\n model,\n text: result.message,\n usage: result.usage,\n }),\n );\n return;\n }\n\n res.status(200);\n res.set({\n 'Content-Type': 'text/event-stream; charset=utf-8',\n 'Cache-Control': 'no-cache, no-transform',\n Connection: 'keep-alive',\n 'X-Accel-Buffering': 'no',\n });\n res.flushHeaders?.();\n\n writeResponsesEvent(res, 'response.created', {\n type: 'response.created',\n response: responsesPayload({\n responseId,\n itemId,\n status: 'in_progress',\n model,\n }),\n });\n writeResponsesEvent(res, 'response.output_item.added', {\n type: 'response.output_item.added',\n output_index: 0,\n item: {\n id: itemId,\n type: 'message',\n role: 'assistant',\n status: 'in_progress',\n content: [],\n },\n });\n writeResponsesEvent(res, 'response.content_part.added', {\n type: 'response.content_part.added',\n item_id: itemId,\n output_index: 0,\n content_index: 0,\n part: { type: 'output_text', text: '' },\n });\n\n let text = '';\n try {\n const result = await callDeepSeekStream(messages, {\n onToken: (delta) => {\n text += delta;\n writeResponsesEvent(res, 'response.output_text.delta', {\n type: 'response.output_text.delta',\n item_id: itemId,\n output_index: 0,\n content_index: 0,\n delta,\n });\n },\n });\n text = result.message || text;\n writeResponsesEvent(res, 'response.output_text.done', {\n type: 'response.output_text.done',\n item_id: itemId,\n output_index: 0,\n content_index: 0,\n text,\n });\n writeResponsesEvent(res, 'response.content_part.done', {\n type: 'response.content_part.done',\n item_id: itemId,\n output_index: 0,\n content_index: 0,\n part: { type: 'output_text', text },\n });\n writeResponsesEvent(res, 'response.output_item.done', {\n type: 'response.output_item.done',\n output_index: 0,\n item: {\n id: itemId,\n type: 'message',\n role: 'assistant',\n status: 'completed',\n content: [{ type: 'output_text', text }],\n },\n });\n writeResponsesEvent(res, 'response.completed', {\n type: 'response.completed',\n response: responsesPayload({\n responseId,\n itemId,\n status: 'completed',\n model,\n text,\n usage: result.usage,\n }),\n });\n res.end();\n } catch (error) {\n writeResponsesEvent(res, 'response.failed', {\n type: 'response.failed',\n response: responsesPayload({\n responseId,\n itemId,\n status: 'failed',\n model,\n text,\n error,\n }),\n });\n res.end();\n }\n }),\n);\n\napp.use(\n '/api/openclaw/runtime/files',\n express.static(openClawRuntimeUpdatesDir, {\n fallthrough: false,\n immutable: true,\n maxAge: '7d',\n setHeaders(res) {\n res.setHeader('X-Content-Type-Options', 'nosniff');\n },\n }),\n);\n\napp.get(\n '/api/openclaw/runtime/manifest',\n asyncHandler(async (req, res) => {\n const platform = readText(req.query.platform);\n const currentVersion = readText(req.query.currentVersion);\n const appVersion = readText(req.query.appVersion);\n\n let manifest;\n try {\n manifest = await readJsonFile(openClawRuntimeManifestPath);\n } catch (error) {\n logger.info(\n { manifestPath: openClawRuntimeManifestPath, error: String(error) },\n 'OpenClaw runtime manifest unavailable',\n );\n return jsonResponse(res, 200, {\n success: true,\n schema: 1,\n updateAvailable: false,\n reason: 'manifest-unavailable',\n platform,\n currentVersion,\n appVersion,\n });\n }\n\n const latest = manifest.latest && typeof manifest.latest === 'object'\n ? manifest.latest\n : manifest;\n const platforms = latest.platforms && typeof latest.platforms === 'object'\n ? latest.platforms\n : {};\n const platformRelease = platform ? platforms[platform] : null;\n const version = readText(latest.version || manifest.version);\n\n if (!platform || !platformRelease || typeof platformRelease !== 'object') {\n return jsonResponse(res, 200, {\n success: true,\n schema: Number(manifest.schema || 1),\n channel: readText(manifest.channel || latest.channel || 'stable'),\n updateAvailable: false,\n reason: platform ? 'platform-unavailable' : 'platform-required',\n platform,\n currentVersion,\n appVersion,\n latestVersion: version,\n });\n }\n\n if (platform.startsWith('macos-') && !openClawRuntimeEnableMacosUpdates) {\n return jsonResponse(res, 200, {\n success: true,\n schema: Number(manifest.schema || 1),\n channel: readText(manifest.channel || latest.channel || 'stable'),\n updateAvailable: false,\n reason: 'macos-runtime-updates-disabled',\n platform,\n currentVersion,\n appVersion,\n latestVersion: version,\n });\n }\n\n const downloadUrl = absolutizeOpenClawRuntimeUrl(\n req,\n platformRelease.archiveUrl || platformRelease.url || platformRelease.fileName,\n );\n const release = {\n version,\n channel: readText(manifest.channel || latest.channel || 'stable'),\n minAppVersion: readText(latest.minAppVersion || platformRelease.minAppVersion),\n notes: readText(latest.notes || platformRelease.notes),\n nodeExecutable: readText(\n platformRelease.nodeExecutable || latest.nodeExecutable || 'node/bin/node',\n ),\n cliEntrypoint: readText(\n platformRelease.cliEntrypoint || latest.cliEntrypoint || 'openclaw/openclaw.mjs',\n ),\n defaultPort: Number(latest.defaultPort || manifest.defaultPort || 18789),\n defaultModel: readText(latest.defaultModel || manifest.defaultModel || 'openclaw/default'),\n defaultModelOverride: readText(latest.defaultModelOverride || manifest.defaultModelOverride),\n gatewayArgs: Array.isArray(latest.gatewayArgs)\n ? latest.gatewayArgs.map((item) => String(item))\n : ['gateway', '--port', '{port}', '--force'],\n archiveUrl: downloadUrl,\n sha256: readText(platformRelease.sha256),\n size: Number(platformRelease.size || 0),\n };\n\n jsonResponse(res, 200, {\n success: true,\n schema: Number(manifest.schema || 1),\n updateAvailable: Boolean(version && downloadUrl && version !== currentVersion),\n platform,\n currentVersion,\n appVersion,\n latest: release,\n generatedAt: readText(manifest.generatedAt || latest.generatedAt),\n });\n }),\n);\n\napp.get('/health', (_req, res) => {\n const codexRuntime = createCodexDeepSeekRuntime();\n const libreChatRuntime = createLibreChatAgentRuntime();\n const openClawRuntime = createOpenClawRuntime();\n jsonResponse(res, 200, {\n status: 'ok',\n service: 'dacheng-ai-backend',\n provider: libreChatRuntime.enabled ? libreChatRuntime.provider : 'deepseek',\n model: libreChatRuntime.enabled ? libreChatRuntime.model : deepseekModel,\n deepseekModel,\n deepseekAvailableModels,\n libreChatAgentChatEnabled: libreChatRuntime.enabled,\n libreChatAgentProvider: libreChatRuntime.provider,\n libreChatAgentReason: libreChatRuntime.enabled ? undefined : libreChatRuntime.reason,\n codexSdkAvailable: true,\n codexSdkChatEnabled: codexRuntime.enabled,\n codexSdkProvider: codexRuntime.provider,\n openClawAgentChatEnabled: openClawRuntime.enabled,\n openClawAgentProvider: openClawRuntime.provider,\n openClawAgentReason: openClawRuntime.enabled ? undefined : openClawRuntime.reason,\n requireMemberForAi,\n memberMonthlyLimit,\n freeMonthlyLimit,\n deepseekMaxCompletionTokens,\n openClawRuntimeUpdatesDir,\n openClawRuntimeManifestConfigured: fs.existsSync(openClawRuntimeManifestPath),\n cbetaApiRoot,\n timestamp: nowIso(),\n });\n});\n\napp.get(\n '/api/miniapps/registry',\n asyncHandler(async (req, res) => {\n const user = await resolveUser(req, { username: req.query.username });\n const official = officialMiniAppRegistry(req);\n const store = await readMiniAppStore();\n const visibleSandbox = store.apps.filter((appRecord) => {\n if (appRecord.reviewStatus === 'approved' && appRecord.source === 'marketplace') {\n return true;\n }\n return appRecord.ownerId === user.userId;\n });\n const bots = [\n ...official.bots,\n ...visibleSandbox.map((record) => miniAppRecordToBot(record)),\n ];\n const miniApps = [\n ...official.miniApps,\n ...visibleSandbox.map((record) => miniAppRecordToManifest(req, record)),\n ];\n const registry = {\n schemaVersion: 1,\n hostApiVersion: miniAppHostApiVersion,\n bots,\n miniApps,\n updatedAt: nowIso(),\n };\n registry.signature = sha256(JSON.stringify({ bots, miniApps }));\n jsonResponse(res, 200, { success: true, registry });\n }),\n);\n\napp.get(\n '/api/miniapps/dev/:miniAppId/index.html',\n asyncHandler(async (req, res) => {\n const miniAppId = safeMiniAppId(req.params.miniAppId);\n const store = await readMiniAppStore();\n const record = store.apps.find((appRecord) => appRecord.miniAppId === miniAppId);\n if (!record) {\n res.status(404).type('text/html').send('<h1>Mini app not found</h1>');\n return;\n }\n res.setHeader('Content-Type', 'text/html; charset=utf-8');\n res.setHeader('Cache-Control', 'no-store');\n res.send(record.sourceHtml || generatedMiniAppHtml(record));\n }),\n);\n\napp.post(\n '/api/miniapps/dev/create',\n asyncHandler(async (req, res) => {\n const user = await resolveUser(req, req.body || {});\n const title = safeUserText(req.body?.title, '个人小程序').slice(0, 80);\n const subtitle = safeUserText(req.body?.subtitle, '个人沙箱小程序').slice(0, 120);\n const permissions = sanitizeMiniAppPermissions(req.body?.permissions);\n const prompt = safeUserText(req.body?.prompt || title);\n const idSeed = crypto.randomUUID().replaceAll('-', '').slice(0, 12);\n const miniAppId = `sandbox.${idSeed}`;\n const botId = `bot.${miniAppId}`;\n const sourceHtml = generatedMiniAppHtml({ title, prompt });\n const scan = miniAppScan(permissions, sourceHtml);\n const createdAt = nowIso();\n const record = {\n ownerId: user.userId,\n miniAppId,\n botId,\n title,\n subtitle,\n initials: title.slice(0, 1) || '小',\n icon: 'code',\n avatarColor: '#3D8BFF',\n greeting: '你好,我是这个个人沙箱小程序的机器人。',\n inputHint: '输入消息,或打开小程序面板',\n version: '0.0.1',\n permissions,\n surfaces: ['chatPanel'],\n theme: 'telegramDark',\n source: 'sandbox',\n reviewStatus: 'sandbox',\n sourceHtml,\n scan,\n createdAt,\n updatedAt: createdAt,\n };\n const store = await readMiniAppStore();\n store.apps.push(record);\n await writeMiniAppStore(store);\n jsonResponse(res, 200, {\n success: true,\n miniApp: miniAppRecordToManifest(req, record),\n bot: miniAppRecordToBot(record),\n scan,\n });\n }),\n);\n\napp.post(\n '/api/miniapps/dev/:miniAppId/version',\n asyncHandler(async (req, res) => {\n const user = await resolveUser(req, req.body || {});\n const miniAppId = safeMiniAppId(req.params.miniAppId);\n const store = await readMiniAppStore();\n const index = store.apps.findIndex((appRecord) => appRecord.miniAppId === miniAppId);\n if (index < 0) {\n return jsonResponse(res, 404, { success: false, message: 'mini app not found' });\n }\n const record = store.apps[index];\n if (record.ownerId !== user.userId) {\n return jsonResponse(res, 403, { success: false, message: 'not your mini app' });\n }\n const title = safeUserText(req.body?.title, record.title).slice(0, 80);\n const subtitle = safeUserText(req.body?.subtitle, record.subtitle).slice(0, 120);\n const sourceHtml =\n safeUserText(req.body?.sourceHtml) ||\n generatedMiniAppHtml({ title, prompt: safeUserText(req.body?.prompt || title) });\n const permissions = sanitizeMiniAppPermissions(req.body?.permissions || record.permissions);\n const scan = miniAppScan(permissions, sourceHtml);\n store.apps[index] = {\n ...record,\n title,\n subtitle,\n sourceHtml,\n permissions,\n scan,\n reviewStatus: 'sandbox',\n version: safeUserText(req.body?.version, record.version || '0.0.1'),\n updatedAt: nowIso(),\n };\n await writeMiniAppStore(store);\n jsonResponse(res, 200, {\n success: true,\n miniApp: miniAppRecordToManifest(req, store.apps[index]),\n bot: miniAppRecordToBot(store.apps[index]),\n scan,\n });\n }),\n);\n\napp.post(\n '/api/miniapps/:miniAppId/submit-review',\n asyncHandler(async (req, res) => {\n const user = await resolveUser(req, req.body || {});\n const miniAppId = safeMiniAppId(req.params.miniAppId);\n const store = await readMiniAppStore();\n const index = store.apps.findIndex((appRecord) => appRecord.miniAppId === miniAppId);\n if (index < 0) {\n return jsonResponse(res, 404, { success: false, message: 'mini app not found' });\n }\n const record = store.apps[index];\n if (record.ownerId !== user.userId) {\n return jsonResponse(res, 403, { success: false, message: 'not your mini app' });\n }\n const scan = miniAppScan(record.permissions || [], record.sourceHtml || '');\n if (!scan.passed) {\n return jsonResponse(res, 400, {\n success: false,\n message: 'security scan failed',\n scan,\n });\n }\n store.apps[index] = {\n ...record,\n scan,\n reviewStatus: 'pending_review',\n updatedAt: nowIso(),\n };\n await writeMiniAppStore(store);\n jsonResponse(res, 200, {\n success: true,\n miniApp: miniAppRecordToManifest(req, store.apps[index]),\n scan,\n });\n }),\n);\n\napp.post(\n '/api/botfather/generate-miniapp',\n asyncHandler(async (req, res) => {\n const user = await resolveUser(req, req.body || {});\n const prompt = safeUserText(req.body?.prompt);\n if (!prompt) {\n return jsonResponse(res, 400, { success: false, message: 'prompt is required' });\n }\n const title = titleFromPromptForMiniApp(prompt);\n const subtitle = '机器人之父生成的个人沙箱小程序';\n const requestedPermissions = Array.isArray(req.body?.permissions) ? req.body.permissions : [];\n const generated = await generateBotFatherMiniAppHtml({ title, prompt, user });\n let sourceHtml = generated.sourceHtml;\n let generation = generated.generation;\n const permissions = sanitizeMiniAppPermissions([\n ...requestedPermissions,\n ...inferMiniAppPermissionsFromText(prompt, sourceHtml),\n ]);\n let scan = miniAppScan(permissions, sourceHtml);\n if (!scan.passed) {\n sourceHtml = generatedMiniAppHtml({ title, prompt });\n generation = {\n provider: 'template',\n fallbackReason: 'ai_output_failed_security_scan',\n };\n scan = miniAppScan(permissions, sourceHtml);\n }\n const idSeed = crypto.randomUUID().replaceAll('-', '').slice(0, 12);\n const miniAppId = `sandbox.${idSeed}`;\n const createdAt = nowIso();\n const record = {\n ownerId: user.userId,\n miniAppId,\n botId: `bot.${miniAppId}`,\n title,\n subtitle,\n initials: title.slice(0, 1) || '小',\n icon: 'code',\n avatarColor: '#3D8BFF',\n greeting: `我是「${title}」的机器人。`,\n inputHint: '继续描述修改需求,或打开小程序面板',\n version: '0.0.1',\n permissions,\n surfaces: ['chatPanel'],\n theme: 'telegramDark',\n source: 'sandbox',\n reviewStatus: 'sandbox',\n sourceHtml,\n scan,\n generation,\n prompt,\n createdAt,\n updatedAt: createdAt,\n };\n const store = await readMiniAppStore();\n store.apps.push(record);\n await writeMiniAppStore(store);\n jsonResponse(res, 200, {\n success: true,\n miniApp: miniAppRecordToManifest(req, record),\n bot: miniAppRecordToBot(record),\n scan,\n generation,\n actions: ['openMiniApp', 'continueEditing', 'submitReview'],\n });\n }),\n);\n\napp.get('/api/ai/models', (_req, res) => {\n jsonResponse(res, 200, {\n success: true,\n provider: 'deepseek',\n defaultModel: deepseekModel,\n models: deepseekAvailableModels.map((model) => ({\n id: model,\n label: model === 'deepseek-reasoner' ? 'DeepSeek Reasoner' : 'DeepSeek Chat',\n })),\n });\n});\napp.get(\n '/api/ai/quota',\n asyncHandler(async (req, res) => {\n const user = await resolveUser(req, { username: req.query.username });\n const budget = enforceTokenBudget(user, 0);\n const usage = usageFor(user.userId);\n jsonResponse(res, 200, {\n success: true,\n isMember: user.isMember,\n monthlyLimit: budget.limit,\n usedTokens: usage.used,\n remainingTokens: Math.max(0, budget.limit - usage.used),\n month: usage.month,\n });\n }),\n);\n\napp.post(\n '/api/agent/chat',\n asyncHandler(async (req, res) => {\n const message = safeUserText(req.body?.message);\n if (!message) {\n return jsonResponse(res, 400, { success: false, message: 'message is required' });\n }\n\n const user = await resolveUser(req, req.body || {});\n const promptEstimate = estimateTokens(message);\n const estimated = promptEstimate + 600;\n const budget = enforceTokenBudget(user, estimated);\n const maxCompletionTokens = completionBudgetFor(budget, promptEstimate);\n const createdAt = nowIso();\n const conversationId = safeUserText(req.body?.conversationId) || crypto.randomUUID();\n const mode = normalizeAgentMode(req.body?.mode);\n const stream = req.body?.stream === true;\n let conversation = statements.getConversation.get(conversationId, user.userId);\n const isNewConversation = !conversation;\n\n if (isNewConversation) {\n conversation = { id: conversationId, user_id: user.userId, title: titleFromMessage(message) };\n statements.insertConversation.run({\n id: conversationId,\n userId: user.userId,\n title: conversation.title,\n provider: 'agent',\n model: openClawAgentId,\n createdAt,\n updatedAt: createdAt,\n });\n }\n\n const messageId = safeUserText(req.body?.messageId) || crypto.randomUUID();\n statements.insertMessage.run({\n id: messageId,\n conversationId,\n role: 'user',\n content: message,\n promptTokens: 0,\n completionTokens: 0,\n totalTokens: 0,\n createdAt,\n });\n\n const runId = `run_${crypto.randomUUID().replaceAll('-', '')}`;\n const sessionKey = openClawSessionKey(user, conversationId);\n const openClawRuntime = createOpenClawRuntime();\n statements.insertAgentRun.run({\n id: runId,\n userId: user.userId,\n conversationId,\n messageId,\n openClawRunId: null,\n openClawSessionKey: sessionKey,\n status: stream ? 'queued' : 'running',\n mode,\n provider: openClawRuntime.enabled ? openClawRuntime.provider : 'dacheng-ai-fallback',\n model: openClawRuntime.enabled ? openClawAgentId : deepseekModel,\n startedAt: createdAt,\n });\n\n if (stream) {\n return jsonResponse(res, 200, {\n success: true,\n runId,\n conversationId,\n streamUrl: `/api/agent/runs/${runId}/events`,\n });\n }\n\n const controller = new AbortController();\n activeAgentRuns.set(runId, controller);\n try {\n const skillResult = await runResourceFinderSkill({ message, user });\n const skillContext = resourceContextMessage(skillResult);\n const historyRows = statements.listMessages.all(conversationId).slice(-14);\n const modelMessages = [\n { role: 'system', content: systemPrompt },\n ...(skillContext ? [{ role: 'system', content: skillContext }] : []),\n ...normalizeMessages(historyRows),\n ];\n\n const aiResult = await runAgentModel({\n messages: modelMessages,\n user,\n conversationId,\n mode,\n signal: controller.signal,\n maxCompletionTokens,\n });\n const usage = {\n promptTokens: aiResult.usage?.promptTokens || estimateTokens(JSON.stringify(modelMessages)),\n completionTokens: aiResult.usage?.completionTokens || estimateTokens(aiResult.message),\n totalTokens:\n aiResult.usage?.totalTokens ||\n estimateTokens(JSON.stringify(modelMessages)) + estimateTokens(aiResult.message),\n };\n recordUsage(user.userId, usage.totalTokens);\n const answeredAt = nowIso();\n const assistantMessageId = crypto.randomUUID();\n statements.insertMessage.run({\n id: assistantMessageId,\n conversationId,\n role: 'assistant',\n content: aiResult.message,\n promptTokens: usage.promptTokens,\n completionTokens: usage.completionTokens,\n totalTokens: usage.totalTokens,\n createdAt: answeredAt,\n });\n statements.updateConversation.run({\n id: conversationId,\n userId: user.userId,\n title: isNewConversation ? titleFromMessage(message) : conversation.title,\n updatedAt: answeredAt,\n });\n updateAgentRunStatus({ id: runId, user_id: user.userId }, 'completed', {\n openClawRunId: aiResult.runId,\n inputTokens: usage.promptTokens,\n outputTokens: usage.completionTokens,\n });\n const action = determineClientAction(skillResult, message);\n return jsonResponse(res, 200, {\n success: true,\n runId,\n conversationId,\n messageId: assistantMessageId,\n provider: aiResult.provider,\n model: aiResult.model,\n message: aiResult.message,\n usage: agentUsagePayload(usage, user, budget),\n ...(action ? { clientAction: action } : {}),\n });\n } catch (error) {\n const status = error.name === 'AbortError' ? 'cancelled' : 'failed';\n updateAgentRunStatus({ id: runId, user_id: user.userId }, status, {\n errorCode: error.name || 'AGENT_RUN_FAILED',\n errorMessage: publicAiErrorMessage(error),\n });\n if (status === 'cancelled') {\n return jsonResponse(res, 499, { success: false, runId, status: 'cancelled' });\n }\n throw error;\n } finally {\n activeAgentRuns.delete(runId);\n }\n }),\n);\n\napp.get(\n '/api/agent/runs/:runId/events',\n asyncHandler(async (req, res) => {\n const user = await resolveUser(req, {});\n const run = statements.getAgentRun.get(req.params.runId, user.userId);\n if (!run) {\n return jsonResponse(res, 404, { success: false, message: 'run not found' });\n }\n if (run.status === 'completed' || run.status === 'failed' || run.status === 'cancelled') {\n return jsonResponse(res, 409, { success: false, message: `run is already ${run.status}` });\n }\n\n sseHeaders(res);\n const controller = new AbortController();\n activeAgentRuns.set(run.id, controller);\n updateAgentRunStatus(run, 'running');\n writeSse(res, 'run.started', { runId: run.id, conversationId: run.conversation_id });\n\n try {\n let streamedText = '';\n const modelMessages = buildAgentMessages(run.conversation_id);\n const lastUserMsg = [...modelMessages].reverse().find(m => m.role === 'user');\n const userMsgText = lastUserMsg ? lastUserMsg.content : '';\n const skillResult = userMsgText ? await runResourceFinderSkill({\n message: userMsgText,\n user,\n onStep: (step) => writeSse(res, 'tool.call.completed', {\n tool: 'agent.step',\n displayName: step.title || 'Agent step',\n summary: step.message || '',\n }),\n }) : null;\n const skillContext = resourceContextMessage(skillResult);\n const finalMessages = skillContext ? [\n modelMessages[0],\n { role: 'system', content: skillContext },\n ...modelMessages.slice(1)\n ] : modelMessages;\n\n const promptEstimate = estimateTokens(JSON.stringify(finalMessages));\n const estimated = promptEstimate + 600;\n const budget = enforceTokenBudget(user, estimated);\n const maxCompletionTokens = completionBudgetFor(budget, promptEstimate);\n const aiResult = await runAgentModel({\n messages: finalMessages,\n user,\n conversationId: run.conversation_id,\n mode: run.mode,\n signal: controller.signal,\n maxCompletionTokens,\n callbacks: {\n onStep: (step) => writeSse(res, 'tool.call.completed', {\n tool: 'agent.step',\n displayName: step.title || 'Agent step',\n summary: step.message || '',\n }),\n onToken: (delta) => {\n streamedText += delta;\n writeSse(res, 'assistant.delta', { text: delta });\n },\n },\n });\n const message = aiResult.message || streamedText.trim();\n const usage = {\n promptTokens: aiResult.usage?.promptTokens || estimateTokens(JSON.stringify(finalMessages)),\n completionTokens: aiResult.usage?.completionTokens || estimateTokens(message),\n totalTokens:\n aiResult.usage?.totalTokens || estimateTokens(JSON.stringify(finalMessages)) + estimateTokens(message),\n };\n recordUsage(user.userId, usage.totalTokens);\n const assistantMessageId = crypto.randomUUID();\n const answeredAt = nowIso();\n statements.insertMessage.run({\n id: assistantMessageId,\n conversationId: run.conversation_id,\n role: 'assistant',\n content: message,\n promptTokens: usage.promptTokens,\n completionTokens: usage.completionTokens,\n totalTokens: usage.totalTokens,\n createdAt: answeredAt,\n });\n statements.updateConversation.run({\n id: run.conversation_id,\n userId: user.userId,\n title: titleFromMessage(message),\n updatedAt: answeredAt,\n });\n updateAgentRunStatus(run, 'completed', {\n openClawRunId: aiResult.runId,\n inputTokens: usage.promptTokens,\n outputTokens: usage.completionTokens,\n });\n const action = determineClientAction(skillResult, userMsgText);\n writeSse(res, 'assistant.message', { messageId: assistantMessageId, content: message });\n writeSse(res, 'run.completed', {\n usage: agentUsagePayload(usage, user, budget),\n ...(action ? { clientAction: action } : {}),\n });\n res.end();\n } catch (error) {\n const status = error.name === 'AbortError' ? 'cancelled' : 'failed';\n updateAgentRunStatus(run, status, {\n errorCode: error.name || 'AGENT_RUN_FAILED',\n errorMessage: publicAiErrorMessage(error),\n });\n writeSse(res, status === 'cancelled' ? 'run.cancelled' : 'run.failed', {\n runId: run.id,\n message: publicAiErrorMessage(error),\n });\n res.end();\n } finally {\n activeAgentRuns.delete(run.id);\n }\n }),\n);\n\napp.post(\n '/api/agent/runs/:runId/cancel',\n asyncHandler(async (req, res) => {\n const user = await resolveUser(req, req.body || {});\n const run = statements.getAgentRun.get(req.params.runId, user.userId);\n if (!run) {\n return jsonResponse(res, 404, { success: false, message: 'run not found' });\n }\n const controller = activeAgentRuns.get(run.id);\n if (controller) controller.abort();\n updateAgentRunStatus(run, 'cancelled');\n return jsonResponse(res, 200, { success: true, runId: run.id, status: 'cancelled' });\n }),\n);\n\napp.post(\n '/api/agent/messages/:messageId/feedback',\n asyncHandler(async (req, res) => {\n const user = await resolveUser(req, req.body || {});\n const rating = safeUserText(req.body?.rating);\n if (!['up', 'down'].includes(rating)) {\n return jsonResponse(res, 400, { success: false, message: 'rating must be up or down' });\n }\n statements.insertAgentFeedback.run({\n id: crypto.randomUUID(),\n messageId: req.params.messageId,\n userId: user.userId,\n rating,\n reason: safeUserText(req.body?.reason),\n comment: safeUserText(req.body?.comment),\n createdAt: nowIso(),\n });\n return jsonResponse(res, 200, { success: true });\n }),\n);\n\napp.post(\n '/api/ai/chat',\n asyncHandler(async (req, res) => {\n const message = safeUserText(req.body?.message);\n if (!message) {\n return jsonResponse(res, 400, { success: false, message: 'message is required' });\n }\n\n const user = await resolveUser(req, req.body || {});\n const promptEstimate = estimateTokens(message);\n const estimated = promptEstimate + 600;\n const budget = enforceTokenBudget(user, estimated);\n const maxCompletionTokens = completionBudgetFor(budget, promptEstimate);\n const createdAt = nowIso();\n const conversationId = safeUserText(req.body?.conversationId) || crypto.randomUUID();\n const requestedModel = normalizeDeepSeekModelName(req.body?.model);\n const clientContext = clientContextMessage(req.body?.client);\n let conversation = statements.getConversation.get(conversationId, user.userId);\n const isNewConversation = !conversation;\n\n if (isNewConversation) {\n conversation = {\n id: conversationId,\n user_id: user.userId,\n title: titleFromMessage(message),\n };\n statements.insertConversation.run({\n id: conversationId,\n userId: user.userId,\n title: conversation.title,\n provider: 'deepseek',\n model: requestedModel,\n createdAt,\n updatedAt: createdAt,\n });\n }\n\n statements.insertMessage.run({\n id: crypto.randomUUID(),\n conversationId,\n role: 'user',\n content: message,\n promptTokens: 0,\n completionTokens: 0,\n totalTokens: 0,\n createdAt,\n });\n\n const libreChatRuntime = createLibreChatAgentRuntime();\n const skillResult = await runResourceFinderSkill({ message, user });\n const skillContext = resourceContextMessage(skillResult);\n const historyRows = statements.listMessages.all(conversationId).slice(-14);\n const modelMessages = [\n { role: 'system', content: systemPrompt },\n ...(clientContext ? [{ role: 'system', content: clientContext }] : []),\n ...(skillContext ? [{ role: 'system', content: skillContext }] : []),\n ...normalizeMessages(historyRows),\n ];\n\n const openClawRuntime = createOpenClawRuntime();\n const codexRuntime = createCodexDeepSeekRuntime();\n let provider = openClawRuntime.enabled\n ? openClawRuntime.provider\n : libreChatRuntime.enabled\n ? libreChatRuntime.provider\n : 'deepseek';\n let responseModel = openClawRuntime.enabled\n ? openClawRuntime.model\n : libreChatRuntime.enabled\n ? libreChatRuntime.model\n : requestedModel;\n let aiResult;\n try {\n if (openClawRuntime.enabled) {\n aiResult = await callOpenClawAgent({\n messages: modelMessages,\n user,\n conversationId,\n mode: normalizeAgentMode(req.body?.mode),\n });\n provider = aiResult.provider || provider;\n responseModel = aiResult.model || responseModel;\n } else if (libreChatRuntime.enabled) {\n aiResult = await callLibreChatAgent(modelMessages);\n } else if (codexRuntime.enabled) {\n provider = codexRuntime.provider;\n aiResult = await callCodexSdkDeepSeek(modelMessages);\n } else {\n aiResult = await callDeepSeek(modelMessages, { model: requestedModel, maxCompletionTokens });\n }\n } catch (error) {\n if (libreChatRuntime.enabled) {\n logger.warn({ error: String(error) }, 'LibreChat Agent chat failed, falling back to Codex SDK or DeepSeek direct');\n provider = codexRuntime.enabled ? codexRuntime.provider : 'deepseek';\n responseModel = requestedModel;\n if (codexRuntime.enabled) {\n try {\n aiResult = await callCodexSdkDeepSeek(modelMessages);\n } catch (codexError) {\n if (isDeepSeekQuotaError(codexError)) throw codexError;\n logger.warn({ error: String(codexError) }, 'Codex SDK chat failed, falling back to DeepSeek direct');\n provider = 'deepseek';\n aiResult = await callDeepSeek(modelMessages, { model: requestedModel, maxCompletionTokens });\n }\n } else {\n aiResult = await callDeepSeek(modelMessages, { model: requestedModel, maxCompletionTokens });\n }\n } else {\n if (!codexRuntime.enabled || isDeepSeekQuotaError(error)) throw error;\n logger.warn({ error: String(error) }, 'Codex SDK chat failed, falling back to DeepSeek direct');\n provider = 'deepseek';\n responseModel = requestedModel;\n aiResult = await callDeepSeek(modelMessages, { model: requestedModel, maxCompletionTokens });\n }\n }\n const usage = {\n promptTokens: aiResult.usage.promptTokens || estimateTokens(JSON.stringify(modelMessages)),\n completionTokens: aiResult.usage.completionTokens || estimateTokens(aiResult.message),\n totalTokens:\n aiResult.usage.totalTokens ||\n estimateTokens(JSON.stringify(modelMessages)) + estimateTokens(aiResult.message),\n };\n recordUsage(user.userId, usage.totalTokens);\n\n const answeredAt = nowIso();\n statements.insertMessage.run({\n id: crypto.randomUUID(),\n conversationId,\n role: 'assistant',\n content: aiResult.message,\n promptTokens: usage.promptTokens,\n completionTokens: usage.completionTokens,\n totalTokens: usage.totalTokens,\n createdAt: answeredAt,\n });\n statements.updateConversation.run({\n id: conversationId,\n userId: user.userId,\n title: isNewConversation ? titleFromMessage(message) : conversation.title,\n updatedAt: answeredAt,\n });\n\n const latestUsage = usageFor(user.userId);\n const monthlyLimit = budget.limit;\n const action = determineClientAction(skillResult, message);\n jsonResponse(res, 200, {\n success: true,\n conversationId,\n provider,\n model: responseModel,\n message: aiResult.message,\n usage: {\n ...usage,\n monthlyLimit,\n remainingTokens: Math.max(0, monthlyLimit - latestUsage.used),\n },\n ...(action ? { clientAction: action } : {}),\n });\n }),\n);\n\napp.post('/api/ai/chat/stream', async (req, res) => {\n sseHeaders(res);\n\n try {\n const message = safeUserText(req.body?.message);\n if (!message) {\n writeSse(res, 'error', { message: 'message is required' });\n res.end();\n return;\n }\n\n const user = await resolveUser(req, req.body || {});\n const promptEstimate = estimateTokens(message);\n const estimated = promptEstimate + 600;\n const budget = enforceTokenBudget(user, estimated);\n const maxCompletionTokens = completionBudgetFor(budget, promptEstimate);\n const createdAt = nowIso();\n const conversationId = safeUserText(req.body?.conversationId) || crypto.randomUUID();\n const requestedModel = normalizeDeepSeekModelName(req.body?.model);\n const clientContext = clientContextMessage(req.body?.client);\n let conversation = statements.getConversation.get(conversationId, user.userId);\n const isNewConversation = !conversation;\n\n if (isNewConversation) {\n conversation = {\n id: conversationId,\n user_id: user.userId,\n title: titleFromMessage(message),\n };\n statements.insertConversation.run({\n id: conversationId,\n userId: user.userId,\n title: conversation.title,\n provider: 'deepseek',\n model: requestedModel,\n createdAt,\n updatedAt: createdAt,\n });\n }\n\n statements.insertMessage.run({\n id: crypto.randomUUID(),\n conversationId,\n role: 'user',\n content: message,\n promptTokens: 0,\n completionTokens: 0,\n totalTokens: 0,\n createdAt,\n });\n\n const codexRuntime = createCodexDeepSeekRuntime();\n const libreChatRuntime = createLibreChatAgentRuntime();\n const initialOpenClawRuntime = createOpenClawRuntime();\n let provider = initialOpenClawRuntime.enabled\n ? initialOpenClawRuntime.provider\n : libreChatRuntime.enabled\n ? libreChatRuntime.provider\n : codexRuntime.enabled\n ? codexRuntime.provider\n : 'deepseek';\n let responseModel = initialOpenClawRuntime.enabled\n ? initialOpenClawRuntime.model\n : libreChatRuntime.enabled\n ? libreChatRuntime.model\n : requestedModel;\n writeSse(res, 'meta', {\n conversationId,\n provider,\n model: responseModel,\n });\n const skillResult = await runResourceFinderSkill({\n message,\n user,\n onStep: (step) => writeSse(res, 'step', step),\n });\n const skillContext = resourceContextMessage(skillResult);\n const historyRows = statements.listMessages.all(conversationId).slice(-14);\n const modelMessages = [\n { role: 'system', content: systemPrompt },\n ...(clientContext ? [{ role: 'system', content: clientContext }] : []),\n ...(skillContext ? [{ role: 'system', content: skillContext }] : []),\n ...normalizeMessages(historyRows),\n ];\n\n let aiResult;\n const openClawRuntime = initialOpenClawRuntime;\n try {\n if (openClawRuntime.enabled) {\n aiResult = await callOpenClawAgent({\n messages: modelMessages,\n user,\n conversationId,\n mode: normalizeAgentMode(req.body?.mode),\n });\n provider = aiResult.provider || provider;\n responseModel = aiResult.model || responseModel;\n writeSse(res, 'delta', { text: aiResult.message });\n } else if (libreChatRuntime.enabled) {\n aiResult = await callLibreChatAgentStream(modelMessages, {\n onToken: (token) => writeSse(res, 'delta', { text: token }),\n });\n } else if (codexRuntime.enabled) {\n aiResult = await callCodexSdkDeepSeek(modelMessages, {\n onToken: (token) => writeSse(res, 'delta', { text: token }),\n onStep: (step) => writeSse(res, 'step', step),\n });\n } else {\n aiResult = await callDeepSeekStream(modelMessages, {\n model: requestedModel,\n maxCompletionTokens,\n onToken: (token) => writeSse(res, 'delta', { text: token }),\n });\n }\n } catch (error) {\n if (libreChatRuntime.enabled) {\n logger.warn({ error: String(error) }, 'LibreChat Agent streaming chat failed, falling back to Codex SDK or DeepSeek direct');\n provider = codexRuntime.enabled ? codexRuntime.provider : 'deepseek';\n responseModel = requestedModel;\n writeSse(res, 'meta', {\n conversationId,\n provider,\n model: responseModel,\n });\n if (codexRuntime.enabled) {\n try {\n aiResult = await callCodexSdkDeepSeek(modelMessages, {\n onToken: (token) => writeSse(res, 'delta', { text: token }),\n onStep: (step) => writeSse(res, 'step', step),\n });\n } catch (codexError) {\n if (isDeepSeekQuotaError(codexError)) throw codexError;\n logger.warn({ error: String(codexError) }, 'Codex SDK streaming chat failed, falling back to DeepSeek direct');\n provider = 'deepseek';\n responseModel = requestedModel;\n writeSse(res, 'meta', {\n conversationId,\n provider,\n model: responseModel,\n });\n aiResult = await callDeepSeekStream(modelMessages, {\n model: requestedModel,\n maxCompletionTokens,\n onToken: (token) => writeSse(res, 'delta', { text: token }),\n });\n }\n } else {\n aiResult = await callDeepSeekStream(modelMessages, {\n model: requestedModel,\n maxCompletionTokens,\n onToken: (token) => writeSse(res, 'delta', { text: token }),\n });\n }\n } else {\n if (!codexRuntime.enabled || isDeepSeekQuotaError(error)) throw error;\n logger.warn({ error: String(error) }, 'Codex SDK streaming chat failed, falling back to DeepSeek direct');\n provider = 'deepseek';\n responseModel = requestedModel;\n writeSse(res, 'meta', {\n conversationId,\n provider,\n model: responseModel,\n });\n aiResult = await callDeepSeekStream(modelMessages, {\n model: requestedModel,\n maxCompletionTokens,\n onToken: (token) => writeSse(res, 'delta', { text: token }),\n });\n }\n }\n\n const usage = {\n promptTokens: aiResult.usage.promptTokens || estimateTokens(JSON.stringify(modelMessages)),\n completionTokens: aiResult.usage.completionTokens || estimateTokens(aiResult.message),\n totalTokens:\n aiResult.usage.totalTokens ||\n estimateTokens(JSON.stringify(modelMessages)) + estimateTokens(aiResult.message),\n };\n recordUsage(user.userId, usage.totalTokens);\n\n const answeredAt = nowIso();\n statements.insertMessage.run({\n id: crypto.randomUUID(),\n conversationId,\n role: 'assistant',\n content: aiResult.message,\n promptTokens: usage.promptTokens,\n completionTokens: usage.completionTokens,\n totalTokens: usage.totalTokens,\n createdAt: answeredAt,\n });\n statements.updateConversation.run({\n id: conversationId,\n userId: user.userId,\n title: isNewConversation ? titleFromMessage(message) : conversation.title,\n updatedAt: answeredAt,\n });\n\n const latestUsage = usageFor(user.userId);\n const monthlyLimit = budget.limit;\n const action = determineClientAction(skillResult, message);\n writeSse(res, 'done', {\n conversationId,\n provider,\n model: responseModel,\n message: aiResult.message,\n usage: {\n ...usage,\n monthlyLimit,\n remainingTokens: Math.max(0, monthlyLimit - latestUsage.used),\n },\n ...(action ? { clientAction: action } : {}),\n });\n res.end();\n } catch (error) {\n logger.error({ error: error.stack || String(error) }, 'streaming chat failed');\n writeSse(res, 'error', {\n message: publicAiErrorMessage(error),\n details: error.details,\n });\n res.end();\n }\n});\n\napp.get(\n '/api/ai/conversations',\n asyncHandler(async (req, res) => {\n const user = await resolveUser(req, { username: req.query.username });\n const items = statements.listConversations.all(user.userId).map((item) => ({\n id: item.id,\n title: item.title,\n updatedAt: item.updatedAt,\n }));\n jsonResponse(res, 200, { success: true, items });\n }),\n);\n\napp.get(\n '/api/ai/conversations/:id',\n asyncHandler(async (req, res) => {\n const user = await resolveUser(req);\n const conversation = statements.getConversation.get(req.params.id, user.userId);\n if (!conversation) {\n return jsonResponse(res, 404, { success: false, message: 'conversation not found' });\n }\n const messages = statements.listMessages.all(req.params.id).map((item) => ({\n role: item.role,\n content: item.content,\n createdAt: item.createdAt,\n }));\n jsonResponse(res, 200, {\n success: true,\n conversation: {\n id: conversation.id,\n title: conversation.title,\n updatedAt: conversation.updated_at,\n },\n messages,\n });\n }),\n);\n\nfunction buildUrl(base, endpoint, params = {}) {\n const url = new URL(endpoint, `${base}/`);\n for (const [key, value] of Object.entries(params)) {\n if (Array.isArray(value)) {\n for (const item of value) {\n if (item !== undefined && item !== null && item !== '') {\n url.searchParams.append(key, String(item));\n }\n }\n } else if (value !== undefined && value !== null && value !== '') {\n url.searchParams.set(key, String(value));\n }\n }\n return url;\n}\n\nasync function fetchJson(url) {\n const response = await fetch(url, {\n headers: { Accept: 'application/json' },\n signal: AbortSignal.timeout(15_000),\n });\n if (!response.ok) {\n throw new Error(`Request failed: ${response.status}`);\n }\n return response.json();\n}\n\nfunction stripHtml(value) {\n return String(value || '')\n .replace(/<script[\\s\\S]*?<\\/script>/gi, ' ')\n .replace(/<style[\\s\\S]*?<\\/style>/gi, ' ')\n .replace(/<[^>]+>/g, ' ')\n .replace(/ /g, ' ')\n .replace(/</g, '<')\n .replace(/>/g, '>')\n .replace(/&/g, '&')\n .replace(/&#(\\d+);/g, (_match, code) => String.fromCharCode(Number(code)))\n .replace(/\\s+/g, ' ')\n .trim();\n}\n\nfunction cleanExtractedText(value) {\n let text = String(value || '')\n .replace(/\\b[A-Z]{1,3}\\d{2}n\\d{4,5}[a-z]?_p?\\d{4}[a-z]\\d{2}\\b/g, ' ')\n .replace(/\\uFFFD/g, '')\n .replace(/\\s+([,。;:!?、」』)》】])/g, '$1')\n .replace(/([「『《(【])\\s+/g, '$1');\n for (let i = 0; i < 3; i += 1) {\n text = text.replace(/([\\u3400-\\u9fff])\\s+([\\u3400-\\u9fff])/g, '$1$2');\n }\n return text.replace(/[ \\t]{2,}/g, ' ').trim();\n}\n\nfunction compactText(value, limit = 140) {\n const text = stripHtml(value);\n return text.length > limit ? `${text.slice(0, limit)}...` : text;\n}\n\nfunction collectSearchResults(data) {\n if (!data || typeof data !== 'object') return [];\n if (Array.isArray(data.results)) return data.results;\n if (Array.isArray(data?.results?.docs)) return data.results.docs;\n if (Array.isArray(data?.response?.docs)) return data.response.docs;\n if (Array.isArray(data.docs)) return data.docs;\n return [];\n}\n\nfunction firstText(item, keys) {\n for (const key of keys) {\n const value = item?.[key];\n if (Array.isArray(value)) {\n const first = value.map((entry) => safeUserText(String(entry))).find(Boolean);\n if (first) return first;\n }\n if (typeof value === 'string' && value.trim()) return value.trim();\n if (typeof value === 'number') return String(value);\n }\n return '';\n}\n\nfunction firstNumber(item, keys, fallback = undefined) {\n for (const key of keys) {\n const value = item?.[key];\n if (typeof value === 'number') return value;\n if (typeof value === 'string' && value.trim() && Number.isFinite(Number(value))) {\n return Number(value);\n }\n }\n return fallback;\n}\n\nasync function searchCbetaTitle(query, limit) {\n const url = buildUrl(cbetaApiRoot, 'search/title', { q: query, start: 0, rows: limit });\n const data = await fetchJson(url);\n return collectSearchResults(data).flatMap((item) => {\n const work = firstText(item, ['work', 'work_id', 'workId']);\n const title = firstText(item, ['content', 'title', 'work_title', 'workTitle']);\n if (!work || !title) return [];\n const juan = firstNumber(item, ['juan', 'juan_num', 'juanNum'], 1);\n return [\n {\n id: `cbeta-title-${work}-${juan}`,\n title,\n sourceName: 'CBETA 佛典',\n url: `cbeta:${work}:${juan}`,\n snippet: firstText(item, ['byline', 'creators', 'time_dynasty']) || '可下载经文正文并用于全球法布施。',\n resourceType: 'scripture',\n work,\n juan,\n },\n ];\n });\n}\n\nasync function searchCbetaContent(query, limit) {\n const endpoints = ['search/fulltext', 'search/content', 'search/all_in_one'];\n const settled = await Promise.allSettled(\n endpoints.map((endpoint) =>\n fetchJson(buildUrl(cbetaApiRoot, endpoint, { q: query, start: 0, rows: limit, field: 'content' })),\n ),\n );\n const results = [];\n for (const response of settled) {\n if (response.status !== 'fulfilled') continue;\n for (const item of collectSearchResults(response.value)) {\n const work = firstText(item, ['work', 'work_id', 'workId', 'sutra', 'id']);\n const snippet = compactText(firstText(item, ['kwic', 'highlight', 'snippet', 'content', 'text', 'body', 'p']));\n if (!work || !snippet) continue;\n const juan = firstNumber(item, ['juan', 'juan_num', 'juanNum', '卷'], 1);\n const title = firstText(item, ['title', 'work_title', 'workTitle', 'sutra_name', 'book', 'name']) || work;\n results.push({\n id: `cbeta-content-${work}-${juan}-${sha256(snippet).slice(0, 8)}`,\n title,\n sourceName: 'CBETA 佛典',\n url: `cbeta:${work}:${juan}`,\n snippet,\n resourceType: 'scripture',\n work,\n juan,\n });\n if (results.length >= limit) return results;\n }\n }\n return results;\n}\n\nfunction directUrlResource(query) {\n const url = safeUserText(query);\n let parsed;\n try {\n parsed = new URL(url);\n } catch {\n return null;\n }\n if (!['http:', 'https:'].includes(parsed.protocol)) return null;\n return {\n id: `url-${sha256(url).slice(0, 12)}`,\n title: parsed.hostname,\n sourceName: '网页资源',\n url,\n snippet: '直接下载并提取这个链接的可读正文。',\n resourceType: 'web',\n };\n}\n\nfunction looksLikeAudioQuery(query) {\n return /音乐|音樂|音频|音訊|mp3|audio|念诵|念誦|诵经|誦經|唱诵|唱誦|读诵|讀誦/.test(\n safeUserText(query).toLowerCase(),\n );\n}\n\nfunction archiveSearchTerm(query) {\n return (\n safeUserText(query)\n .replace(/下载|查找|寻找|找一?个|资源|音频|音訊|音乐|音樂|mp3|audio|念诵|念誦|诵经|誦經|唱诵|唱誦|读诵|讀誦/gi, ' ')\n .replace(/\\s+/g, ' ')\n .trim() || safeUserText(query)\n );\n}\n\nfunction solrPhrase(value) {\n return String(value).replace(/\\\\/g, '\\\\\\\\').replace(/\"/g, '\\\\\"');\n}\n\nasync function searchInternetArchiveAudio(query, limit) {\n if (!looksLikeAudioQuery(query)) return [];\n const term = archiveSearchTerm(query);\n if (!term) return [];\n const variants = Array.from(\n new Set([\n term,\n term.replaceAll('金刚经', '金剛經'),\n term.replaceAll('金剛經', '金刚经'),\n ].filter(Boolean)),\n );\n const phraseQuery = variants\n .flatMap((item) => [`title:\"${solrPhrase(item)}\"`, `description:\"${solrPhrase(item)}\"`])\n .join(' OR ');\n const url = buildUrl('https://archive.org', 'advancedsearch.php', {\n q: `(${phraseQuery}) AND mediatype:audio`,\n rows: Math.min(Math.max(limit, 1), 10),\n output: 'json',\n 'fl[]': ['identifier', 'title', 'description'],\n });\n const data = await fetchJson(url);\n const docs = Array.isArray(data?.response?.docs) ? data.response.docs : [];\n return docs\n .map((item) => {\n const identifier = firstText(item, ['identifier']);\n const title = firstText(item, ['title']) || identifier;\n if (!identifier || !title) return null;\n return {\n id: `ia-audio-${identifier}`,\n title,\n sourceName: 'Internet Archive 音频',\n url: `ia:${identifier}`,\n snippet: compactText(firstText(item, ['description'])) || '公开可访问的音频资源,将下载第一个可用 MP3 文件。',\n resourceType: 'audio',\n identifier,\n };\n })\n .filter(Boolean);\n}\n\nconst curatedCbetaWorks = [\n {\n work: 'T0235',\n juan: 1,\n title: '金剛般若波羅蜜經',\n keywords: ['金剛般若波羅蜜經', '金刚般若波罗蜜经', '金剛經', '金刚经'],\n snippet: '姚秦 鳩摩羅什譯。常用于读诵、共修与法布施的般若经典。',\n },\n {\n work: 'T0251',\n juan: 1,\n title: '般若波羅蜜多心經',\n keywords: ['般若波羅蜜多心經', '般若波罗蜜多心经', '心經', '心经'],\n snippet: '唐 玄奘譯。篇幅短,适合加入禅室功课本与日常持诵。',\n },\n {\n work: 'T0262',\n juan: 1,\n title: '妙法蓮華經',\n keywords: ['妙法蓮華經', '妙法莲华经', '法華經', '法华经'],\n snippet: '姚秦 鳩摩羅什譯。可按卷下载正文,用于长期功课与分享。',\n },\n {\n work: 'T0279',\n juan: 1,\n title: '大方廣佛華嚴經',\n keywords: ['大方廣佛華嚴經', '大方广佛华严经', '華嚴經', '华严经'],\n snippet: '唐 實叉難陀譯。可按卷下载正文,用于系统研读与法布施。',\n },\n {\n work: 'T0360',\n juan: 1,\n title: '佛說無量壽經',\n keywords: ['佛說無量壽經', '佛说无量寿经', '無量壽經', '无量寿经'],\n snippet: '净土经典。可下载正文,用于净土功课本与全球法布施。',\n },\n {\n work: 'T0365',\n juan: 1,\n title: '佛說觀無量壽佛經',\n keywords: ['佛說觀無量壽佛經', '佛说观无量寿佛经', '觀無量壽經', '观无量寿经'],\n snippet: '净土三经之一。适合查找下载后加入净土功课。',\n },\n {\n work: 'T0366',\n juan: 1,\n title: '佛說阿彌陀經',\n keywords: ['佛說阿彌陀經', '佛说阿弥陀经', '阿彌陀經', '阿弥陀经'],\n snippet: '姚秦 鳩摩羅什譯。篇幅适中,适合共修、持诵与分享。',\n },\n {\n work: 'T0475',\n juan: 1,\n title: '維摩詰所說經',\n keywords: ['維摩詰所說經', '维摩诘所说经', '維摩詰經', '维摩诘经'],\n snippet: '姚秦 鳩摩羅什譯。大乘经典,可按卷下载用于研读。',\n },\n {\n work: 'T0676',\n juan: 1,\n title: '解深密經',\n keywords: ['解深密經', '解深密经'],\n snippet: '唯识相关大乘经典。适合研读、整理笔记与法布施。',\n },\n {\n work: 'T1666',\n juan: 1,\n title: '大乘起信論',\n keywords: ['大乘起信論', '大乘起信论', '起信論', '起信论'],\n snippet: '大乘论典。适合下载后作为研习功课资料。',\n },\n];\n\nfunction localCbetaResults(query, limit) {\n const normalizedQuery = safeUserText(query).toLowerCase();\n if (!normalizedQuery) return [];\n return curatedCbetaWorks\n .filter((item) =>\n item.keywords.some((keyword) => {\n const normalizedKeyword = keyword.toLowerCase();\n return normalizedKeyword.includes(normalizedQuery) || normalizedQuery.includes(normalizedKeyword);\n }),\n )\n .slice(0, limit)\n .map((item) => ({\n id: `cbeta-curated-${item.work}-${item.juan}`,\n title: item.title,\n sourceName: 'CBETA 佛典',\n url: `cbeta:${item.work}:${item.juan}`,\n snippet: item.snippet,\n resourceType: 'scripture',\n work: item.work,\n juan: item.juan,\n }));\n}\n\nfunction uniqueResources(items, limit) {\n const seen = new Set();\n const results = [];\n for (const item of items) {\n const key = `${item.url}:${item.title}`;\n if (seen.has(key)) continue;\n seen.add(key);\n results.push(item);\n if (results.length >= limit) break;\n }\n return results;\n}\n\nasync function findResourceCandidates(query, limit) {\n const direct = directUrlResource(query);\n const results = [];\n if (direct) results.push(direct);\n try {\n results.push(...(await searchInternetArchiveAudio(query, limit)));\n } catch (error) {\n logger.warn({ error: String(error), query }, 'Internet Archive audio search skipped');\n }\n results.push(...localCbetaResults(query, limit));\n\n const [titleResults, contentResults] = await Promise.allSettled([\n searchCbetaTitle(query, limit),\n searchCbetaContent(query, limit),\n ]);\n if (titleResults.status === 'fulfilled') results.push(...titleResults.value);\n if (contentResults.status === 'fulfilled') results.push(...contentResults.value);\n\n return uniqueResources(results, limit);\n}\n\nfunction looksLikeResourceTask(message) {\n const text = safeUserText(message).toLowerCase();\n if (!text) return false;\n return /下载|查找|寻找|找一?个|资源|經文|经文|佛经|佛經|仪轨|儀軌|音乐|音樂|mp3|audio|念诵|念誦|诵经|誦經/.test(text);\n}\n\nfunction extractResourceQuery(message) {\n const text = safeUserText(message)\n .replace(/请|請|帮我|幫我|麻烦|麻煩|可以|能不能|需要|我要|想要/g, ' ')\n .replace(/下载|下載|查找|寻找|尋找|找一个|找一個|找|资源|資源|并|並|然后|然後|用于|用於|全球法布施|加入功课本|加入功課本/g, ' ')\n .replace(/\\s+/g, ' ')\n .trim();\n return text || safeUserText(message);\n}\n\nfunction resourceContextMessage(skillResult) {\n if (!skillResult) return '';\n const lines = [\n '本轮资源检索/下载已经由大乘 App 后端完成。',\n '不要再次调用 search_dharma_resources、download_dharma_resource 或其他资源 MCP 工具;请直接基于以下结果回答用户。',\n `后端已调用 skill: ${skillResult.skillName}`,\n `执行目标: ${skillResult.query}`,\n ];\n if (skillResult.selected) {\n lines.push(`候选资源: ${skillResult.selected.title}`);\n lines.push(`来源: ${skillResult.selected.sourceName}`);\n lines.push(`资源地址: ${skillResult.selected.url}`);\n }\n if (skillResult.downloaded) {\n lines.push(`下载标题: ${skillResult.downloaded.title}`);\n lines.push(`下载文件: ${skillResult.downloaded.fileName}`);\n const text = safeUserText(skillResult.downloaded.contentText);\n if (text) {\n lines.push('下载正文摘录:');\n lines.push(text.slice(0, 8000));\n }\n }\n return lines.join('\\n');\n}\n\nfunction determineClientAction(skillResult, message) {\n if (skillResult && skillResult.downloaded) {\n const text = safeUserText(message);\n if (/法布施|全球法布施|分享/.test(text)) {\n return {\n type: 'prepare_dharma_share_text',\n title: skillResult.downloaded.title,\n text: skillResult.downloaded.contentText || '',\n };\n }\n }\n return null;\n}\n\nasync function runResourceFinderSkill({ message, user, onStep }) {\n if (!looksLikeResourceTask(message)) return null;\n\n const skillName = 'resource-finder-downloader';\n const query = extractResourceQuery(message);\n const audioTask = looksLikeAudioQuery(message) || looksLikeAudioQuery(query);\n onStep?.({\n type: 'skill.step',\n skillName,\n stage: 'start',\n title: '调用资源查找下载 skill',\n message: `识别到资源任务: ${query}`,\n });\n\n onStep?.({\n type: 'skill.step',\n skillName,\n stage: 'search',\n title: '搜索可分享资源',\n message: audioTask\n ? '优先检索公开可访问音频资源,失败时再回落到佛典正文资源。'\n : '优先检索 CBETA 佛典正文接口,搜索索引不可用时使用内置经典映射。',\n });\n const candidates = await findResourceCandidates(query, 8);\n if (candidates.length === 0) {\n onStep?.({\n type: 'skill.step',\n skillName,\n stage: 'empty',\n title: '没有找到可下载资源',\n message: '后端没有找到明确可下载且可分享的资源。',\n });\n return {\n skillName,\n query,\n candidates: [],\n selected: null,\n downloaded: null,\n };\n }\n\n const selected = candidates[0];\n onStep?.({\n type: 'skill.step',\n skillName,\n stage: 'select',\n title: '选择可信候选资源',\n message: `${selected.title} / ${selected.sourceName}`,\n });\n\n onStep?.({\n type: 'skill.step',\n skillName,\n stage: 'download',\n title: '下载并提取资源',\n message: selected.url,\n });\n const cbeta = parseCbetaResource(selected);\n const internetArchive = parseInternetArchiveResource(selected);\n const downloaded = cbeta\n ? await downloadCbetaResource({ ...selected, ...cbeta })\n : internetArchive\n ? await downloadInternetArchiveAudio({ ...selected, ...internetArchive })\n : await downloadWebResource(selected);\n const persisted = persistDownloadedResource(user, downloaded);\n\n onStep?.({\n type: 'skill.step',\n skillName,\n stage: 'done',\n title: '资源已准备完成',\n message: downloaded.fileName,\n });\n\n return {\n skillName,\n query,\n candidates,\n selected,\n downloaded: {\n ...downloaded,\n downloadId: persisted.id,\n },\n };\n}\n\napp.post(\n '/api/resources/search',\n asyncHandler(async (req, res) => {\n const query = safeUserText(req.body?.query);\n const limit = Math.min(Math.max(Number(req.body?.limit || 12), 1), 20);\n if (query.length < 2) {\n return jsonResponse(res, 400, { success: false, message: 'query is required' });\n }\n\n const items = await findResourceCandidates(query, limit);\n\n jsonResponse(res, 200, {\n success: true,\n source: 'resource-index',\n items,\n });\n }),\n);\n\nfunction parseCbetaResource(input) {\n const work = safeUserText(input.work);\n const juan = Number(input.juan || 1);\n if (work) return { work, juan: Number.isFinite(juan) ? juan : 1 };\n\n const url = safeUserText(input.url);\n const match = /^cbeta:([^:]+):(\\d+)$/i.exec(url);\n if (!match) return null;\n return {\n work: match[1].toUpperCase(),\n juan: Number(match[2] || 1),\n };\n}\n\nfunction parseInternetArchiveResource(input) {\n const url = safeUserText(input.url);\n const match = /^ia:([^:]+)$/i.exec(url);\n const identifier = match?.[1] || safeUserText(input.identifier);\n if (!identifier) return null;\n return { identifier };\n}\n\nfunction safeFileName(value) {\n const normalized = String(value || 'dharma-resource')\n .replace(/[\\\\/:*?\"<>|]+/g, '-')\n .replace(/\\s+/g, '-')\n .replace(/-+/g, '-')\n .replace(/^-|-$/g, '')\n .slice(0, 80);\n return normalized || 'dharma-resource';\n}\n\nfunction extractCbetaHtml(data) {\n const first = Array.isArray(data?.results) ? data.results[0] : null;\n if (typeof first === 'string') return first;\n if (first && typeof first === 'object' && typeof first.html === 'string') return first.html;\n if (typeof data?.html === 'string') return data.html;\n return '';\n}\n\nasync function downloadCbetaResource(resource) {\n const url = buildUrl(cbetaApiRoot, 'juans', {\n work: resource.work,\n juan: resource.juan,\n work_info: 1,\n toc: 1,\n });\n const data = await fetchJson(url);\n const workInfo = data.work_info || {};\n const title = safeUserText(workInfo.title) || safeUserText(resource.title) || resource.work;\n const html = extractCbetaHtml(data);\n const text = cleanExtractedText(stripHtml(html));\n if (!text) throw new Error('CBETA resource returned empty content');\n return {\n title,\n sourceName: 'CBETA 佛典',\n url: `cbeta:${resource.work}:${resource.juan}`,\n contentText: text.slice(0, maxResourceTextChars),\n fileName: `${resource.work}_${resource.juan}_${safeFileName(title)}.txt`,\n };\n}\n\nasync function downloadInternetArchiveAudio(resource) {\n const identifier = safeUserText(resource.identifier);\n if (!identifier) throw new Error('Internet Archive identifier is required');\n const metadata = await fetchJson(`https://archive.org/metadata/${encodeURIComponent(identifier)}`);\n const title = safeUserText(metadata?.metadata?.title) || safeUserText(resource.title) || identifier;\n const files = Array.isArray(metadata?.files) ? metadata.files : [];\n const audioFile = files.find((file) => {\n const name = safeUserText(file?.name);\n const format = safeUserText(file?.format).toLowerCase();\n return /\\.(mp3|m4a|ogg)$/i.test(name) || format.includes('mp3');\n });\n if (!audioFile?.name) {\n throw new Error('Internet Archive audio item has no downloadable audio file');\n }\n const sourceFileName = audioFile.name;\n const fileUrl = `https://archive.org/download/${encodeURIComponent(identifier)}/${sourceFileName\n .split('/')\n .map((part) => encodeURIComponent(part))\n .join('/')}`;\n const response = await fetch(fileUrl, {\n headers: {\n Accept: 'audio/mpeg, audio/*;q=0.9, */*;q=0.5',\n 'User-Agent': 'DachengResourceDownloader/1.0 (+https://ombhrum.com)',\n },\n signal: AbortSignal.timeout(60_000),\n });\n if (!response.ok) {\n throw new Error(`Internet Archive audio download failed: ${response.status}`);\n }\n const binaryContent = Buffer.from(await response.arrayBuffer());\n if (!binaryContent.length) throw new Error('Internet Archive audio returned empty content');\n const sizeMb = (binaryContent.length / 1024 / 1024).toFixed(1);\n return {\n title,\n sourceName: 'Internet Archive 音频',\n url: fileUrl,\n contentText: `已下载音频资源: ${title}\\n文件名: ${sourceFileName}\\n大小: ${sizeMb} MB\\n来源: ${fileUrl}`,\n binaryContent,\n fileName: safeFileName(sourceFileName),\n };\n}\n\nasync function downloadWebResource(input) {\n const url = safeUserText(input.url);\n const parsed = new URL(url);\n if (!['http:', 'https:'].includes(parsed.protocol)) {\n throw new Error('Only http/https resources can be downloaded');\n }\n\n const response = await fetch(parsed, {\n headers: {\n Accept: 'text/html, text/plain, application/xhtml+xml, application/xml;q=0.8, */*;q=0.5',\n 'User-Agent': 'DachengResourceDownloader/1.0 (+https://ombhrum.com)',\n },\n signal: AbortSignal.timeout(25_000),\n });\n if (!response.ok) throw new Error(`Resource download failed: ${response.status}`);\n const contentType = response.headers.get('content-type') || '';\n const body = await response.text();\n const title = safeUserText(input.title) || parsed.hostname;\n const contentText = contentType.includes('html') ? cleanExtractedText(stripHtml(body)) : body.trim();\n if (!contentText) throw new Error('Resource returned empty content');\n return {\n title,\n sourceName: safeUserText(input.sourceName) || parsed.hostname,\n url,\n contentText: contentText.slice(0, maxResourceTextChars),\n fileName: `${safeFileName(title)}.txt`,\n };\n}\n\nfunction persistDownloadedResource(user, content) {\n const id = crypto.randomUUID();\n const fileName = `${id}-${safeFileName(content.fileName)}`;\n const filePath = path.join(resourcesDir, fileName);\n if (content.binaryContent) {\n fs.writeFileSync(filePath, content.binaryContent);\n } else {\n fs.writeFileSync(filePath, content.contentText, 'utf8');\n }\n statements.insertResourceDownload.run({\n id,\n userId: user.userId,\n title: content.title,\n sourceName: content.sourceName,\n sourceUrl: content.url,\n fileName: content.fileName,\n filePath,\n createdAt: nowIso(),\n });\n return { id, fileName, filePath };\n}\n\nfunction createLibreChatMcpServer() {\n const server = new McpServer({\n name: 'dacheng-ai-tools',\n version: '0.1.0',\n });\n\n server.registerTool(\n 'search_dharma_resources',\n {\n title: 'Search dharma resources',\n description:\n 'Search for legally shareable Buddhist scripture, text, web, or audio resources that can be prepared for Fabushi sharing.',\n inputSchema: {\n query: z.string().min(2).max(200),\n limit: z.number().int().min(1).max(12).optional(),\n },\n outputSchema: {\n query: z.string(),\n items: z.array(\n z.object({\n id: z.string(),\n title: z.string(),\n sourceName: z.string(),\n url: z.string(),\n snippet: z.string(),\n resourceType: z.string(),\n work: z.string().optional(),\n juan: z.number().optional(),\n }),\n ),\n },\n annotations: {\n readOnlyHint: true,\n destructiveHint: false,\n openWorldHint: true,\n },\n },\n async ({ query, limit }) => {\n const items = await findResourceCandidates(query, limit ?? 8);\n return textToolResult(`Found ${items.length} candidate resources for \"${query}\".`, {\n query,\n items,\n });\n },\n );\n\n server.registerTool(\n 'download_dharma_resource',\n {\n title: 'Download dharma resource',\n description:\n 'Download and extract a selected resource. Use the url/work/juan returned by search_dharma_resources whenever possible.',\n inputSchema: {\n url: z.string().min(1).max(1000),\n title: z.string().max(300).optional(),\n sourceName: z.string().max(120).optional(),\n work: z.string().max(40).optional(),\n juan: z.number().int().min(1).optional(),\n identifier: z.string().max(300).optional(),\n },\n outputSchema: {\n title: z.string(),\n sourceName: z.string(),\n url: z.string(),\n fileName: z.string(),\n downloadId: z.string(),\n contentText: z.string(),\n },\n annotations: {\n readOnlyHint: false,\n destructiveHint: false,\n openWorldHint: true,\n },\n },\n async (input) => {\n const user = { userId: 'librechat:mcp' };\n const cbeta = parseCbetaResource(input);\n const internetArchive = parseInternetArchiveResource(input);\n const content = cbeta\n ? await downloadCbetaResource({ ...input, ...cbeta })\n : internetArchive\n ? await downloadInternetArchiveAudio({ ...input, ...internetArchive })\n : await downloadWebResource(input);\n const persisted = persistDownloadedResource(user, content);\n const { binaryContent: _binaryContent, ...publicContent } = content;\n return textToolResult(`Downloaded ${publicContent.title} as ${publicContent.fileName}.`, {\n ...publicContent,\n downloadId: persisted.id,\n });\n },\n );\n\n server.registerTool(\n 'prepare_dharma_share_text',\n {\n title: 'Prepare dharma sharing text',\n description:\n 'Prepare a text payload for the Fabushi app to confirm and add to global dharma sharing. This returns a clientAction; it does not start sending.',\n inputSchema: {\n title: z.string().min(1).max(120),\n text: z.string().min(1).max(20000),\n },\n outputSchema: {\n clientAction: z.object({\n type: z.literal('prepare_dharma_share_text'),\n title: z.string(),\n text: z.string(),\n }),\n },\n annotations: {\n readOnlyHint: false,\n destructiveHint: false,\n openWorldHint: false,\n },\n },\n async ({ title, text }) =>\n textToolResult(`Prepared \"${title}\" for user confirmation in the Fabushi app.`, {\n clientAction: {\n type: 'prepare_dharma_share_text',\n title: safeUserText(title).slice(0, 120),\n text: String(text || '').trim().slice(0, 20000),\n },\n }),\n );\n\n server.registerTool(\n 'prepare_practice_book_item',\n {\n title: 'Prepare practice book item',\n description:\n 'Prepare a scripture, chant, or practice item for the Fabushi app practice book. This returns a clientAction; it does not mutate the app by itself.',\n inputSchema: {\n title: z.string().min(1).max(120),\n sourceName: z.string().max(120).optional(),\n text: z.string().min(1).max(20000),\n },\n outputSchema: {\n clientAction: z.object({\n type: z.literal('prepare_practice_book_item'),\n title: z.string(),\n sourceName: z.string(),\n text: z.string(),\n }),\n },\n annotations: {\n readOnlyHint: false,\n destructiveHint: false,\n openWorldHint: false,\n },\n },\n async ({ title, sourceName, text }) =>\n textToolResult(`Prepared \"${title}\" for the Fabushi practice book confirmation flow.`, {\n clientAction: {\n type: 'prepare_practice_book_item',\n title: safeUserText(title).slice(0, 120),\n sourceName: safeUserText(sourceName || '大乘 AI').slice(0, 120),\n text: String(text || '').trim().slice(0, 20000),\n },\n }),\n );\n\n return server;\n}\n\nasync function handleMcpRequest(req, res) {\n const server = createLibreChatMcpServer();\n const transport = new StreamableHTTPServerTransport({\n sessionIdGenerator: undefined,\n enableJsonResponse: true,\n });\n\n res.on('close', () => {\n transport.close();\n server.close();\n });\n\n await server.connect(transport);\n await transport.handleRequest(req, res, req.body);\n}\n\napp.all('/mcp', async (req, res) => {\n try {\n await handleMcpRequest(req, res);\n } catch (error) {\n logger.error({ error: error.stack || String(error) }, 'MCP request failed');\n if (!res.headersSent) {\n res.status(500).json({\n error: 'MCP request failed',\n message: publicAiErrorMessage(error),\n });\n }\n }\n});\n\napp.post(\n '/api/resources/download',\n asyncHandler(async (req, res) => {\n const user = await resolveUser(req, req.body || {});\n const cbeta = parseCbetaResource(req.body || {});\n const internetArchive = parseInternetArchiveResource(req.body || {});\n const content = cbeta\n ? await downloadCbetaResource({ ...req.body, ...cbeta })\n : internetArchive\n ? await downloadInternetArchiveAudio({ ...req.body, ...internetArchive })\n : await downloadWebResource(req.body || {});\n const persisted = persistDownloadedResource(user, content);\n const { binaryContent: _binaryContent, ...publicContent } = content;\n jsonResponse(res, 200, {\n success: true,\n downloadId: persisted.id,\n ...publicContent,\n });\n }),\n);\n\napp.post(\n '/api/codex/resource-task',\n asyncHandler(async (req, res) => {\n if (process.env.ENABLE_CODEX_SDK !== 'true') {\n return jsonResponse(res, 403, {\n success: false,\n message: 'Codex SDK task runner is disabled. Set ENABLE_CODEX_SDK=true on the server to enable it.',\n });\n }\n const prompt = safeUserText(req.body?.prompt);\n if (!prompt) {\n return jsonResponse(res, 400, { success: false, message: 'prompt is required' });\n }\n const { Codex } = await import('@openai/codex-sdk');\n const codexRuntime = createCodexDeepSeekRuntime();\n const codex = new Codex(codexRuntime.enabled ? codexRuntime.options : {});\n const thread = codex.startThread(\n codexRuntime.enabled\n ? codexRuntime.threadOptions\n : { skipGitRepoCheck: true, networkAccessEnabled: true },\n );\n const result = await thread.run(prompt);\n jsonResponse(res, 200, {\n success: true,\n threadId: thread.id,\n provider: codexRuntime.provider,\n model: deepseekModel,\n result,\n });\n }),\n);\n\nregisterPlatformApi({ app, db, resolveUser, asyncHandler });\n\napp.use((error, _req, res, _next) => {\n const status = Number(error.statusCode || error.status || 500);\n const safeStatus = status >= 400 && status < 600 ? status : 500;\n logger.error({ error: error.stack || String(error) }, 'request failed');\n jsonResponse(res, safeStatus, {\n success: false,\n message: error.message || 'Internal Server Error',\n details: error.details,\n });\n});\n\nconst port = Number(process.env.PORT || 8788);\napp.listen(port, '0.0.0.0', () => {\n logger.info(\n {\n port,\n dbPath,\n dataDir,\n deepseekModel,\n cbetaApiRoot,\n },\n 'Dacheng AI backend started',\n );\n});\n"}, "0755befbf0a549c616d652b8040e523a851ada25": {"t": 1785757970.187446, "n": 145621, "h": "82dd590c4812a2d1", "c": "import crypto from 'node:crypto';\nimport fs from 'node:fs';\nimport path from 'node:path';\nimport process from 'node:process';\nimport { fileURLToPath } from 'node:url';\n\nimport { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';\nimport { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';\nimport Database from 'better-sqlite3';\nimport cors from 'cors';\nimport dotenv from 'dotenv';\nimport express from 'express';\nimport rateLimit from 'express-rate-limit';\nimport helmet from 'helmet';\nimport pino from 'pino';\nimport { z } from 'zod';\n\ndotenv.config();\n\nconst __dirname = path.dirname(fileURLToPath(import.meta.url));\nconst rootDir = path.resolve(__dirname, '..');\nconst dataDir = process.env.DATA_DIR || path.join(rootDir, 'data');\nconst resourcesDir = path.join(dataDir, 'resources');\nconst miniAppsDir = path.join(dataDir, 'miniapps');\nconst miniAppsStorePath = path.join(miniAppsDir, 'sandbox-miniapps.json');\nconst openClawRuntimeUpdatesDir =\n process.env.OPENCLAW_RUNTIME_UPDATES_DIR || path.join(dataDir, 'openclaw-runtime');\nconst openClawRuntimeEnableMacosUpdates =\n process.env.OPENCLAW_RUNTIME_ENABLE_MACOS_UPDATES === 'true';\nconst codexHomeDir = process.env.CODEX_HOME || path.join(dataDir, 'codex-home');\nconst codexTempDir = process.env.CODEX_TMPDIR || path.join(dataDir, 'codex-tmp');\nconst codexRuntimeDir = process.env.XDG_RUNTIME_DIR || path.join(dataDir, 'codex-runtime');\nconst dbPath = process.env.SQLITE_PATH || path.join(dataDir, 'dacheng-ai.sqlite');\n\nfs.mkdirSync(resourcesDir, { recursive: true });\nfs.mkdirSync(miniAppsDir, { recursive: true });\nfs.mkdirSync(openClawRuntimeUpdatesDir, { recursive: true });\nfs.mkdirSync(codexHomeDir, { recursive: true });\nfs.mkdirSync(codexTempDir, { recursive: true });\nfs.mkdirSync(codexRuntimeDir, { recursive: true });\n\nconst logger = pino({\n level: process.env.LOG_LEVEL || 'info',\n});\n\nconst app = express();\napp.disable('x-powered-by');\napp.set('trust proxy', Number(process.env.TRUST_PROXY_HOPS || 1));\napp.use(helmet());\nconst corsOrigin =\n !process.env.CORS_ORIGIN || process.env.CORS_ORIGIN.trim() === '*'\n ? true\n : process.env.CORS_ORIGIN.split(',').map((item) => item.trim()).filter(Boolean);\napp.use(cors({ origin: corsOrigin }));\napp.use(express.json({ limit: '1mb' }));\napp.use(\n rateLimit({\n windowMs: 60_000,\n limit: Number(process.env.RATE_LIMIT_PER_MINUTE || 90),\n standardHeaders: true,\n legacyHeaders: false,\n }),\n);\n\nconst db = new Database(dbPath);\ndb.pragma('journal_mode = WAL');\ndb.pragma('foreign_keys = ON');\n\ndb.exec(`\n CREATE TABLE IF NOT EXISTS conversations (\n id TEXT PRIMARY KEY,\n user_id TEXT NOT NULL,\n title TEXT NOT NULL,\n provider TEXT NOT NULL,\n model TEXT NOT NULL,\n created_at TEXT NOT NULL,\n updated_at TEXT NOT NULL\n );\n\n CREATE INDEX IF NOT EXISTS idx_conversations_user_updated\n ON conversations (user_id, updated_at DESC);\n\n CREATE TABLE IF NOT EXISTS messages (\n id TEXT PRIMARY KEY,\n conversation_id TEXT NOT NULL,\n role TEXT NOT NULL,\n content TEXT NOT NULL,\n prompt_tokens INTEGER NOT NULL DEFAULT 0,\n completion_tokens INTEGER NOT NULL DEFAULT 0,\n total_tokens INTEGER NOT NULL DEFAULT 0,\n created_at TEXT NOT NULL,\n FOREIGN KEY (conversation_id) REFERENCES conversations(id) ON DELETE CASCADE\n );\n\n CREATE INDEX IF NOT EXISTS idx_messages_conversation_created\n ON messages (conversation_id, created_at ASC);\n\n CREATE TABLE IF NOT EXISTS usage_months (\n user_id TEXT NOT NULL,\n month TEXT NOT NULL,\n total_tokens INTEGER NOT NULL DEFAULT 0,\n updated_at TEXT NOT NULL,\n PRIMARY KEY (user_id, month)\n );\n\n CREATE TABLE IF NOT EXISTS resource_downloads (\n id TEXT PRIMARY KEY,\n user_id TEXT NOT NULL,\n title TEXT NOT NULL,\n source_name TEXT NOT NULL,\n source_url TEXT NOT NULL,\n file_name TEXT NOT NULL,\n file_path TEXT NOT NULL,\n created_at TEXT NOT NULL\n );\n\n CREATE TABLE IF NOT EXISTS agent_runs (\n id TEXT PRIMARY KEY,\n user_id TEXT NOT NULL,\n conversation_id TEXT NOT NULL,\n message_id TEXT NOT NULL,\n openclaw_run_id TEXT,\n openclaw_session_key TEXT NOT NULL,\n status TEXT NOT NULL,\n mode TEXT NOT NULL,\n provider TEXT NOT NULL,\n model TEXT,\n input_tokens INTEGER NOT NULL DEFAULT 0,\n output_tokens INTEGER NOT NULL DEFAULT 0,\n tool_call_count INTEGER NOT NULL DEFAULT 0,\n cost_microusd INTEGER NOT NULL DEFAULT 0,\n started_at TEXT NOT NULL,\n completed_at TEXT,\n failed_at TEXT,\n error_code TEXT,\n error_message TEXT\n );\n\n CREATE INDEX IF NOT EXISTS idx_agent_runs_user_started\n ON agent_runs (user_id, started_at DESC);\n\n CREATE INDEX IF NOT EXISTS idx_agent_runs_status_started\n ON agent_runs (status, started_at DESC);\n\n CREATE TABLE IF NOT EXISTS agent_message_feedback (\n id TEXT PRIMARY KEY,\n message_id TEXT NOT NULL,\n user_id TEXT NOT NULL,\n rating TEXT NOT NULL,\n reason TEXT,\n comment TEXT,\n created_at TEXT NOT NULL\n );\n`);\n\nconst statements = {\n insertConversation: db.prepare(`\n INSERT INTO conversations (id, user_id, title, provider, model, created_at, updated_at)\n VALUES (@id, @userId, @title, @provider, @model, @createdAt, @updatedAt)\n `),\n updateConversation: db.prepare(`\n UPDATE conversations\n SET title = @title, updated_at = @updatedAt\n WHERE id = @id AND user_id = @userId\n `),\n getConversation: db.prepare(`\n SELECT * FROM conversations WHERE id = ? AND user_id = ? LIMIT 1\n `),\n listConversations: db.prepare(`\n SELECT id, title, updated_at AS updatedAt\n FROM conversations\n WHERE user_id = ?\n ORDER BY updated_at DESC\n LIMIT 30\n `),\n insertMessage: db.prepare(`\n INSERT INTO messages (\n id, conversation_id, role, content,\n prompt_tokens, completion_tokens, total_tokens, created_at\n )\n VALUES (\n @id, @conversationId, @role, @content,\n @promptTokens, @completionTokens, @totalTokens, @createdAt\n )\n `),\n listMessages: db.prepare(`\n SELECT role, content, prompt_tokens AS promptTokens,\n completion_tokens AS completionTokens, total_tokens AS totalTokens,\n created_at AS createdAt\n FROM messages\n WHERE conversation_id = ?\n ORDER BY created_at ASC\n `),\n getUsage: db.prepare(`\n SELECT total_tokens AS totalTokens FROM usage_months WHERE user_id = ? AND month = ?\n `),\n upsertUsage: db.prepare(`\n INSERT INTO usage_months (user_id, month, total_tokens, updated_at)\n VALUES (@userId, @month, @totalTokens, @updatedAt)\n ON CONFLICT(user_id, month)\n DO UPDATE SET total_tokens = usage_months.total_tokens + excluded.total_tokens,\n updated_at = excluded.updated_at\n `),\n insertResourceDownload: db.prepare(`\n INSERT INTO resource_downloads (\n id, user_id, title, source_name, source_url, file_name, file_path, created_at\n )\n VALUES (\n @id, @userId, @title, @sourceName, @sourceUrl, @fileName, @filePath, @createdAt\n )\n `),\n insertAgentRun: db.prepare(`\n INSERT INTO agent_runs (\n id, user_id, conversation_id, message_id, openclaw_run_id, openclaw_session_key,\n status, mode, provider, model, started_at\n )\n VALUES (\n @id, @userId, @conversationId, @messageId, @openClawRunId, @openClawSessionKey,\n @status, @mode, @provider, @model, @startedAt\n )\n `),\n getAgentRun: db.prepare(`\n SELECT * FROM agent_runs WHERE id = ? AND user_id = ? LIMIT 1\n `),\n updateAgentRunStatus: db.prepare(`\n UPDATE agent_runs\n SET status = @status,\n openclaw_run_id = COALESCE(@openClawRunId, openclaw_run_id),\n input_tokens = COALESCE(@inputTokens, input_tokens),\n output_tokens = COALESCE(@outputTokens, output_tokens),\n tool_call_count = COALESCE(@toolCallCount, tool_call_count),\n completed_at = @completedAt,\n failed_at = @failedAt,\n error_code = @errorCode,\n error_message = @errorMessage\n WHERE id = @id AND user_id = @userId\n `),\n insertAgentFeedback: db.prepare(`\n INSERT INTO agent_message_feedback (id, message_id, user_id, rating, reason, comment, created_at)\n VALUES (@id, @messageId, @userId, @rating, @reason, @comment, @createdAt)\n `),\n};\n\nconst deepseekApiKey = process.env.DEEPSEEK_API_KEY || '';\nconst deepseekBaseUrl = (process.env.DEEPSEEK_BASE_URL || 'https://api.deepseek.com').replace(/\\/+$/, '');\nconst deepseekModel = process.env.DEEPSEEK_MODEL || 'deepseek-chat';\nconst deepseekAvailableModels = Array.from(\n new Set(\n (process.env.DEEPSEEK_MODELS || `${deepseekModel},deepseek-chat,deepseek-reasoner`)\n .split(',')\n .map((item) => normalizeDeepSeekModelName(item))\n .filter(Boolean),\n ),\n);\nconst cbetaApiRoot = (process.env.CBETA_API_ROOT || 'https://api.ombhrum.com/api/cbeta').replace(/\\/+$/, '');\nconst fabushiApiBaseUrl = (process.env.FABUSHI_API_BASE_URL || 'https://api.ombhrum.com').replace(/\\/+$/, '');\nconst memberMonthlyLimit = Number(process.env.MEMBER_MONTHLY_TOKEN_LIMIT || 1_000_000);\nconst freeMonthlyLimit = Number(process.env.FREE_MONTHLY_TOKEN_LIMIT || 1_000);\nconst requireMemberForAi = process.env.REQUIRE_MEMBER_FOR_AI === 'true';\nconst deepseekMaxCompletionTokens = Math.max(1, Number(process.env.DEEPSEEK_MAX_COMPLETION_TOKENS || 1400));\nconst maxResourceTextChars = Number(process.env.MAX_RESOURCE_TEXT_CHARS || 80_000);\nconst openClawRuntimeManifestPath =\n process.env.OPENCLAW_RUNTIME_MANIFEST_PATH || path.join(openClawRuntimeUpdatesDir, 'manifest.json');\nconst openClawRuntimePublicBaseUrl = (process.env.OPENCLAW_RUNTIME_PUBLIC_BASE_URL || '').replace(/\\/+$/, '');\nconst enableLibreChatAgentChat = process.env.ENABLE_LIBRECHAT_AGENT_CHAT === 'true';\nconst libreChatAgentsBaseUrl = (\n process.env.LIBRECHAT_AGENTS_BASE_URL || 'http://127.0.0.1:3080/api/agents/v1'\n).replace(/\\/+$/, '');\nconst libreChatAgentApiKey = process.env.LIBRECHAT_AGENT_API_KEY || '';\nconst libreChatAgentId = process.env.LIBRECHAT_AGENT_ID || '';\nconst enableCodexSdkChat = process.env.ENABLE_CODEX_SDK_CHAT === 'true';\nconst enableOpenClawAgentChat = process.env.ENABLE_OPENCLAW_AGENT_CHAT === 'true';\nconst openClawGatewayUrl = (process.env.OPENCLAW_GATEWAY_URL || 'http://127.0.0.1:18789').replace(/\\/+$/, '');\nconst openClawGatewayToken = process.env.OPENCLAW_GATEWAY_TOKEN || '';\nconst openClawAgentId = process.env.OPENCLAW_AGENT_ID || 'fabushi-public-agent';\nconst openClawRunsEndpoint = process.env.OPENCLAW_RUNS_ENDPOINT || '';\nconst codexDeepSeekProviderId = 'deepseek-chat-completions';\nconst codexResponsesBaseUrl = (\n process.env.CODEX_DEEPSEEK_RESPONSES_BASE_URL ||\n `http://127.0.0.1:${process.env.PORT || 8788}/codex-deepseek/v1`\n).replace(/\\/+$/, '');\n\nconst systemPrompt = [\n '你是“大乘”App 的 AI 助手。',\n '你的核心任务是帮助用户查找、整理、理解并全球法布施合法可分享的佛法资源。',\n '回答要庄重、简洁、可执行;涉及经典、仪轨或资源时,提醒用户尊重版权、来源和当地法规。',\n '如果用户想找资源、下载经文、寻找音频或准备可分享资料,你必须先在后端自动执行搜索、验证和下载步骤;不要要求用户去前端 + 菜单手动查找。',\n '你可以把执行进度总结给用户,但不要输出隐藏推理原文。',\n].join('\\n');\n\nfunction nowIso() {\n return new Date().toISOString();\n}\n\nfunction monthKey(date = new Date()) {\n return `${date.getUTCFullYear()}-${String(date.getUTCMonth() + 1).padStart(2, '0')}`;\n}\n\nfunction jsonResponse(res, status, payload) {\n res.status(status).json(payload);\n}\n\nasync function readJsonFile(filePath) {\n const raw = await fs.promises.readFile(filePath, 'utf8');\n return JSON.parse(raw);\n}\n\nfunction requestOrigin(req) {\n if (openClawRuntimePublicBaseUrl) return openClawRuntimePublicBaseUrl;\n const proto = req.get('X-Forwarded-Proto') || req.protocol || 'https';\n const host = req.get('X-Forwarded-Host') || req.get('Host');\n return host ? `${proto}://${host}` : '';\n}\n\nfunction absolutizeOpenClawRuntimeUrl(req, value) {\n const text = readText(value);\n if (!text) return '';\n if (/^https?:\\/\\//i.test(text)) return text;\n const normalized = text.startsWith('/')\n ? text\n : `/api/openclaw/runtime/files/${text.replace(/^\\/+/, '')}`;\n const origin = requestOrigin(req);\n return origin ? new URL(normalized, `${origin}/`).toString() : normalized;\n}\n\nfunction textToolResult(text, structuredContent = {}) {\n return {\n structuredContent,\n content: [{ type: 'text', text }],\n };\n}\n\nfunction sseHeaders(res) {\n res.status(200);\n res.setHeader('Content-Type', 'text/event-stream; charset=utf-8');\n res.setHeader('Cache-Control', 'no-cache, no-transform');\n res.setHeader('Connection', 'keep-alive');\n res.setHeader('X-Accel-Buffering', 'no');\n if (typeof res.flushHeaders === 'function') res.flushHeaders();\n}\n\nfunction writeSse(res, event, payload) {\n res.write(`event: ${event}\\n`);\n res.write(`data: ${JSON.stringify(payload)}\\n\\n`);\n}\n\nfunction asyncHandler(fn) {\n return (req, res, next) => {\n Promise.resolve(fn(req, res, next)).catch(next);\n };\n}\n\nfunction bearerToken(req) {\n const delegated = readText(req.get('x-dacheng-auth-token'));\n if (\n delegated &&\n delegated !== 'DACHENG_AUTH_TOKEN' &&\n !delegated.startsWith('secretref-env:') &&\n delegated !== 'dacheng-openclaw-proxy'\n ) {\n return delegated;\n }\n const header = req.get('Authorization') || '';\n if (!header.startsWith('Bearer ')) return '';\n const token = header.slice('Bearer '.length).trim();\n return token === 'dacheng-openclaw-proxy' ? '' : token;\n}\n\nfunction sha256(value) {\n return crypto.createHash('sha256').update(value).digest('hex');\n}\n\nfunction safeUserText(value, fallback = '') {\n if (typeof value !== 'string') return fallback;\n return value.replace(/\\u0000/g, '').trim();\n}\n\nfunction estimateTokens(text) {\n const normalized = String(text || '').trim();\n if (!normalized) return 0;\n return Math.max(1, Math.ceil(normalized.length / 3));\n}\n\nfunction readText(value) {\n if (value === undefined || value === null) return '';\n return String(value).replace(/\\u0000/g, '').trim();\n}\n\nconst miniAppHostApiVersion = '1.5';\nconst miniAppHighRiskPermissions = new Set([\n 'auth.token',\n 'payments.alipay',\n 'payments.fudeGold',\n 'desktop.control',\n 'network.udp',\n 'network.interfaces',\n 'system.keepAwake',\n 'hotspot.settings',\n 'local.loopback',\n 'fs.readWrite',\n 'shell.execute',\n 'browser.external',\n 'files.write',\n 'projects.write',\n 'openclaw.restart',\n]);\n\nfunction officialMiniAppRegistry(req) {\n const webBase = process.env.MINIAPP_WEB_BASE_URL || 'https://fabushi.ombhrum.com';\n const official = [\n {\n botId: 'bot.global-dharma',\n miniAppId: 'official.global-dharma',\n title: '全球法布施',\n subtitle: '地区、循环、场能都在对话框上方设置',\n initials: '法',\n icon: 'public',\n avatarColor: '#4CAF7A',\n kind: 'global_dharma',\n entryUrl: process.env.GLOBAL_DHARMA_MINIAPP_URL,\n greeting: '把要分享的文字、链接或素材发给我,我会按上方选择的地区与模式启动全球法布施。',\n inputHint: '输入要法布施的文字/链接,或点 + 添加素材',\n permissions: [\n 'app.context',\n 'bot.chat',\n 'auth.session',\n 'wallet.balance',\n 'payments.entitlement',\n 'payments.fudeGold',\n 'payments.alipay',\n 'files.pick',\n 'network.udp',\n 'network.interfaces',\n 'system.keepAwake',\n 'hotspot.settings',\n 'openclaw.status',\n 'openclaw.chat',\n 'desktop.control',\n ],\n },\n {\n botId: 'bot.flashcards',\n miniAppId: 'official.flashcards',\n title: '背诵闪卡制作',\n subtitle: '随机挖空 / AI 制卡从顶部模式按钮选择',\n initials: '卡',\n icon: 'flashcards',\n avatarColor: '#7E57C2',\n kind: 'flashcards',\n entryUrl: process.env.FLASHCARDS_MINIAPP_URL,\n greeting: '粘贴经文、文章正文或链接即可制作背诵闪卡。制卡模式请在上方按钮切换。',\n inputHint: '粘贴链接或正文,发送后制作闪卡',\n permissions: ['app.context', 'bot.chat', 'flashcards.create', 'openclaw.status', 'openclaw.chat'],\n },\n {\n botId: 'bot.platform-publish',\n miniAppId: 'official.platform-publish',\n title: '法布施到平台',\n subtitle: '选择平台后生成发布草稿并打开入口',\n initials: '发',\n icon: 'publish',\n avatarColor: '#FF9F43',\n kind: 'platform_publish',\n entryUrl: process.env.PLATFORM_PUBLISH_MINIAPP_URL,\n greeting: '把要发布的正文或链接发给我,上方选择平台后,我会生成发布草稿并打开对应平台入口。',\n inputHint: '输入要发布到平台的正文/链接',\n permissions: [\n 'app.context',\n 'bot.chat',\n 'platform.publish',\n 'files.pick',\n 'fs.readWrite',\n 'shell.execute',\n 'browser.external',\n ],\n },\n {\n botId: 'bot.father',\n miniAppId: 'official.bot-father',\n title: '机器人之父',\n subtitle: '用对话生成个人沙箱小程序',\n initials: '父',\n icon: 'bot_father',\n avatarColor: '#3D8BFF',\n kind: 'bot_father',\n entryUrl: process.env.BOT_FATHER_MINIAPP_URL,\n greeting: '告诉我你想要的小程序,我会生成 manifest、界面和权限声明,并放进你的个人沙箱。',\n inputHint: '描述你想创建的小程序',\n permissions: ['app.context', 'bot.chat', 'miniapps.dev'],\n },\n ];\n return {\n bots: official.map((bot, index) => ({\n ...bot,\n destinationIndex: index,\n source: 'official',\n })),\n miniApps: official.map((bot) => ({\n miniAppId: bot.miniAppId,\n botId: bot.botId,\n title: bot.title,\n subtitle: bot.subtitle,\n entryUrl:\n bot.entryUrl ||\n `${webBase.replace(/\\/+$/, '')}/miniapps/${encodeURIComponent(bot.miniAppId)}`,\n version: '1.5.0',\n permissions: bot.permissions,\n surfaces: ['homePinned', 'chatPanel'],\n theme: 'telegramDark',\n signature: 'official',\n reviewStatus: 'trusted',\n source: 'official',\n })),\n };\n}\n\nasync function readMiniAppStore() {\n try {\n const raw = await fs.promises.readFile(miniAppsStorePath, 'utf8');\n const decoded = JSON.parse(raw);\n return {\n apps: Array.isArray(decoded.apps) ? decoded.apps : [],\n };\n } catch (error) {\n if (error?.code === 'ENOENT') return { apps: [] };\n throw error;\n }\n}\n\nasync function writeMiniAppStore(store) {\n await fs.promises.mkdir(miniAppsDir, { recursive: true });\n await fs.promises.writeFile(\n miniAppsStorePath,\n JSON.stringify({ apps: store.apps || [] }, null, 2),\n );\n}\n\nfunction miniAppEntryUrl(req, miniAppId) {\n const origin = requestOrigin(req) || '';\n const pathName = `/api/miniapps/dev/${encodeURIComponent(miniAppId)}/index.html`;\n return origin ? new URL(pathName, `${origin}/`).toString() : pathName;\n}\n\nfunction sanitizeMiniAppPermissions(value, { allowHighRisk = false } = {}) {\n const raw = Array.isArray(value) ? value : [];\n const normalized = raw\n .map((item) => readText(item).toLowerCase())\n .filter((item) => /^[a-z][a-z0-9_.:-]{0,64}$/.test(item));\n const deduped = [...new Set(['app.context', 'bot.chat', ...normalized])];\n return deduped.filter((permission) => allowHighRisk || !miniAppHighRiskPermissions.has(permission));\n}\n\nfunction miniAppScan(permissions, sourceHtml = '') {\n const highRisk = permissions.filter((permission) => miniAppHighRiskPermissions.has(permission));\n const blockedPatterns = [\n /authorization\\s*:\\s*['\"`]bearer/i,\n /desktop_control_bridge_token/i,\n /openclaw_gateway_token/i,\n /eval\\s*\\(/i,\n ];\n const blocked = blockedPatterns\n .filter((pattern) => pattern.test(sourceHtml))\n .map((pattern) => pattern.toString());\n return {\n passed: blocked.length === 0,\n highRiskPermissions: highRisk,\n blockedPatterns: blocked,\n };\n}\n\nfunction inferMiniAppPermissionsFromText(...values) {\n const text = values.map((value) => readText(value)).join('\\n').toLowerCase();\n const rules = [\n ['auth.session', [/auth\\.(getsession|requirelogin)/, /登录|用户信息|宿主账号/]],\n ['auth.token', [/auth\\.getaccesstoken/, /access token|访问 token/]],\n ['payments.alipay', [/payments\\.alipay/, /支付宝|支付|购买|订单/]],\n ['payments.entitlement', [/payments\\.checkentitlement|权益|解锁状态|购买状态/]],\n ['payments.fudeGold', [/payments\\.requestpayment|福德金|代币|钱包|余额/]],\n ['wallet.balance', [/wallet\\.getbalance|福德金|代币|钱包|余额/]],\n ['network.udp', [/network\\.udp|udp|datagram/, /全球法布施|法布施发送|数据报/]],\n ['network.interfaces', [/network\\.interfaces|网卡|网络接口|广播地址/]],\n ['system.keepAwake', [/system\\.keepawake|保持唤醒|后台任务|keep awake/]],\n ['hotspot.settings', [/hotspot\\.opensettings|热点|本地场能/]],\n ['local.loopback', [/localloopback\\./, /localhost|127\\.0\\.0\\.1|本地回环|本地转经轮/]],\n ['flashcards.create', [/flashcards\\./, /闪卡|背诵卡|挖空/]],\n ['platform.publish', [/platformpublish\\./, /发布草稿|发布到平台|公众号|小红书|知乎/]],\n ['files.pick', [/files\\.pick/, /选择文件|上传文件|本地素材/]],\n ['fs.readWrite', [/fs\\.(writefile|readfile)/, /本地文件|写文件|读文件|文件处理/]],\n ['shell.execute', [/shell\\.execute/, /终端|命令行|shell|cli|playwright/]],\n ['browser.external', [/browser\\.open/, /浏览器|打开网页|打开平台/]],\n ['desktop.control', [/desktopcontrol\\./, /桌面控制|点击|键盘|鼠标/]],\n ['openclaw.chat', [/openclaw\\.chat/, /openclaw/]],\n ];\n return rules\n .filter(([, patterns]) => patterns.some((pattern) => pattern.test(text)))\n .map(([permission]) => permission);\n}\n\nfunction miniAppRecordToBot(record) {\n return {\n botId: record.botId,\n miniAppId: record.miniAppId,\n title: record.title,\n subtitle: record.subtitle,\n initials: record.initials || '小',\n icon: record.icon || 'code',\n avatarColor: record.avatarColor || '#3D8BFF',\n kind: 'third_party',\n greeting: record.greeting || '你好,我是这个小程序的机器人。',\n inputHint: record.inputHint || '输入消息,或打开小程序面板',\n permissions: record.permissions || ['app.context', 'bot.chat'],\n source: record.source || 'sandbox',\n };\n}\n\nfunction miniAppRecordToManifest(req, record) {\n return {\n miniAppId: record.miniAppId,\n botId: record.botId,\n title: record.title,\n subtitle: record.subtitle,\n entryUrl: miniAppEntryUrl(req, record.miniAppId),\n version: record.version || '0.0.1',\n permissions: record.permissions || ['app.context', 'bot.chat'],\n surfaces: record.surfaces || ['chatPanel'],\n theme: record.theme || 'telegramDark',\n signature: record.signature || `sandbox:${record.ownerId}`,\n reviewStatus: record.reviewStatus || 'sandbox',\n source: record.source || 'sandbox',\n };\n}\n\nfunction safeMiniAppId(value) {\n return readText(value).replace(/[^a-zA-Z0-9_.-]/g, '-').slice(0, 80);\n}\n\nfunction titleFromPromptForMiniApp(prompt) {\n const text = readText(prompt).replace(/\\s+/g, ' ');\n if (!text) return '个人小程序';\n return text.length <= 18 ? text : text.slice(0, 18);\n}\n\nfunction escapeHtml(value) {\n return String(value || '')\n .replace(/&/g, '&')\n .replace(/</g, '<')\n .replace(/>/g, '>')\n .replace(/\"/g, '"')\n .replace(/'/g, ''');\n}\n\nfunction generatedMiniAppHtml({ title, prompt }) {\n const safeTitle = escapeHtml(title);\n const safePrompt = escapeHtml(prompt);\n const promptJson = JSON.stringify(readText(prompt));\n return `<!doctype html>\n<html lang=\"zh-Hans\">\n<head>\n <meta charset=\"utf-8\" />\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n <title>${safeTitle}</title>\n <style>\n body { margin:0; font-family:-apple-system,BlinkMacSystemFont,\"Segoe UI\",sans-serif; background:#0f1722; color:#fff; }\n main { padding:20px; display:grid; gap:14px; }\n .card { border:1px solid #263445; background:#17212b; border-radius:16px; padding:16px; }\n button { border:0; border-radius:999px; padding:10px 14px; background:#3390ec; color:white; font-weight:800; }\n pre { white-space:pre-wrap; color:#9ec7ff; }\n </style>\n</head>\n<body>\n <main>\n <section class=\"card\">\n <h1>${safeTitle}</h1>\n <p>这是机器人之父根据你的描述生成的个人沙箱小程序。</p>\n <pre>${safePrompt}</pre>\n </section>\n <section class=\"card\">\n <button id=\"context\">读取宿主上下文</button>\n <button id=\"spec\">读取宿主能力</button>\n <button id=\"chat\">让机器人解释这个小程序</button>\n <pre id=\"output\">等待操作...</pre>\n </section>\n </main>\n <script>\n const out = document.getElementById('output');\n async function invoke(method, params) {\n if (!window.FabushiMiniApp) throw new Error('FabushiMiniApp host SDK not ready');\n const result = await window.FabushiMiniApp.invoke(method, params || {});\n out.textContent = JSON.stringify(result, null, 2);\n }\n document.getElementById('context').onclick = () => invoke('app.getContext');\n document.getElementById('spec').onclick = () => invoke('app.getHostApiSpec');\n const prompt = ${promptJson};\n document.getElementById('chat').onclick = () => invoke('bot.sendMessage', {\n message: '请简要说明这个小程序能做什么:' + prompt\n });\n const seenCommands = new Set();\n function deliverMiniAppCommand(detail) {\n if (!detail || typeof detail !== 'object') return;\n const commandKey = detail.commandId || detail.id || [detail.createdAt, detail.command, detail.rawText || detail.text].filter(Boolean).join(':');\n if (commandKey) {\n if (seenCommands.has(commandKey)) return;\n seenCommands.add(commandKey);\n }\n handleMiniAppCommand(detail);\n }\n async function handleMiniAppCommand(detail) {\n out.textContent = '收到后台命令:' + JSON.stringify(detail, null, 2);\n try {\n await invoke('bot.postMessage', {\n commandId: detail.commandId || detail.id,\n message: '「${safeTitle}」已收到后台命令:' + (detail.args || detail.text || '')\n });\n await invoke('bot.reportCommandResult', {\n commandId: detail.commandId || detail.id,\n status: 'completed',\n message: '「${safeTitle}」后台命令已处理。'\n });\n } catch (error) {\n out.textContent = String(error && error.message || error);\n }\n }\n window.addEventListener('fabushi-miniapp-command', (event) => {\n deliverMiniAppCommand(event.detail || {});\n });\n if (window.__fabushiLastMiniAppCommand) {\n (window.queueMicrotask || ((fn) => setTimeout(fn, 0)))(() => {\n deliverMiniAppCommand(window.__fabushiLastMiniAppCommand);\n });\n }\n </script>\n</body>\n</html>`;\n}\n\nfunction extractMiniAppHtml(value) {\n const text = readText(value);\n if (!text) return '';\n const fenced = text.match(/```(?:html)?\\s*([\\s\\S]*?)```/i);\n const candidate = (fenced ? fenced[1] : text).trim();\n if (!/<html[\\s>]/i.test(candidate) || !/<script[\\s>]/i.test(candidate)) return '';\n return candidate.startsWith('<!doctype') || candidate.startsWith('<html')\n ? candidate\n : `<!doctype html>\\n${candidate}`;\n}\n\nasync function generateBotFatherMiniAppHtml({ title, prompt, user }) {\n const fallback = generatedMiniAppHtml({ title, prompt });\n try {\n const result = await runAgentModel({\n user,\n mode: 'miniapp_generation',\n maxCompletionTokens: deepseekMaxCompletionTokens,\n messages: [\n {\n role: 'system',\n content: [\n '你是 Fabushi App 的小程序生成器。',\n '只输出一个完整静态 HTML 文件,不要 Markdown,不要解释。',\n '小程序只能通过 window.FabushiMiniApp.invoke(method, params) 调用宿主能力。',\n '先调用 app.getHostApiSpec 或 app.getCapabilities 判断宿主版本和已授权权限,再显示对应 UI。',\n '聊天后台调用协议:优先使用 window.FabushiMiniApp.bot.onCommand(\"/start\", handler) 或 bot.onAnyCommand(handler);如果手写监听 window 的 fabushi-miniapp-command 事件,注册后也要读取 window.__fabushiLastMiniAppCommand 并按 commandId 去重补处理。detail.command 默认为 /start,detail.args 是用户文本;处理完成后调用 bot.postMessage 或 bot.reportCommandResult 把进度/结果写回聊天框。',\n '标准能力:auth.getSession/auth.requireLogin 复用宿主登录;payments.requestPayment 使用福德金支付并由宿主登记权益;payments.checkEntitlement 查询商品是否已解锁;payments.alipay.pay/createOrder 仅作为宿主官方现金支付兜底;network.udp/network.interfaces 提供 UDP 和网卡原语;system.keepAwake/hotspot.openSettings 提供系统级辅助;flashcards.createDeck 接入背诵闪卡;platformPublish.createDraft 接入发布草稿;fs.writeFile/fs.readFile、shell.execute、browser.open 用于本地文件处理、终端和浏览器。',\n '高危权限包括 auth.token、payments.alipay、payments.fudeGold、network.udp、network.interfaces、system.keepAwake、hotspot.settings、local.loopback、fs.readWrite、shell.execute、browser.external、desktop.control;只有用户需求明确需要时才使用。',\n '禁止外链脚本、eval、Authorization token、桌面桥 token、OpenClaw token。',\n '不要调用 auth.getAccessToken,除非用户明确要求对接需要 token 的受信服务。',\n '优先实现一个可点击、可立即使用的小面板,包含 app.getContext 或 bot.sendMessage 示例。',\n ].join('\\n'),\n },\n {\n role: 'user',\n content: [\n `小程序标题:${title}`,\n `用户需求:${prompt}`,\n '请生成 HTML/CSS/JS 单文件。',\n ].join('\\n'),\n },\n ],\n });\n const sourceHtml = extractMiniAppHtml(result.message);\n if (!sourceHtml) {\n return {\n sourceHtml: fallback,\n generation: { provider: 'template', fallbackReason: 'ai_output_not_html' },\n };\n }\n return {\n sourceHtml,\n generation: {\n provider: result.provider || 'ai',\n model: result.model || '',\n },\n };\n } catch (error) {\n return {\n sourceHtml: fallback,\n generation: {\n provider: 'template',\n fallbackReason: readText(error?.message || error),\n },\n };\n }\n}\n\nfunction clientContextMessage(client) {\n if (!client || typeof client !== 'object') return '';\n const selectedModel = readText(client.selectedModel || client.model);\n const project = client.project && typeof client.project === 'object' ? client.project : {};\n const projectName = readText(project.name);\n const projectPath = readText(project.path);\n const lines = ['桌面端上下文:'];\n if (selectedModel) lines.push(`用户选择的 DeepSeek 模型:${normalizeDeepSeekModelName(selectedModel)}`);\n if (projectPath) {\n lines.push(`当前项目名称:${projectName || projectPath}`);\n lines.push(`当前项目路径:${projectPath}`);\n lines.push('如果本轮任务涉及读取、修改、运行、分析或整理项目,请优先围绕当前项目路径工作;不要擅自操作其他目录。');\n }\n return lines.length > 1 ? lines.join('\\n') : '';\n}\nfunction unwrapPayload(payload) {\n if (!payload || typeof payload !== 'object') return {};\n return payload.data && typeof payload.data === 'object' ? payload.data : payload;\n}\n\nfunction readFirstText(values) {\n for (const value of values) {\n const text = readText(value);\n if (text) return text;\n }\n return '';\n}\n\nfunction activeFlag(value) {\n if (value === true) return true;\n if (value === 1) return true;\n if (typeof value !== 'string') return false;\n return ['true', '1', 'yes', 'active'].includes(value.trim().toLowerCase());\n}\n\nfunction futureDate(value) {\n const text = readText(value);\n if (!text) return false;\n const timestamp = Date.parse(text);\n return Number.isFinite(timestamp) && timestamp > Date.now();\n}\n\nfunction normalizeMembershipPayload(payload) {\n if (!payload || typeof payload !== 'object') return false;\n const data = unwrapPayload(payload);\n const membership = data.membership && typeof data.membership === 'object' ? data.membership : data;\n const type = readText(membership.type || membership.membershipType || data.membershipType).toLowerCase();\n const status = readText(membership.status || data.status).toLowerCase();\n const expiry = membership.expiresAt ||\n membership.expires_at ||\n membership.membershipExpiry ||\n membership.membership_expires_at ||\n data.membershipExpiry ||\n data.membership_expires_at;\n const hasExplicitActiveFlag =\n membership.isActive !== undefined ||\n membership.active !== undefined ||\n membership.hasPremiumAccess !== undefined ||\n membership.hasPremiumMembership !== undefined ||\n membership.isPremium !== undefined;\n\n if (hasExplicitActiveFlag) {\n return Boolean(\n activeFlag(membership.isActive) ||\n activeFlag(membership.active) ||\n activeFlag(membership.hasPremiumAccess) ||\n activeFlag(membership.hasPremiumMembership) ||\n activeFlag(membership.isPremium),\n );\n }\n\n return Boolean(\n status === 'active' ||\n type === 'paid' ||\n type === 'premium' ||\n type === 'trial' ||\n futureDate(expiry),\n );\n}\n\nfunction extractRemoteAccount(payload) {\n const data = unwrapPayload(payload);\n const nestedUser = data.user && typeof data.user === 'object' ? data.user : {};\n const id = readFirstText([\n data.userId,\n data.user_id,\n data.userNo,\n data.user_no,\n data.id,\n nestedUser.userId,\n nestedUser.user_id,\n nestedUser.userNo,\n nestedUser.user_no,\n nestedUser.id,\n ]);\n const username = readFirstText([data.username, data.name, nestedUser.username, nestedUser.name]);\n const email = readFirstText([data.email, nestedUser.email]);\n const membership = data.membership && typeof data.membership === 'object' ? data.membership : null;\n return {\n authenticated: Boolean(id || username || email || membership || data.success === true),\n id,\n username,\n email,\n isMember: normalizeMembershipPayload(payload),\n membership,\n };\n}\n\nasync function resolveRemoteAccount(token) {\n const fallback = {\n authenticated: false,\n id: '',\n username: '',\n email: '',\n isMember: false,\n membership: null,\n };\n if (!token || !fabushiApiBaseUrl) return fallback;\n\n const endpoints = [\n '/api/stripe/membership-status',\n '/api/alipay/check-membership',\n '/api/auth/user-info',\n ];\n\n for (const endpoint of endpoints) {\n try {\n const response = await fetch(`${fabushiApiBaseUrl}${endpoint}`, {\n headers: {\n Accept: 'application/json',\n Authorization: `Bearer ${token}`,\n },\n signal: AbortSignal.timeout(8_000),\n });\n if (response.status === 401 || response.status === 403) return fallback;\n if (!response.ok) continue;\n const payload = await response.json();\n const account = extractRemoteAccount(payload);\n if (account.authenticated || account.isMember) {\n return {\n ...fallback,\n ...account,\n authenticated: true,\n };\n }\n } catch (error) {\n logger.debug({ endpoint, error: String(error) }, 'membership lookup skipped');\n }\n }\n\n return fallback;\n}\n\nasync function resolveUser(req, body = {}) {\n const token = bearerToken(req);\n const usernameHint = safeUserText(body.username || req.query.username);\n\n const testToken = process.env.TEST_ACCOUNT_TOKEN || 'dacheng-test-token-bypass-limits';\n if (token && token === testToken) {\n return {\n userId: 'user:test_account',\n username: 'TestAccount',\n tokenHash: sha256(token).slice(0, 24),\n isAuthenticated: true,\n isMember: true,\n isTestAccount: true,\n membership: { type: 'lifetime', active: true },\n };\n }\n\n const tokenHash = token ? sha256(token).slice(0, 24) : '';\n const anonymousHash = sha256(`${req.ip}|${req.get('User-Agent') || ''}`).slice(0, 24);\n const remoteAccount = await resolveRemoteAccount(token);\n const remoteIdentity = remoteAccount.id ||\n remoteAccount.username ||\n remoteAccount.email ||\n (remoteAccount.authenticated ? tokenHash : '');\n const userId = remoteAccount.authenticated\n ? `user:${remoteIdentity}`\n : tokenHash\n ? `token:${tokenHash}`\n : `anon:${anonymousHash}`;\n return {\n userId,\n username: remoteAccount.username || usernameHint,\n tokenHash,\n isAuthenticated: remoteAccount.authenticated,\n isMember: remoteAccount.isMember,\n membership: remoteAccount.membership,\n };\n}\n\nfunction usageFor(userId) {\n const month = monthKey();\n const row = statements.getUsage.get(userId, month);\n return {\n month,\n used: Number(row?.totalTokens || 0),\n };\n}\n\nfunction enforceTokenBudget(user, estimatedTokensForRequest) {\n if (user && user.isTestAccount) {\n return { limit: 999999999, usage: { month: monthKey(), used: 0 } };\n }\n if (requireMemberForAi && !user.isMember) {\n const error = new Error('大乘 AI 需要开通会员后使用');\n error.statusCode = 403;\n error.code = 'AI_MEMBERSHIP_REQUIRED';\n error.details = {\n requireMemberForAi,\n monthlyLimit: memberMonthlyLimit,\n remainingTokens: 0,\n };\n throw error;\n }\n\n const limit = user.isMember ? memberMonthlyLimit : freeMonthlyLimit;\n const usage = usageFor(user.userId);\n if (usage.used + estimatedTokensForRequest > limit) {\n const error = new Error('本月 AI token 额度已不足');\n error.statusCode = 429;\n error.code = 'USER_TOKEN_QUOTA_EXCEEDED';\n error.details = {\n monthlyLimit: limit,\n usedTokens: usage.used,\n remainingTokens: Math.max(0, limit - usage.used),\n };\n throw error;\n }\n return { limit, usage };\n}\n\nfunction completionBudgetFor(budget, estimatedPromptTokens) {\n const remainingAfterPrompt = budget.limit - budget.usage.used - estimatedPromptTokens;\n return Math.max(1, Math.min(deepseekMaxCompletionTokens, Math.floor(remainingAfterPrompt)));\n}\n\nfunction recordUsage(userId, totalTokens) {\n if (!totalTokens || totalTokens < 1) return;\n statements.upsertUsage.run({\n userId,\n month: monthKey(),\n totalTokens,\n updatedAt: nowIso(),\n });\n}\n\nfunction titleFromMessage(message) {\n const compact = message.replace(/\\s+/g, ' ').trim();\n if (!compact) return '新对话';\n return compact.length > 22 ? `${compact.slice(0, 22)}...` : compact;\n}\n\nfunction normalizeMessages(rows) {\n return rows.map((row) => ({\n role: row.role,\n content: row.content,\n }));\n}\n\nfunction normalizeMaxCompletionTokens(value) {\n const numeric = Number(value || deepseekMaxCompletionTokens);\n if (!Number.isFinite(numeric) || numeric < 1) return deepseekMaxCompletionTokens;\n return Math.max(1, Math.min(deepseekMaxCompletionTokens, Math.floor(numeric)));\n}\n\nfunction normalizeDeepSeekModelName(value) {\n const raw = readText(value || deepseekModel);\n if (!raw) return deepseekModel;\n const modelId = raw.includes('/') ? raw.split('/').pop() : raw;\n return modelId || deepseekModel;\n}\n\nasync function callDeepSeek(messages, options = {}) {\n if (!deepseekApiKey) {\n const error = new Error('DeepSeek API key is not configured');\n error.statusCode = 503;\n throw error;\n }\n\n const model = normalizeDeepSeekModelName(options.model);\n const response = await fetch(`${deepseekBaseUrl}/chat/completions`, {\n method: 'POST',\n headers: {\n Accept: 'application/json',\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${deepseekApiKey}`,\n },\n body: JSON.stringify({\n model,\n messages,\n temperature: 0.4,\n max_tokens: normalizeMaxCompletionTokens(options.maxCompletionTokens),\n stream: false,\n }),\n signal: AbortSignal.timeout(60_000),\n });\n\n const bodyText = await response.text();\n let payload;\n try {\n payload = bodyText ? JSON.parse(bodyText) : {};\n } catch {\n payload = { error: bodyText };\n }\n\n if (!response.ok) {\n const upstreamMessage = payload?.error?.message || payload?.message || '';\n const friendlyMessage =\n response.status === 402 || /insufficient\\s+balance/i.test(upstreamMessage)\n ? '大乘 AI 服务额度暂不可用,请联系管理员充值 DeepSeek 账户后重试。'\n : upstreamMessage || `DeepSeek request failed: ${response.status}`;\n const error = new Error(friendlyMessage);\n error.statusCode = response.status;\n if (response.status === 402 || /insufficient\\s+balance/i.test(upstreamMessage)) {\n error.code = 'DEEPSEEK_PROVIDER_QUOTA';\n }\n throw error;\n }\n\n const message = payload?.choices?.[0]?.message?.content?.trim();\n if (!message) {\n const error = new Error('DeepSeek returned an empty response');\n error.statusCode = 502;\n throw error;\n }\n\n return {\n message,\n model,\n usage: {\n promptTokens: Number(payload?.usage?.prompt_tokens || 0),\n completionTokens: Number(payload?.usage?.completion_tokens || 0),\n totalTokens: Number(payload?.usage?.total_tokens || 0),\n },\n };\n}\n\nasync function callDeepSeekStream(messages, callbacks = {}) {\n if (!deepseekApiKey) {\n const error = new Error('DeepSeek API key is not configured');\n error.statusCode = 503;\n throw error;\n }\n\n const model = normalizeDeepSeekModelName(callbacks.model);\n const response = await fetch(`${deepseekBaseUrl}/chat/completions`, {\n method: 'POST',\n headers: {\n Accept: 'text/event-stream',\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${deepseekApiKey}`,\n },\n body: JSON.stringify({\n model,\n messages,\n temperature: 0.4,\n max_tokens: normalizeMaxCompletionTokens(callbacks.maxCompletionTokens),\n stream: true,\n stream_options: { include_usage: true },\n }),\n signal: callbacks.signal || AbortSignal.timeout(90_000),\n });\n\n if (!response.ok) {\n const bodyText = await response.text();\n let payload;\n try {\n payload = bodyText ? JSON.parse(bodyText) : {};\n } catch {\n payload = { error: bodyText };\n }\n const upstreamMessage = payload?.error?.message || payload?.message || '';\n const friendlyMessage =\n response.status === 402 || /insufficient\\s+balance/i.test(upstreamMessage)\n ? '大乘 AI 服务额度暂不可用,请联系管理员充值 DeepSeek 账户后重试。'\n : upstreamMessage || `DeepSeek request failed: ${response.status}`;\n const error = new Error(friendlyMessage);\n error.statusCode = response.status;\n if (response.status === 402 || /insufficient\\s+balance/i.test(upstreamMessage)) {\n error.code = 'DEEPSEEK_PROVIDER_QUOTA';\n }\n throw error;\n }\n\n let buffer = '';\n let message = '';\n let usage = {\n promptTokens: 0,\n completionTokens: 0,\n totalTokens: 0,\n };\n const decoder = new TextDecoder();\n\n for await (const chunk of response.body) {\n buffer += decoder.decode(chunk, { stream: true });\n const lines = buffer.split(/\\r?\\n/);\n buffer = lines.pop() || '';\n\n for (const rawLine of lines) {\n const line = rawLine.trim();\n if (!line.startsWith('data:')) continue;\n const data = line.slice('data:'.length).trim();\n if (!data || data === '[DONE]') continue;\n\n let payload;\n try {\n payload = JSON.parse(data);\n } catch {\n continue;\n }\n\n const delta = payload?.choices?.[0]?.delta?.content || '';\n if (delta) {\n message += delta;\n callbacks.onToken?.(delta);\n }\n\n if (payload?.usage) {\n usage = {\n promptTokens: Number(payload.usage.prompt_tokens || usage.promptTokens || 0),\n completionTokens: Number(payload.usage.completion_tokens || usage.completionTokens || 0),\n totalTokens: Number(payload.usage.total_tokens || usage.totalTokens || 0),\n };\n }\n }\n }\n\n message = message.trim();\n if (!message) {\n const error = new Error('DeepSeek returned an empty response');\n error.statusCode = 502;\n throw error;\n }\n\n return { message, model, usage };\n}\n\nfunction createCodexDeepSeekRuntime() {\n if (!enableCodexSdkChat || !deepseekApiKey) {\n return {\n enabled: false,\n provider: 'deepseek-direct',\n reason: enableCodexSdkChat ? 'DeepSeek API key is not configured' : 'ENABLE_CODEX_SDK_CHAT is not true',\n };\n }\n\n return {\n enabled: true,\n provider: 'codex-sdk-deepseek',\n options: {\n apiKey: deepseekApiKey,\n baseUrl: deepseekBaseUrl,\n env: {\n ...process.env,\n DEEPSEEK_API_KEY: deepseekApiKey,\n HOME: codexHomeDir,\n CODEX_HOME: codexHomeDir,\n TMPDIR: codexTempDir,\n XDG_RUNTIME_DIR: codexRuntimeDir,\n XDG_CACHE_HOME: path.join(codexHomeDir, '.cache'),\n XDG_CONFIG_HOME: path.join(codexHomeDir, '.config'),\n XDG_DATA_HOME: path.join(codexHomeDir, '.local', 'share'),\n PATH: process.env.PATH || '/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin',\n },\n config: {\n model_provider: codexDeepSeekProviderId,\n model_providers: {\n [codexDeepSeekProviderId]: {\n name: 'DeepSeek Chat Completions',\n base_url: codexResponsesBaseUrl,\n env_key: 'DEEPSEEK_API_KEY',\n wire_api: 'responses',\n query_params: {},\n },\n },\n },\n },\n threadOptions: {\n model: deepseekModel,\n approvalPolicy: 'never',\n sandboxMode: 'read-only',\n skipGitRepoCheck: true,\n networkAccessEnabled: true,\n },\n };\n}\n\nfunction codexPromptFromMessages(messages) {\n return messages\n .map((message) => {\n const role = String(message.role || 'user').toUpperCase();\n return `${role}:\\n${message.content}`;\n })\n .join('\\n\\n');\n}\n\nfunction responseContentText(content) {\n if (typeof content === 'string') return content;\n if (!Array.isArray(content)) return '';\n return content\n .map((part) => {\n if (typeof part === 'string') return part;\n if (!part || typeof part !== 'object') return '';\n return String(part.text || part.input_text || part.output_text || '');\n })\n .filter(Boolean)\n .join('\\n');\n}\n\nfunction codexResponsesPrompt(body) {\n const input = Array.isArray(body?.input) ? body.input : [];\n const userItems = input.filter((item) => item?.role === 'user');\n const source = userItems[userItems.length - 1] || input[input.length - 1];\n const prompt = responseContentText(source?.content).trim();\n if (prompt) return prompt;\n if (typeof body?.input === 'string') return body.input.trim();\n return '';\n}\n\nfunction writeResponsesEvent(res, event, data) {\n res.write(`event: ${event}\\n`);\n res.write(`data: ${JSON.stringify(data)}\\n\\n`);\n}\n\nfunction responseUsage(usage = {}) {\n return {\n input_tokens: Number(usage.promptTokens || usage.input_tokens || 0),\n cached_input_tokens: 0,\n output_tokens: Number(usage.completionTokens || usage.output_tokens || 0),\n reasoning_output_tokens: 0,\n total_tokens: Number(usage.totalTokens || usage.total_tokens || 0),\n };\n}\n\nfunction createLibreChatAgentRuntime() {\n if (!enableLibreChatAgentChat || !libreChatAgentApiKey || !libreChatAgentId) {\n return {\n enabled: false,\n provider: 'librechat-agent',\n reason: !enableLibreChatAgentChat\n ? 'ENABLE_LIBRECHAT_AGENT_CHAT is not true'\n : !libreChatAgentApiKey\n ? 'LIBRECHAT_AGENT_API_KEY is not configured'\n : 'LIBRECHAT_AGENT_ID is not configured',\n };\n }\n\n return {\n enabled: true,\n provider: 'librechat-agent',\n model: libreChatAgentId,\n chatCompletionsUrl: `${libreChatAgentsBaseUrl}/chat/completions`,\n };\n}\n\nfunction libreChatUsage(usage = {}) {\n return {\n promptTokens: Number(usage.prompt_tokens || usage.input_tokens || usage.promptTokens || 0),\n completionTokens: Number(usage.completion_tokens || usage.output_tokens || usage.completionTokens || 0),\n totalTokens: Number(usage.total_tokens || usage.totalTokens || 0),\n };\n}\n\nasync function callLibreChatAgent(messages) {\n const runtime = createLibreChatAgentRuntime();\n if (!runtime.enabled) {\n const error = new Error(runtime.reason || 'LibreChat Agent chat is not enabled');\n error.statusCode = 503;\n throw error;\n }\n\n const response = await fetch(runtime.chatCompletionsUrl, {\n method: 'POST',\n headers: {\n Accept: 'application/json',\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${libreChatAgentApiKey}`,\n },\n body: JSON.stringify({\n model: runtime.model,\n messages,\n temperature: 0.4,\n stream: false,\n }),\n signal: AbortSignal.timeout(120_000),\n });\n\n const bodyText = await response.text();\n let payload;\n try {\n payload = bodyText ? JSON.parse(bodyText) : {};\n } catch {\n payload = { error: bodyText };\n }\n\n if (!response.ok) {\n const upstreamMessage = payload?.error?.message || payload?.message || bodyText || '';\n const error = new Error(upstreamMessage || `LibreChat Agent request failed: ${response.status}`);\n error.statusCode = response.status;\n throw error;\n }\n\n const message = payload?.choices?.[0]?.message?.content?.trim();\n if (!message) {\n const error = new Error('LibreChat Agent returned an empty response');\n error.statusCode = 502;\n throw error;\n }\n\n return {\n message,\n usage: libreChatUsage(payload?.usage),\n };\n}\n\nasync function callLibreChatAgentStream(messages, callbacks = {}) {\n const runtime = createLibreChatAgentRuntime();\n if (!runtime.enabled) {\n const error = new Error(runtime.reason || 'LibreChat Agent chat is not enabled');\n error.statusCode = 503;\n throw error;\n }\n\n const response = await fetch(runtime.chatCompletionsUrl, {\n method: 'POST',\n headers: {\n Accept: 'text/event-stream',\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${libreChatAgentApiKey}`,\n },\n body: JSON.stringify({\n model: runtime.model,\n messages,\n temperature: 0.4,\n stream: true,\n }),\n signal: callbacks.signal || AbortSignal.timeout(180_000),\n });\n\n if (!response.ok) {\n const bodyText = await response.text();\n let payload;\n try {\n payload = bodyText ? JSON.parse(bodyText) : {};\n } catch {\n payload = { error: bodyText };\n }\n const upstreamMessage = payload?.error?.message || payload?.message || bodyText || '';\n const error = new Error(upstreamMessage || `LibreChat Agent request failed: ${response.status}`);\n error.statusCode = response.status;\n throw error;\n }\n\n let buffer = '';\n let message = '';\n let usage = { promptTokens: 0, completionTokens: 0, totalTokens: 0 };\n const decoder = new TextDecoder();\n\n for await (const chunk of response.body) {\n buffer += decoder.decode(chunk, { stream: true });\n const lines = buffer.split(/\\r?\\n/);\n buffer = lines.pop() || '';\n\n for (const rawLine of lines) {\n const line = rawLine.trim();\n if (!line.startsWith('data:')) continue;\n const data = line.slice('data:'.length).trim();\n if (!data || data === '[DONE]') continue;\n\n let payload;\n try {\n payload = JSON.parse(data);\n } catch {\n continue;\n }\n\n const delta = payload?.choices?.[0]?.delta?.content || '';\n if (delta) {\n message += delta;\n callbacks.onToken?.(delta);\n }\n\n if (payload?.usage) {\n usage = libreChatUsage(payload.usage);\n }\n }\n }\n\n message = message.trim();\n if (!message) {\n const error = new Error('LibreChat Agent returned an empty response');\n error.statusCode = 502;\n throw error;\n }\n\n return { message, usage };\n}\n\nfunction isDeepSeekQuotaError(error) {\n if (error?.code === 'DEEPSEEK_PROVIDER_QUOTA') return true;\n if (error?.code === 'USER_TOKEN_QUOTA_EXCEEDED' || error?.code === 'AI_MEMBERSHIP_REQUIRED') return false;\n const message = String(error?.message || error || '');\n return /insufficient\\s+balance|balance/i.test(message);\n}\n\nfunction publicAiErrorMessage(error) {\n if (isDeepSeekQuotaError(error)) {\n return '大乘 AI 服务额度暂不可用,请联系管理员充值 DeepSeek 账户后重试。';\n }\n return error?.message || 'Internal Server Error';\n}\n\nfunction responsesPayload({\n responseId,\n itemId,\n status,\n model,\n text = '',\n usage = null,\n error = null,\n}) {\n return {\n id: responseId,\n object: 'response',\n status,\n model,\n output:\n status === 'completed'\n ? [\n {\n id: itemId,\n type: 'message',\n role: 'assistant',\n status: 'completed',\n content: [{ type: 'output_text', text }],\n },\n ]\n : [],\n ...(usage ? { usage: responseUsage(usage) } : {}),\n ...(error\n ? {\n error: {\n message: error.message || String(error),\n type: 'server_error',\n },\n }\n : {}),\n };\n}\n\nasync function callCodexSdkDeepSeek(messages, callbacks = {}) {\n const codexRuntime = createCodexDeepSeekRuntime();\n if (!codexRuntime.enabled) {\n const error = new Error(codexRuntime.reason || 'Codex SDK chat is not enabled');\n error.statusCode = 503;\n throw error;\n }\n\n const { Codex } = await import('@openai/codex-sdk');\n const codex = new Codex(codexRuntime.options);\n const thread = codex.startThread(codexRuntime.threadOptions);\n const prompt = codexPromptFromMessages(messages);\n\n if (callbacks.onToken || callbacks.onStep) {\n const { events } = await thread.runStreamed(prompt, { signal: callbacks.signal });\n let finalResponse = '';\n let usage = null;\n\n for await (const event of events) {\n if (event.type === 'item.completed') {\n const item = event.item;\n if (item.type === 'agent_message') {\n finalResponse = item.text || finalResponse;\n callbacks.onToken?.(item.text || '');\n } else if (item.type === 'todo_list') {\n callbacks.onStep?.({\n title: 'Codex SDK 执行计划',\n message: item.items.map((todo) => `${todo.completed ? '✓' : '•'} ${todo.text}`).join('\\n'),\n });\n } else if (item.type === 'command_execution') {\n callbacks.onStep?.({\n title: 'Codex SDK 执行命令',\n message: item.command,\n });\n } else if (item.type === 'mcp_tool_call') {\n callbacks.onStep?.({\n title: 'Codex SDK 调用工具',\n message: `${item.server}.${item.tool}`,\n });\n }\n } else if (event.type === 'turn.completed') {\n usage = event.usage;\n } else if (event.type === 'turn.failed') {\n throw new Error(event.error?.message || 'Codex SDK turn failed');\n } else if (event.type === 'error') {\n throw new Error(event.message || 'Codex SDK stream failed');\n }\n }\n\n if (!finalResponse.trim()) {\n const error = new Error('Codex SDK returned an empty response');\n error.statusCode = 502;\n throw error;\n }\n\n return {\n message: finalResponse.trim(),\n usage: {\n promptTokens: Number(usage?.input_tokens || 0),\n completionTokens: Number(usage?.output_tokens || 0),\n totalTokens: Number((usage?.input_tokens || 0) + (usage?.output_tokens || 0)),\n },\n };\n }\n\n const turn = await thread.run(prompt);\n if (!turn.finalResponse?.trim()) {\n const error = new Error('Codex SDK returned an empty response');\n error.statusCode = 502;\n throw error;\n }\n\n return {\n message: turn.finalResponse.trim(),\n usage: {\n promptTokens: Number(turn.usage?.input_tokens || 0),\n completionTokens: Number(turn.usage?.output_tokens || 0),\n totalTokens: Number((turn.usage?.input_tokens || 0) + (turn.usage?.output_tokens || 0)),\n },\n };\n}\n\nconst activeAgentRuns = new Map();\n\nfunction normalizeAgentMode(value) {\n const mode = safeUserText(value || 'dharma_guide');\n return /^[a-z][a-z0-9_:-]{0,48}$/i.test(mode) ? mode : 'dharma_guide';\n}\n\nfunction openClawSessionKey(user, conversationId) {\n const env = process.env.NODE_ENV === 'production' ? 'prod' : 'dev';\n return `fabushi:env:${env}:user:${user.userId}:conversation:${conversationId}`;\n}\n\nfunction createOpenClawRuntime() {\n if (!enableOpenClawAgentChat || !openClawGatewayToken || !openClawGatewayUrl) {\n return {\n enabled: false,\n provider: 'openclaw-gateway',\n reason: !enableOpenClawAgentChat\n ? 'ENABLE_OPENCLAW_AGENT_CHAT is not true'\n : !openClawGatewayToken\n ? 'OPENCLAW_GATEWAY_TOKEN is not configured'\n : 'OPENCLAW_GATEWAY_URL is not configured',\n };\n }\n\n return {\n enabled: true,\n provider: 'openclaw-gateway',\n model: openClawAgentId,\n runsUrl: openClawRunsEndpoint || `${openClawGatewayUrl}/v1/agents/${encodeURIComponent(openClawAgentId)}/runs`,\n };\n}\n\nfunction extractOpenClawText(payload) {\n if (!payload || typeof payload !== 'object') return '';\n const candidates = [\n payload.message,\n payload.content,\n payload.output_text,\n payload.finalResponse,\n payload.final_response,\n payload.result?.message,\n payload.result?.content,\n payload.output?.message,\n payload.output?.content,\n payload.choices?.[0]?.message?.content,\n ];\n for (const candidate of candidates) {\n if (typeof candidate === 'string' && candidate.trim()) return candidate.trim();\n }\n return '';\n}\n\nfunction extractOpenClawUsage(payload) {\n const usage = payload?.usage || payload?.result?.usage || {};\n return {\n promptTokens: Number(usage.input_tokens || usage.prompt_tokens || usage.promptTokens || 0),\n completionTokens: Number(usage.output_tokens || usage.completion_tokens || usage.completionTokens || 0),\n totalTokens: Number(usage.total_tokens || usage.totalTokens || 0),\n };\n}\n\nasync function callOpenClawAgent({ messages, user, conversationId, mode, signal }) {\n const runtime = createOpenClawRuntime();\n if (!runtime.enabled) {\n const error = new Error(runtime.reason || 'OpenClaw Agent chat is not enabled');\n error.statusCode = 503;\n throw error;\n }\n\n const sessionKey = openClawSessionKey(user, conversationId);\n const response = await fetch(runtime.runsUrl, {\n method: 'POST',\n headers: {\n Accept: 'application/json',\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${openClawGatewayToken}`,\n },\n body: JSON.stringify({\n agentId: openClawAgentId,\n sessionKey,\n mode,\n stream: false,\n input: { messages },\n messages,\n metadata: {\n app: 'fabushi',\n userId: user.userId,\n conversationId,\n toolPolicy: 'public-user-read-mostly',\n },\n }),\n signal: signal || AbortSignal.timeout(180_000),\n });\n\n const bodyText = await response.text();\n let payload;\n try {\n payload = bodyText ? JSON.parse(bodyText) : {};\n } catch {\n payload = { message: bodyText };\n }\n\n if (!response.ok) {\n const error = new Error(payload?.error?.message || payload?.message || `OpenClaw request failed: ${response.status}`);\n error.statusCode = response.status;\n throw error;\n }\n\n const message = extractOpenClawText(payload);\n if (!message) {\n const error = new Error('OpenClaw returned an empty response');\n error.statusCode = 502;\n throw error;\n }\n\n return {\n message,\n runId: payload.runId || payload.id || payload.result?.runId || null,\n provider: runtime.provider,\n model: runtime.model,\n usage: extractOpenClawUsage(payload),\n };\n}\n\nasync function runAgentModel({ messages, user, conversationId, mode, callbacks = {}, signal, maxCompletionTokens }) {\n const openClawRuntime = createOpenClawRuntime();\n if (openClawRuntime.enabled && !callbacks.onToken) {\n return await callOpenClawAgent({ messages, user, conversationId, mode, signal });\n }\n\n const codexRuntime = createCodexDeepSeekRuntime();\n const libreChatRuntime = createLibreChatAgentRuntime();\n let provider = libreChatRuntime.enabled\n ? libreChatRuntime.provider\n : codexRuntime.enabled\n ? codexRuntime.provider\n : 'deepseek';\n let model = libreChatRuntime.enabled ? libreChatRuntime.model : deepseekModel;\n\n try {\n if (libreChatRuntime.enabled) {\n const result = callbacks.onToken\n ? await callLibreChatAgentStream(messages, { ...callbacks, signal })\n : await callLibreChatAgent(messages);\n return { ...result, provider, model };\n }\n if (codexRuntime.enabled) {\n const result = await callCodexSdkDeepSeek(messages, { ...callbacks, signal });\n return { ...result, provider, model };\n }\n const result = callbacks.onToken\n ? await callDeepSeekStream(messages, { ...callbacks, signal, maxCompletionTokens })\n : await callDeepSeek(messages, { maxCompletionTokens });\n return { ...result, provider, model };\n } catch (error) {\n if (!codexRuntime.enabled || isDeepSeekQuotaError(error)) throw error;\n logger.warn({ error: String(error) }, 'Agent model failed, falling back to DeepSeek direct');\n const result = callbacks.onToken\n ? await callDeepSeekStream(messages, { ...callbacks, signal, maxCompletionTokens })\n : await callDeepSeek(messages, { maxCompletionTokens });\n return { ...result, provider: 'deepseek', model: deepseekModel };\n }\n}\n\nfunction updateAgentRunStatus(run, status, fields = {}) {\n statements.updateAgentRunStatus.run({\n id: run.id,\n userId: run.user_id || run.userId,\n status,\n openClawRunId: fields.openClawRunId ?? null,\n inputTokens: fields.inputTokens ?? null,\n outputTokens: fields.outputTokens ?? null,\n toolCallCount: fields.toolCallCount ?? null,\n completedAt: status === 'completed' || status === 'cancelled' ? nowIso() : null,\n failedAt: status === 'failed' ? nowIso() : null,\n errorCode: fields.errorCode ?? null,\n errorMessage: fields.errorMessage ?? null,\n });\n}\n\nfunction buildAgentMessages(conversationId) {\n const historyRows = statements.listMessages.all(conversationId).slice(-14);\n return [{ role: 'system', content: systemPrompt }, ...normalizeMessages(historyRows)];\n}\n\nfunction agentUsagePayload(usage, user, budget) {\n const promptTokens = usage.promptTokens || usage.inputTokens || 0;\n const completionTokens = usage.completionTokens || usage.outputTokens || 0;\n const totalTokens = usage.totalTokens || promptTokens + completionTokens;\n const latestUsage = usageFor(user.userId);\n return {\n inputTokens: promptTokens,\n outputTokens: completionTokens,\n totalTokens,\n monthlyLimit: budget.limit,\n remainingTokens: Math.max(0, budget.limit - latestUsage.used),\n };\n}\n\nfunction openAiContentToText(content) {\n if (typeof content === 'string') return content;\n if (Array.isArray(content)) {\n return content\n .map((part) => {\n if (typeof part === 'string') return part;\n if (!part || typeof part !== 'object') return '';\n return readText(part.text || part.content || part.value);\n })\n .filter(Boolean)\n .join('\\n');\n }\n return readText(content);\n}\n\nfunction normalizeOpenAiMessages(value) {\n if (!Array.isArray(value)) return [];\n const allowedRoles = new Set(['system', 'user', 'assistant', 'tool']);\n return value\n .map((item) => {\n if (!item || typeof item !== 'object') return null;\n const role = allowedRoles.has(item.role) ? item.role : 'user';\n const content = openAiContentToText(item.content);\n if (!content) return null;\n return { role, content };\n })\n .filter(Boolean);\n}\n\nfunction truncateForDeepSeekMessage(text, maxChars = 12_000) {\n const value = readText(text);\n if (value.length <= maxChars) return value;\n return `${value.slice(0, maxChars)}\\n\\n[内容已截断]`;\n}\n\nfunction truncateTailForDeepSeekMessage(text, maxChars = 2400) {\n const value = readText(text);\n if (value.length <= maxChars) return value;\n return `[前文已省略]\\n\\n${value.slice(-maxChars)}`;\n}\n\nfunction stripOpenClawPromptNoise(text) {\n const value = readText(text);\n if (!value) return '';\n\n const finalPromptMatch = value.match(/finalPromptText[\"'\\s::]+([\\s\\S]{1,4000})$/i);\n if (finalPromptMatch?.[1]) {\n return readText(finalPromptMatch[1].replace(/^[\"'`]+|[\"'`,}\\]]+$/g, ''));\n }\n\n const markers = [\n 'final prompt:',\n 'final user prompt:',\n 'user prompt:',\n '用户问题:',\n '用户消息:',\n '用户输入:',\n ];\n const lower = value.toLowerCase();\n for (const marker of markers) {\n const index = lower.lastIndexOf(marker.toLowerCase());\n if (index >= 0) {\n const tail = value.slice(index + marker.length).trim();\n if (tail) return tail;\n }\n }\n\n return value;\n}\n\nfunction compactOpenClawMessagesForDeepSeek(messages) {\n const conversationalMessages = messages\n .filter((message) => message.role === 'user' || message.role === 'assistant')\n .map((message) => ({\n role: message.role,\n content: truncateTailForDeepSeekMessage(stripOpenClawPromptNoise(message.content)),\n }))\n .filter((message) => message.content);\n\n const recent = conversationalMessages.slice(-6);\n while (recent.length > 0 && recent[0].role !== 'user') {\n recent.shift();\n }\n\n if (recent.length === 0) {\n const fallback = messages\n .slice()\n .reverse()\n .map((message) => truncateTailForDeepSeekMessage(stripOpenClawPromptNoise(message.content), 2400))\n .find(Boolean);\n if (!fallback) return messages;\n recent.push({ role: 'user', content: fallback });\n }\n\n return [\n {\n role: 'system',\n content: '你是大乘 App 的桌面 AI 助手。请直接回答用户问题,默认使用简洁、准确的中文。',\n },\n ...recent,\n ];\n}\n\nfunction isDeepSeekBlockedError(error) {\n return (\n Number(error?.statusCode) === 403 &&\n /blocked|被拦截|forbidden/i.test(String(error?.message || error || ''))\n );\n}\n\nfunction shouldRetryWithCompactedOpenClawMessages(error, messages, compactedMessages) {\n if (!isDeepSeekBlockedError(error)) return false;\n return JSON.stringify(messages) !== JSON.stringify(compactedMessages);\n}\n\nfunction openClawBlockedFallbackText(messages) {\n const lastUser = messages\n .slice()\n .reverse()\n .find((message) => message.role === 'user')?.content || '';\n const localActionIntent =\n /(file|folder|directory|project|workspace|path|文件|文件夹|目录|项目|工程|路径|本地)/i.test(lastUser) &&\n /(create|make|mkdir|write|edit|modify|rename|delete|move|copy|save|download|read|list|生成|创建|新建|写入|编辑|修改|重命名|删除|移动|复制|保存|下载|读取|列出)/i.test(lastUser);\n\n if (localActionIntent) {\n return [\n '本机工具规划被模型服务拦截了,所以这次没有实际读取、创建、修改或下载本机文件。',\n '如果只是新建文件夹,请先在底部选择项目后重新发送“新建文件夹”;桌面端会直接在已选项目里创建。',\n '如果是更复杂的本地文件操作,请在“连应用”里重启本机 AI 后重试,或把要操作的文件路径和目标说得更具体。',\n ].join('\\n');\n }\n\n return '模型服务拦截了这次回复,未能生成结果。请换一种更具体、更短的表述后重试。';\n}\nfunction usageWithFallback(usage, messages, text) {\n const promptTokens = Number(usage?.promptTokens || usage?.inputTokens || 0);\n const completionTokens = Number(usage?.completionTokens || usage?.outputTokens || 0);\n const totalTokens = Number(usage?.totalTokens || 0);\n if (totalTokens > 0) {\n return {\n promptTokens,\n completionTokens,\n totalTokens,\n };\n }\n const estimatedPrompt = promptTokens || estimateTokens(JSON.stringify(messages));\n const estimatedCompletion = completionTokens || estimateTokens(text);\n return {\n promptTokens: estimatedPrompt,\n completionTokens: estimatedCompletion,\n totalTokens: estimatedPrompt + estimatedCompletion,\n };\n}\n\nfunction openAiUsagePayload(usage, user, budget) {\n const promptTokens = Number(usage.promptTokens || 0);\n const completionTokens = Number(usage.completionTokens || 0);\n const totalTokens = Number(usage.totalTokens || promptTokens + completionTokens);\n const latestUsage = usageFor(user.userId);\n const remainingTokens = Math.max(0, budget.limit - latestUsage.used);\n return {\n prompt_tokens: promptTokens,\n completion_tokens: completionTokens,\n total_tokens: totalTokens,\n promptTokens,\n completionTokens,\n totalTokens,\n monthlyLimit: budget.limit,\n remainingTokens,\n };\n}\n\nfunction writeOpenAiSse(res, payload) {\n res.write(`data: ${JSON.stringify(payload)}\\n\\n`);\n}\n\napp.post(\n '/api/openclaw/deepseek/v1/chat/completions',\n asyncHandler(async (req, res) => {\n const user = await resolveUser(req, req.body || {});\n const incomingMessages = normalizeOpenAiMessages(req.body?.messages);\n if (incomingMessages.length === 0) {\n res.status(400).json({ error: { message: 'messages is required' } });\n return;\n }\n\n const messages = compactOpenClawMessagesForDeepSeek(incomingMessages);\n const promptEstimate = estimateTokens(JSON.stringify(messages));\n const estimated = promptEstimate + 600;\n const budget = enforceTokenBudget(user, estimated);\n const maxCompletionTokens = Math.min(\n completionBudgetFor(budget, promptEstimate),\n normalizeMaxCompletionTokens(req.body?.max_tokens),\n );\n const model = normalizeDeepSeekModelName(req.body?.model);\n const wantsStream =\n req.body?.stream === true ||\n String(req.get('accept') || '').includes('text/event-stream');\n const id = `chatcmpl_${crypto.randomUUID().replaceAll('-', '')}`;\n const created = Math.floor(Date.now() / 1000);\n const compactedMessages = messages;\n\n if (!wantsStream) {\n let result;\n let usageMessages = messages;\n try {\n result = await callDeepSeek(messages, { model, maxCompletionTokens });\n } catch (error) {\n if (!shouldRetryWithCompactedOpenClawMessages(error, messages, compactedMessages)) {\n throw error;\n }\n logger.warn(\n {\n statusCode: error?.statusCode,\n messageCount: messages.length,\n compactedMessageCount: compactedMessages.length,\n },\n 'OpenClaw DeepSeek proxy retrying with compacted messages',\n );\n usageMessages = compactedMessages;\n result = await callDeepSeek(compactedMessages, { model, maxCompletionTokens });\n }\n const usage = usageWithFallback(result.usage, usageMessages, result.message);\n recordUsage(user.userId, usage.totalTokens);\n res.json({\n id,\n object: 'chat.completion',\n created,\n model: result.model || model,\n choices: [\n {\n index: 0,\n message: { role: 'assistant', content: result.message },\n finish_reason: 'stop',\n },\n ],\n usage: openAiUsagePayload(usage, user, budget),\n });\n return;\n }\n\n const abortController = new AbortController();\n req.on('close', () => abortController.abort());\n\n res.status(200);\n res.set({\n 'Content-Type': 'text/event-stream; charset=utf-8',\n 'Cache-Control': 'no-cache, no-transform',\n Connection: 'keep-alive',\n 'X-Accel-Buffering': 'no',\n });\n res.flushHeaders?.();\n\n const baseChunk = {\n id,\n object: 'chat.completion.chunk',\n created,\n model,\n system_fingerprint: 'fabushi-openclaw-deepseek-proxy',\n };\n writeOpenAiSse(res, {\n ...baseChunk,\n choices: [{ index: 0, delta: { role: 'assistant' }, finish_reason: null }],\n });\n\n let text = '';\n let usageMessages = messages;\n try {\n let result;\n try {\n result = await callDeepSeekStream(messages, {\n model,\n maxCompletionTokens,\n signal: abortController.signal,\n onToken: (delta) => {\n text += delta;\n writeOpenAiSse(res, {\n ...baseChunk,\n choices: [{ index: 0, delta: { content: delta }, finish_reason: null }],\n });\n },\n });\n } catch (error) {\n if (!shouldRetryWithCompactedOpenClawMessages(error, messages, compactedMessages)) {\n throw error;\n }\n logger.warn(\n {\n statusCode: error?.statusCode,\n messageCount: messages.length,\n compactedMessageCount: compactedMessages.length,\n },\n 'OpenClaw DeepSeek proxy stream retrying with compacted messages',\n );\n text = '';\n usageMessages = compactedMessages;\n result = await callDeepSeekStream(compactedMessages, {\n model,\n maxCompletionTokens,\n signal: abortController.signal,\n onToken: (delta) => {\n text += delta;\n writeOpenAiSse(res, {\n ...baseChunk,\n choices: [{ index: 0, delta: { content: delta }, finish_reason: null }],\n });\n },\n });\n }\n text = result.message || text;\n const usage = usageWithFallback(result.usage, usageMessages, text);\n recordUsage(user.userId, usage.totalTokens);\n writeOpenAiSse(res, {\n ...baseChunk,\n model: result.model || model,\n choices: [{ index: 0, delta: {}, finish_reason: 'stop' }],\n });\n writeOpenAiSse(res, {\n ...baseChunk,\n model: result.model || model,\n choices: [],\n usage: openAiUsagePayload(usage, user, budget),\n });\n res.write('data: [DONE]\\n\\n');\n } catch (error) {\n if (isDeepSeekBlockedError(error)) {\n const fallbackMessage = openClawBlockedFallbackText(usageMessages);\n const usage = usageWithFallback(null, usageMessages, fallbackMessage);\n recordUsage(user.userId, usage.totalTokens);\n logger.warn(\n { statusCode: error?.statusCode, code: error?.code },\n 'OpenClaw DeepSeek proxy stream blocked; returning local fallback message',\n );\n writeOpenAiSse(res, {\n ...baseChunk,\n choices: [{ index: 0, delta: { content: fallbackMessage }, finish_reason: null }],\n });\n writeOpenAiSse(res, {\n ...baseChunk,\n choices: [{ index: 0, delta: {}, finish_reason: 'stop' }],\n });\n writeOpenAiSse(res, {\n ...baseChunk,\n choices: [],\n usage: openAiUsagePayload(usage, user, budget),\n });\n res.write('data: [DONE]\\n\\n');\n return;\n }\n logger.error(\n { error: String(error), code: error?.code, statusCode: error?.statusCode },\n 'OpenClaw DeepSeek proxy stream failed',\n );\n writeOpenAiSse(res, {\n ...baseChunk,\n choices: [],\n error: {\n message: publicAiErrorMessage(error),\n code: error?.code || 'OPENCLAW_DEEPSEEK_PROXY_ERROR',\n },\n });\n res.write('data: [DONE]\\n\\n');\n } finally {\n res.end();\n }\n }),\n);\n\napp.post(\n '/codex-deepseek/v1/responses',\n asyncHandler(async (req, res) => {\n const bearer = bearerToken(req);\n if (!deepseekApiKey || bearer !== deepseekApiKey) {\n res.status(401).json({\n error: { message: 'Unauthorized Codex DeepSeek adapter request' },\n });\n return;\n }\n\n const prompt = codexResponsesPrompt(req.body);\n if (!prompt) {\n res.status(400).json({ error: { message: 'Responses input is required' } });\n return;\n }\n\n const model = String(req.body?.model || deepseekModel);\n const responseId = `resp_${crypto.randomUUID().replaceAll('-', '')}`;\n const itemId = `msg_${crypto.randomUUID().replaceAll('-', '')}`;\n const wantsStream =\n req.body?.stream !== false ||\n String(req.get('accept') || '').includes('text/event-stream');\n const messages = [{ role: 'user', content: prompt }];\n\n if (!wantsStream) {\n const result = await callDeepSeek(messages);\n res.json(\n responsesPayload({\n responseId,\n itemId,\n status: 'completed',\n model,\n text: result.message,\n usage: result.usage,\n }),\n );\n return;\n }\n\n res.status(200);\n res.set({\n 'Content-Type': 'text/event-stream; charset=utf-8',\n 'Cache-Control': 'no-cache, no-transform',\n Connection: 'keep-alive',\n 'X-Accel-Buffering': 'no',\n });\n res.flushHeaders?.();\n\n writeResponsesEvent(res, 'response.created', {\n type: 'response.created',\n response: responsesPayload({\n responseId,\n itemId,\n status: 'in_progress',\n model,\n }),\n });\n writeResponsesEvent(res, 'response.output_item.added', {\n type: 'response.output_item.added',\n output_index: 0,\n item: {\n id: itemId,\n type: 'message',\n role: 'assistant',\n status: 'in_progress',\n content: [],\n },\n });\n writeResponsesEvent(res, 'response.content_part.added', {\n type: 'response.content_part.added',\n item_id: itemId,\n output_index: 0,\n content_index: 0,\n part: { type: 'output_text', text: '' },\n });\n\n let text = '';\n try {\n const result = await callDeepSeekStream(messages, {\n onToken: (delta) => {\n text += delta;\n writeResponsesEvent(res, 'response.output_text.delta', {\n type: 'response.output_text.delta',\n item_id: itemId,\n output_index: 0,\n content_index: 0,\n delta,\n });\n },\n });\n text = result.message || text;\n writeResponsesEvent(res, 'response.output_text.done', {\n type: 'response.output_text.done',\n item_id: itemId,\n output_index: 0,\n content_index: 0,\n text,\n });\n writeResponsesEvent(res, 'response.content_part.done', {\n type: 'response.content_part.done',\n item_id: itemId,\n output_index: 0,\n content_index: 0,\n part: { type: 'output_text', text },\n });\n writeResponsesEvent(res, 'response.output_item.done', {\n type: 'response.output_item.done',\n output_index: 0,\n item: {\n id: itemId,\n type: 'message',\n role: 'assistant',\n status: 'completed',\n content: [{ type: 'output_text', text }],\n },\n });\n writeResponsesEvent(res, 'response.completed', {\n type: 'response.completed',\n response: responsesPayload({\n responseId,\n itemId,\n status: 'completed',\n model,\n text,\n usage: result.usage,\n }),\n });\n res.end();\n } catch (error) {\n writeResponsesEvent(res, 'response.failed', {\n type: 'response.failed',\n response: responsesPayload({\n responseId,\n itemId,\n status: 'failed',\n model,\n text,\n error,\n }),\n });\n res.end();\n }\n }),\n);\n\napp.use(\n '/api/openclaw/runtime/files',\n express.static(openClawRuntimeUpdatesDir, {\n fallthrough: false,\n immutable: true,\n maxAge: '7d',\n setHeaders(res) {\n res.setHeader('X-Content-Type-Options', 'nosniff');\n },\n }),\n);\n\napp.get(\n '/api/openclaw/runtime/manifest',\n asyncHandler(async (req, res) => {\n const platform = readText(req.query.platform);\n const currentVersion = readText(req.query.currentVersion);\n const appVersion = readText(req.query.appVersion);\n\n let manifest;\n try {\n manifest = await readJsonFile(openClawRuntimeManifestPath);\n } catch (error) {\n logger.info(\n { manifestPath: openClawRuntimeManifestPath, error: String(error) },\n 'OpenClaw runtime manifest unavailable',\n );\n return jsonResponse(res, 200, {\n success: true,\n schema: 1,\n updateAvailable: false,\n reason: 'manifest-unavailable',\n platform,\n currentVersion,\n appVersion,\n });\n }\n\n const latest = manifest.latest && typeof manifest.latest === 'object'\n ? manifest.latest\n : manifest;\n const platforms = latest.platforms && typeof latest.platforms === 'object'\n ? latest.platforms\n : {};\n const platformRelease = platform ? platforms[platform] : null;\n const version = readText(latest.version || manifest.version);\n\n if (!platform || !platformRelease || typeof platformRelease !== 'object') {\n return jsonResponse(res, 200, {\n success: true,\n schema: Number(manifest.schema || 1),\n channel: readText(manifest.channel || latest.channel || 'stable'),\n updateAvailable: false,\n reason: platform ? 'platform-unavailable' : 'platform-required',\n platform,\n currentVersion,\n appVersion,\n latestVersion: version,\n });\n }\n\n if (platform.startsWith('macos-') && !openClawRuntimeEnableMacosUpdates) {\n return jsonResponse(res, 200, {\n success: true,\n schema: Number(manifest.schema || 1),\n channel: readText(manifest.channel || latest.channel || 'stable'),\n updateAvailable: false,\n reason: 'macos-runtime-updates-disabled',\n platform,\n currentVersion,\n appVersion,\n latestVersion: version,\n });\n }\n\n const downloadUrl = absolutizeOpenClawRuntimeUrl(\n req,\n platformRelease.archiveUrl || platformRelease.url || platformRelease.fileName,\n );\n const release = {\n version,\n channel: readText(manifest.channel || latest.channel || 'stable'),\n minAppVersion: readText(latest.minAppVersion || platformRelease.minAppVersion),\n notes: readText(latest.notes || platformRelease.notes),\n nodeExecutable: readText(\n platformRelease.nodeExecutable || latest.nodeExecutable || 'node/bin/node',\n ),\n cliEntrypoint: readText(\n platformRelease.cliEntrypoint || latest.cliEntrypoint || 'openclaw/openclaw.mjs',\n ),\n defaultPort: Number(latest.defaultPort || manifest.defaultPort || 18789),\n defaultModel: readText(latest.defaultModel || manifest.defaultModel || 'openclaw/default'),\n defaultModelOverride: readText(latest.defaultModelOverride || manifest.defaultModelOverride),\n gatewayArgs: Array.isArray(latest.gatewayArgs)\n ? latest.gatewayArgs.map((item) => String(item))\n : ['gateway', '--port', '{port}', '--force'],\n archiveUrl: downloadUrl,\n sha256: readText(platformRelease.sha256),\n size: Number(platformRelease.size || 0),\n };\n\n jsonResponse(res, 200, {\n success: true,\n schema: Number(manifest.schema || 1),\n updateAvailable: Boolean(version && downloadUrl && version !== currentVersion),\n platform,\n currentVersion,\n appVersion,\n latest: release,\n generatedAt: readText(manifest.generatedAt || latest.generatedAt),\n });\n }),\n);\n\napp.get('/health', (_req, res) => {\n const codexRuntime = createCodexDeepSeekRuntime();\n const libreChatRuntime = createLibreChatAgentRuntime();\n const openClawRuntime = createOpenClawRuntime();\n jsonResponse(res, 200, {\n status: 'ok',\n service: 'dacheng-ai-backend',\n provider: libreChatRuntime.enabled ? libreChatRuntime.provider : 'deepseek',\n model: libreChatRuntime.enabled ? libreChatRuntime.model : deepseekModel,\n deepseekModel,\n deepseekAvailableModels,\n libreChatAgentChatEnabled: libreChatRuntime.enabled,\n libreChatAgentProvider: libreChatRuntime.provider,\n libreChatAgentReason: libreChatRuntime.enabled ? undefined : libreChatRuntime.reason,\n codexSdkAvailable: true,\n codexSdkChatEnabled: codexRuntime.enabled,\n codexSdkProvider: codexRuntime.provider,\n openClawAgentChatEnabled: openClawRuntime.enabled,\n openClawAgentProvider: openClawRuntime.provider,\n openClawAgentReason: openClawRuntime.enabled ? undefined : openClawRuntime.reason,\n requireMemberForAi,\n memberMonthlyLimit,\n freeMonthlyLimit,\n deepseekMaxCompletionTokens,\n openClawRuntimeUpdatesDir,\n openClawRuntimeManifestConfigured: fs.existsSync(openClawRuntimeManifestPath),\n cbetaApiRoot,\n timestamp: nowIso(),\n });\n});\n\napp.get(\n '/api/miniapps/registry',\n asyncHandler(async (req, res) => {\n const user = await resolveUser(req, { username: req.query.username });\n const official = officialMiniAppRegistry(req);\n const store = await readMiniAppStore();\n const visibleSandbox = store.apps.filter((appRecord) => {\n if (appRecord.reviewStatus === 'approved' && appRecord.source === 'marketplace') {\n return true;\n }\n return appRecord.ownerId === user.userId;\n });\n const bots = [\n ...official.bots,\n ...visibleSandbox.map((record) => miniAppRecordToBot(record)),\n ];\n const miniApps = [\n ...official.miniApps,\n ...visibleSandbox.map((record) => miniAppRecordToManifest(req, record)),\n ];\n const registry = {\n schemaVersion: 1,\n hostApiVersion: miniAppHostApiVersion,\n bots,\n miniApps,\n updatedAt: nowIso(),\n };\n registry.signature = sha256(JSON.stringify({ bots, miniApps }));\n jsonResponse(res, 200, { success: true, registry });\n }),\n);\n\napp.get(\n '/api/miniapps/dev/:miniAppId/index.html',\n asyncHandler(async (req, res) => {\n const miniAppId = safeMiniAppId(req.params.miniAppId);\n const store = await readMiniAppStore();\n const record = store.apps.find((appRecord) => appRecord.miniAppId === miniAppId);\n if (!record) {\n res.status(404).type('text/html').send('<h1>Mini app not found</h1>');\n return;\n }\n res.setHeader('Content-Type', 'text/html; charset=utf-8');\n res.setHeader('Cache-Control', 'no-store');\n res.send(record.sourceHtml || generatedMiniAppHtml(record));\n }),\n);\n\napp.post(\n '/api/miniapps/dev/create',\n asyncHandler(async (req, res) => {\n const user = await resolveUser(req, req.body || {});\n const title = safeUserText(req.body?.title, '个人小程序').slice(0, 80);\n const subtitle = safeUserText(req.body?.subtitle, '个人沙箱小程序').slice(0, 120);\n const permissions = sanitizeMiniAppPermissions(req.body?.permissions);\n const prompt = safeUserText(req.body?.prompt || title);\n const idSeed = crypto.randomUUID().replaceAll('-', '').slice(0, 12);\n const miniAppId = `sandbox.${idSeed}`;\n const botId = `bot.${miniAppId}`;\n const sourceHtml = generatedMiniAppHtml({ title, prompt });\n const scan = miniAppScan(permissions, sourceHtml);\n const createdAt = nowIso();\n const record = {\n ownerId: user.userId,\n miniAppId,\n botId,\n title,\n subtitle,\n initials: title.slice(0, 1) || '小',\n icon: 'code',\n avatarColor: '#3D8BFF',\n greeting: '你好,我是这个个人沙箱小程序的机器人。',\n inputHint: '输入消息,或打开小程序面板',\n version: '0.0.1',\n permissions,\n surfaces: ['chatPanel'],\n theme: 'telegramDark',\n source: 'sandbox',\n reviewStatus: 'sandbox',\n sourceHtml,\n scan,\n createdAt,\n updatedAt: createdAt,\n };\n const store = await readMiniAppStore();\n store.apps.push(record);\n await writeMiniAppStore(store);\n jsonResponse(res, 200, {\n success: true,\n miniApp: miniAppRecordToManifest(req, record),\n bot: miniAppRecordToBot(record),\n scan,\n });\n }),\n);\n\napp.post(\n '/api/miniapps/dev/:miniAppId/version',\n asyncHandler(async (req, res) => {\n const user = await resolveUser(req, req.body || {});\n const miniAppId = safeMiniAppId(req.params.miniAppId);\n const store = await readMiniAppStore();\n const index = store.apps.findIndex((appRecord) => appRecord.miniAppId === miniAppId);\n if (index < 0) {\n return jsonResponse(res, 404, { success: false, message: 'mini app not found' });\n }\n const record = store.apps[index];\n if (record.ownerId !== user.userId) {\n return jsonResponse(res, 403, { success: false, message: 'not your mini app' });\n }\n const title = safeUserText(req.body?.title, record.title).slice(0, 80);\n const subtitle = safeUserText(req.body?.subtitle, record.subtitle).slice(0, 120);\n const sourceHtml =\n safeUserText(req.body?.sourceHtml) ||\n generatedMiniAppHtml({ title, prompt: safeUserText(req.body?.prompt || title) });\n const permissions = sanitizeMiniAppPermissions(req.body?.permissions || record.permissions);\n const scan = miniAppScan(permissions, sourceHtml);\n store.apps[index] = {\n ...record,\n title,\n subtitle,\n sourceHtml,\n permissions,\n scan,\n reviewStatus: 'sandbox',\n version: safeUserText(req.body?.version, record.version || '0.0.1'),\n updatedAt: nowIso(),\n };\n await writeMiniAppStore(store);\n jsonResponse(res, 200, {\n success: true,\n miniApp: miniAppRecordToManifest(req, store.apps[index]),\n bot: miniAppRecordToBot(store.apps[index]),\n scan,\n });\n }),\n);\n\napp.post(\n '/api/miniapps/:miniAppId/submit-review',\n asyncHandler(async (req, res) => {\n const user = await resolveUser(req, req.body || {});\n const miniAppId = safeMiniAppId(req.params.miniAppId);\n const store = await readMiniAppStore();\n const index = store.apps.findIndex((appRecord) => appRecord.miniAppId === miniAppId);\n if (index < 0) {\n return jsonResponse(res, 404, { success: false, message: 'mini app not found' });\n }\n const record = store.apps[index];\n if (record.ownerId !== user.userId) {\n return jsonResponse(res, 403, { success: false, message: 'not your mini app' });\n }\n const scan = miniAppScan(record.permissions || [], record.sourceHtml || '');\n if (!scan.passed) {\n return jsonResponse(res, 400, {\n success: false,\n message: 'security scan failed',\n scan,\n });\n }\n store.apps[index] = {\n ...record,\n scan,\n reviewStatus: 'pending_review',\n updatedAt: nowIso(),\n };\n await writeMiniAppStore(store);\n jsonResponse(res, 200, {\n success: true,\n miniApp: miniAppRecordToManifest(req, store.apps[index]),\n scan,\n });\n }),\n);\n\napp.post(\n '/api/botfather/generate-miniapp',\n asyncHandler(async (req, res) => {\n const user = await resolveUser(req, req.body || {});\n const prompt = safeUserText(req.body?.prompt);\n if (!prompt) {\n return jsonResponse(res, 400, { success: false, message: 'prompt is required' });\n }\n const title = titleFromPromptForMiniApp(prompt);\n const subtitle = '机器人之父生成的个人沙箱小程序';\n const requestedPermissions = Array.isArray(req.body?.permissions) ? req.body.permissions : [];\n const generated = await generateBotFatherMiniAppHtml({ title, prompt, user });\n let sourceHtml = generated.sourceHtml;\n let generation = generated.generation;\n const permissions = sanitizeMiniAppPermissions([\n ...requestedPermissions,\n ...inferMiniAppPermissionsFromText(prompt, sourceHtml),\n ]);\n let scan = miniAppScan(permissions, sourceHtml);\n if (!scan.passed) {\n sourceHtml = generatedMiniAppHtml({ title, prompt });\n generation = {\n provider: 'template',\n fallbackReason: 'ai_output_failed_security_scan',\n };\n scan = miniAppScan(permissions, sourceHtml);\n }\n const idSeed = crypto.randomUUID().replaceAll('-', '').slice(0, 12);\n const miniAppId = `sandbox.${idSeed}`;\n const createdAt = nowIso();\n const record = {\n ownerId: user.userId,\n miniAppId,\n botId: `bot.${miniAppId}`,\n title,\n subtitle,\n initials: title.slice(0, 1) || '小',\n icon: 'code',\n avatarColor: '#3D8BFF',\n greeting: `我是「${title}」的机器人。`,\n inputHint: '继续描述修改需求,或打开小程序面板',\n version: '0.0.1',\n permissions,\n surfaces: ['chatPanel'],\n theme: 'telegramDark',\n source: 'sandbox',\n reviewStatus: 'sandbox',\n sourceHtml,\n scan,\n generation,\n prompt,\n createdAt,\n updatedAt: createdAt,\n };\n const store = await readMiniAppStore();\n store.apps.push(record);\n await writeMiniAppStore(store);\n jsonResponse(res, 200, {\n success: true,\n miniApp: miniAppRecordToManifest(req, record),\n bot: miniAppRecordToBot(record),\n scan,\n generation,\n actions: ['openMiniApp', 'continueEditing', 'submitReview'],\n });\n }),\n);\n\napp.get('/api/ai/models', (_req, res) => {\n jsonResponse(res, 200, {\n success: true,\n provider: 'deepseek',\n defaultModel: deepseekModel,\n models: deepseekAvailableModels.map((model) => ({\n id: model,\n label: model === 'deepseek-reasoner' ? 'DeepSeek Reasoner' : 'DeepSeek Chat',\n })),\n });\n});\napp.get(\n '/api/ai/quota',\n asyncHandler(async (req, res) => {\n const user = await resolveUser(req, { username: req.query.username });\n const budget = enforceTokenBudget(user, 0);\n const usage = usageFor(user.userId);\n jsonResponse(res, 200, {\n success: true,\n isMember: user.isMember,\n monthlyLimit: budget.limit,\n usedTokens: usage.used,\n remainingTokens: Math.max(0, budget.limit - usage.used),\n month: usage.month,\n });\n }),\n);\n\napp.post(\n '/api/agent/chat',\n asyncHandler(async (req, res) => {\n const message = safeUserText(req.body?.message);\n if (!message) {\n return jsonResponse(res, 400, { success: false, message: 'message is required' });\n }\n\n const user = await resolveUser(req, req.body || {});\n const promptEstimate = estimateTokens(message);\n const estimated = promptEstimate + 600;\n const budget = enforceTokenBudget(user, estimated);\n const maxCompletionTokens = completionBudgetFor(budget, promptEstimate);\n const createdAt = nowIso();\n const conversationId = safeUserText(req.body?.conversationId) || crypto.randomUUID();\n const mode = normalizeAgentMode(req.body?.mode);\n const stream = req.body?.stream === true;\n let conversation = statements.getConversation.get(conversationId, user.userId);\n const isNewConversation = !conversation;\n\n if (isNewConversation) {\n conversation = { id: conversationId, user_id: user.userId, title: titleFromMessage(message) };\n statements.insertConversation.run({\n id: conversationId,\n userId: user.userId,\n title: conversation.title,\n provider: 'agent',\n model: openClawAgentId,\n createdAt,\n updatedAt: createdAt,\n });\n }\n\n const messageId = safeUserText(req.body?.messageId) || crypto.randomUUID();\n statements.insertMessage.run({\n id: messageId,\n conversationId,\n role: 'user',\n content: message,\n promptTokens: 0,\n completionTokens: 0,\n totalTokens: 0,\n createdAt,\n });\n\n const runId = `run_${crypto.randomUUID().replaceAll('-', '')}`;\n const sessionKey = openClawSessionKey(user, conversationId);\n const openClawRuntime = createOpenClawRuntime();\n statements.insertAgentRun.run({\n id: runId,\n userId: user.userId,\n conversationId,\n messageId,\n openClawRunId: null,\n openClawSessionKey: sessionKey,\n status: stream ? 'queued' : 'running',\n mode,\n provider: openClawRuntime.enabled ? openClawRuntime.provider : 'dacheng-ai-fallback',\n model: openClawRuntime.enabled ? openClawAgentId : deepseekModel,\n startedAt: createdAt,\n });\n\n if (stream) {\n return jsonResponse(res, 200, {\n success: true,\n runId,\n conversationId,\n streamUrl: `/api/agent/runs/${runId}/events`,\n });\n }\n\n const controller = new AbortController();\n activeAgentRuns.set(runId, controller);\n try {\n const skillResult = await runResourceFinderSkill({ message, user });\n const skillContext = resourceContextMessage(skillResult);\n const historyRows = statements.listMessages.all(conversationId).slice(-14);\n const modelMessages = [\n { role: 'system', content: systemPrompt },\n ...(skillContext ? [{ role: 'system', content: skillContext }] : []),\n ...normalizeMessages(historyRows),\n ];\n\n const aiResult = await runAgentModel({\n messages: modelMessages,\n user,\n conversationId,\n mode,\n signal: controller.signal,\n maxCompletionTokens,\n });\n const usage = {\n promptTokens: aiResult.usage?.promptTokens || estimateTokens(JSON.stringify(modelMessages)),\n completionTokens: aiResult.usage?.completionTokens || estimateTokens(aiResult.message),\n totalTokens:\n aiResult.usage?.totalTokens ||\n estimateTokens(JSON.stringify(modelMessages)) + estimateTokens(aiResult.message),\n };\n recordUsage(user.userId, usage.totalTokens);\n const answeredAt = nowIso();\n const assistantMessageId = crypto.randomUUID();\n statements.insertMessage.run({\n id: assistantMessageId,\n conversationId,\n role: 'assistant',\n content: aiResult.message,\n promptTokens: usage.promptTokens,\n completionTokens: usage.completionTokens,\n totalTokens: usage.totalTokens,\n createdAt: answeredAt,\n });\n statements.updateConversation.run({\n id: conversationId,\n userId: user.userId,\n title: isNewConversation ? titleFromMessage(message) : conversation.title,\n updatedAt: answeredAt,\n });\n updateAgentRunStatus({ id: runId, user_id: user.userId }, 'completed', {\n openClawRunId: aiResult.runId,\n inputTokens: usage.promptTokens,\n outputTokens: usage.completionTokens,\n });\n const action = determineClientAction(skillResult, message);\n return jsonResponse(res, 200, {\n success: true,\n runId,\n conversationId,\n messageId: assistantMessageId,\n provider: aiResult.provider,\n model: aiResult.model,\n message: aiResult.message,\n usage: agentUsagePayload(usage, user, budget),\n ...(action ? { clientAction: action } : {}),\n });\n } catch (error) {\n const status = error.name === 'AbortError' ? 'cancelled' : 'failed';\n updateAgentRunStatus({ id: runId, user_id: user.userId }, status, {\n errorCode: error.name || 'AGENT_RUN_FAILED',\n errorMessage: publicAiErrorMessage(error),\n });\n if (status === 'cancelled') {\n return jsonResponse(res, 499, { success: false, runId, status: 'cancelled' });\n }\n throw error;\n } finally {\n activeAgentRuns.delete(runId);\n }\n }),\n);\n\napp.get(\n '/api/agent/runs/:runId/events',\n asyncHandler(async (req, res) => {\n const user = await resolveUser(req, {});\n const run = statements.getAgentRun.get(req.params.runId, user.userId);\n if (!run) {\n return jsonResponse(res, 404, { success: false, message: 'run not found' });\n }\n if (run.status === 'completed' || run.status === 'failed' || run.status === 'cancelled') {\n return jsonResponse(res, 409, { success: false, message: `run is already ${run.status}` });\n }\n\n sseHeaders(res);\n const controller = new AbortController();\n activeAgentRuns.set(run.id, controller);\n updateAgentRunStatus(run, 'running');\n writeSse(res, 'run.started', { runId: run.id, conversationId: run.conversation_id });\n\n try {\n let streamedText = '';\n const modelMessages = buildAgentMessages(run.conversation_id);\n const lastUserMsg = [...modelMessages].reverse().find(m => m.role === 'user');\n const userMsgText = lastUserMsg ? lastUserMsg.content : '';\n const skillResult = userMsgText ? await runResourceFinderSkill({\n message: userMsgText,\n user,\n onStep: (step) => writeSse(res, 'tool.call.completed', {\n tool: 'agent.step',\n displayName: step.title || 'Agent step',\n summary: step.message || '',\n }),\n }) : null;\n const skillContext = resourceContextMessage(skillResult);\n const finalMessages = skillContext ? [\n modelMessages[0],\n { role: 'system', content: skillContext },\n ...modelMessages.slice(1)\n ] : modelMessages;\n\n const promptEstimate = estimateTokens(JSON.stringify(finalMessages));\n const estimated = promptEstimate + 600;\n const budget = enforceTokenBudget(user, estimated);\n const maxCompletionTokens = completionBudgetFor(budget, promptEstimate);\n const aiResult = await runAgentModel({\n messages: finalMessages,\n user,\n conversationId: run.conversation_id,\n mode: run.mode,\n signal: controller.signal,\n maxCompletionTokens,\n callbacks: {\n onStep: (step) => writeSse(res, 'tool.call.completed', {\n tool: 'agent.step',\n displayName: step.title || 'Agent step',\n summary: step.message || '',\n }),\n onToken: (delta) => {\n streamedText += delta;\n writeSse(res, 'assistant.delta', { text: delta });\n },\n },\n });\n const message = aiResult.message || streamedText.trim();\n const usage = {\n promptTokens: aiResult.usage?.promptTokens || estimateTokens(JSON.stringify(finalMessages)),\n completionTokens: aiResult.usage?.completionTokens || estimateTokens(message),\n totalTokens:\n aiResult.usage?.totalTokens || estimateTokens(JSON.stringify(finalMessages)) + estimateTokens(message),\n };\n recordUsage(user.userId, usage.totalTokens);\n const assistantMessageId = crypto.randomUUID();\n const answeredAt = nowIso();\n statements.insertMessage.run({\n id: assistantMessageId,\n conversationId: run.conversation_id,\n role: 'assistant',\n content: message,\n promptTokens: usage.promptTokens,\n completionTokens: usage.completionTokens,\n totalTokens: usage.totalTokens,\n createdAt: answeredAt,\n });\n statements.updateConversation.run({\n id: run.conversation_id,\n userId: user.userId,\n title: titleFromMessage(message),\n updatedAt: answeredAt,\n });\n updateAgentRunStatus(run, 'completed', {\n openClawRunId: aiResult.runId,\n inputTokens: usage.promptTokens,\n outputTokens: usage.completionTokens,\n });\n const action = determineClientAction(skillResult, userMsgText);\n writeSse(res, 'assistant.message', { messageId: assistantMessageId, content: message });\n writeSse(res, 'run.completed', {\n usage: agentUsagePayload(usage, user, budget),\n ...(action ? { clientAction: action } : {}),\n });\n res.end();\n } catch (error) {\n const status = error.name === 'AbortError' ? 'cancelled' : 'failed';\n updateAgentRunStatus(run, status, {\n errorCode: error.name || 'AGENT_RUN_FAILED',\n errorMessage: publicAiErrorMessage(error),\n });\n writeSse(res, status === 'cancelled' ? 'run.cancelled' : 'run.failed', {\n runId: run.id,\n message: publicAiErrorMessage(error),\n });\n res.end();\n } finally {\n activeAgentRuns.delete(run.id);\n }\n }),\n);\n\napp.post(\n '/api/agent/runs/:runId/cancel',\n asyncHandler(async (req, res) => {\n const user = await resolveUser(req, req.body || {});\n const run = statements.getAgentRun.get(req.params.runId, user.userId);\n if (!run) {\n return jsonResponse(res, 404, { success: false, message: 'run not found' });\n }\n const controller = activeAgentRuns.get(run.id);\n if (controller) controller.abort();\n updateAgentRunStatus(run, 'cancelled');\n return jsonResponse(res, 200, { success: true, runId: run.id, status: 'cancelled' });\n }),\n);\n\napp.post(\n '/api/agent/messages/:messageId/feedback',\n asyncHandler(async (req, res) => {\n const user = await resolveUser(req, req.body || {});\n const rating = safeUserText(req.body?.rating);\n if (!['up', 'down'].includes(rating)) {\n return jsonResponse(res, 400, { success: false, message: 'rating must be up or down' });\n }\n statements.insertAgentFeedback.run({\n id: crypto.randomUUID(),\n messageId: req.params.messageId,\n userId: user.userId,\n rating,\n reason: safeUserText(req.body?.reason),\n comment: safeUserText(req.body?.comment),\n createdAt: nowIso(),\n });\n return jsonResponse(res, 200, { success: true });\n }),\n);\n\napp.post(\n '/api/ai/chat',\n asyncHandler(async (req, res) => {\n const message = safeUserText(req.body?.message);\n if (!message) {\n return jsonResponse(res, 400, { success: false, message: 'message is required' });\n }\n\n const user = await resolveUser(req, req.body || {});\n const promptEstimate = estimateTokens(message);\n const estimated = promptEstimate + 600;\n const budget = enforceTokenBudget(user, estimated);\n const maxCompletionTokens = completionBudgetFor(budget, promptEstimate);\n const createdAt = nowIso();\n const conversationId = safeUserText(req.body?.conversationId) || crypto.randomUUID();\n const requestedModel = normalizeDeepSeekModelName(req.body?.model);\n const clientContext = clientContextMessage(req.body?.client);\n let conversation = statements.getConversation.get(conversationId, user.userId);\n const isNewConversation = !conversation;\n\n if (isNewConversation) {\n conversation = {\n id: conversationId,\n user_id: user.userId,\n title: titleFromMessage(message),\n };\n statements.insertConversation.run({\n id: conversationId,\n userId: user.userId,\n title: conversation.title,\n provider: 'deepseek',\n model: requestedModel,\n createdAt,\n updatedAt: createdAt,\n });\n }\n\n statements.insertMessage.run({\n id: crypto.randomUUID(),\n conversationId,\n role: 'user',\n content: message,\n promptTokens: 0,\n completionTokens: 0,\n totalTokens: 0,\n createdAt,\n });\n\n const libreChatRuntime = createLibreChatAgentRuntime();\n const skillResult = await runResourceFinderSkill({ message, user });\n const skillContext = resourceContextMessage(skillResult);\n const historyRows = statements.listMessages.all(conversationId).slice(-14);\n const modelMessages = [\n { role: 'system', content: systemPrompt },\n ...(clientContext ? [{ role: 'system', content: clientContext }] : []),\n ...(skillContext ? [{ role: 'system', content: skillContext }] : []),\n ...normalizeMessages(historyRows),\n ];\n\n const openClawRuntime = createOpenClawRuntime();\n const codexRuntime = createCodexDeepSeekRuntime();\n let provider = openClawRuntime.enabled\n ? openClawRuntime.provider\n : libreChatRuntime.enabled\n ? libreChatRuntime.provider\n : 'deepseek';\n let responseModel = openClawRuntime.enabled\n ? openClawRuntime.model\n : libreChatRuntime.enabled\n ? libreChatRuntime.model\n : requestedModel;\n let aiResult;\n try {\n if (openClawRuntime.enabled) {\n aiResult = await callOpenClawAgent({\n messages: modelMessages,\n user,\n conversationId,\n mode: normalizeAgentMode(req.body?.mode),\n });\n provider = aiResult.provider || provider;\n responseModel = aiResult.model || responseModel;\n } else if (libreChatRuntime.enabled) {\n aiResult = await callLibreChatAgent(modelMessages);\n } else if (codexRuntime.enabled) {\n provider = codexRuntime.provider;\n aiResult = await callCodexSdkDeepSeek(modelMessages);\n } else {\n aiResult = await callDeepSeek(modelMessages, { model: requestedModel, maxCompletionTokens });\n }\n } catch (error) {\n if (libreChatRuntime.enabled) {\n logger.warn({ error: String(error) }, 'LibreChat Agent chat failed, falling back to Codex SDK or DeepSeek direct');\n provider = codexRuntime.enabled ? codexRuntime.provider : 'deepseek';\n responseModel = requestedModel;\n if (codexRuntime.enabled) {\n try {\n aiResult = await callCodexSdkDeepSeek(modelMessages);\n } catch (codexError) {\n if (isDeepSeekQuotaError(codexError)) throw codexError;\n logger.warn({ error: String(codexError) }, 'Codex SDK chat failed, falling back to DeepSeek direct');\n provider = 'deepseek';\n aiResult = await callDeepSeek(modelMessages, { model: requestedModel, maxCompletionTokens });\n }\n } else {\n aiResult = await callDeepSeek(modelMessages, { model: requestedModel, maxCompletionTokens });\n }\n } else {\n if (!codexRuntime.enabled || isDeepSeekQuotaError(error)) throw error;\n logger.warn({ error: String(error) }, 'Codex SDK chat failed, falling back to DeepSeek direct');\n provider = 'deepseek';\n responseModel = requestedModel;\n aiResult = await callDeepSeek(modelMessages, { model: requestedModel, maxCompletionTokens });\n }\n }\n const usage = {\n promptTokens: aiResult.usage.promptTokens || estimateTokens(JSON.stringify(modelMessages)),\n completionTokens: aiResult.usage.completionTokens || estimateTokens(aiResult.message),\n totalTokens:\n aiResult.usage.totalTokens ||\n estimateTokens(JSON.stringify(modelMessages)) + estimateTokens(aiResult.message),\n };\n recordUsage(user.userId, usage.totalTokens);\n\n const answeredAt = nowIso();\n statements.insertMessage.run({\n id: crypto.randomUUID(),\n conversationId,\n role: 'assistant',\n content: aiResult.message,\n promptTokens: usage.promptTokens,\n completionTokens: usage.completionTokens,\n totalTokens: usage.totalTokens,\n createdAt: answeredAt,\n });\n statements.updateConversation.run({\n id: conversationId,\n userId: user.userId,\n title: isNewConversation ? titleFromMessage(message) : conversation.title,\n updatedAt: answeredAt,\n });\n\n const latestUsage = usageFor(user.userId);\n const monthlyLimit = budget.limit;\n const action = determineClientAction(skillResult, message);\n jsonResponse(res, 200, {\n success: true,\n conversationId,\n provider,\n model: responseModel,\n message: aiResult.message,\n usage: {\n ...usage,\n monthlyLimit,\n remainingTokens: Math.max(0, monthlyLimit - latestUsage.used),\n },\n ...(action ? { clientAction: action } : {}),\n });\n }),\n);\n\napp.post('/api/ai/chat/stream', async (req, res) => {\n sseHeaders(res);\n\n try {\n const message = safeUserText(req.body?.message);\n if (!message) {\n writeSse(res, 'error', { message: 'message is required' });\n res.end();\n return;\n }\n\n const user = await resolveUser(req, req.body || {});\n const promptEstimate = estimateTokens(message);\n const estimated = promptEstimate + 600;\n const budget = enforceTokenBudget(user, estimated);\n const maxCompletionTokens = completionBudgetFor(budget, promptEstimate);\n const createdAt = nowIso();\n const conversationId = safeUserText(req.body?.conversationId) || crypto.randomUUID();\n const requestedModel = normalizeDeepSeekModelName(req.body?.model);\n const clientContext = clientContextMessage(req.body?.client);\n let conversation = statements.getConversation.get(conversationId, user.userId);\n const isNewConversation = !conversation;\n\n if (isNewConversation) {\n conversation = {\n id: conversationId,\n user_id: user.userId,\n title: titleFromMessage(message),\n };\n statements.insertConversation.run({\n id: conversationId,\n userId: user.userId,\n title: conversation.title,\n provider: 'deepseek',\n model: requestedModel,\n createdAt,\n updatedAt: createdAt,\n });\n }\n\n statements.insertMessage.run({\n id: crypto.randomUUID(),\n conversationId,\n role: 'user',\n content: message,\n promptTokens: 0,\n completionTokens: 0,\n totalTokens: 0,\n createdAt,\n });\n\n const codexRuntime = createCodexDeepSeekRuntime();\n const libreChatRuntime = createLibreChatAgentRuntime();\n const initialOpenClawRuntime = createOpenClawRuntime();\n let provider = initialOpenClawRuntime.enabled\n ? initialOpenClawRuntime.provider\n : libreChatRuntime.enabled\n ? libreChatRuntime.provider\n : codexRuntime.enabled\n ? codexRuntime.provider\n : 'deepseek';\n let responseModel = initialOpenClawRuntime.enabled\n ? initialOpenClawRuntime.model\n : libreChatRuntime.enabled\n ? libreChatRuntime.model\n : requestedModel;\n writeSse(res, 'meta', {\n conversationId,\n provider,\n model: responseModel,\n });\n const skillResult = await runResourceFinderSkill({\n message,\n user,\n onStep: (step) => writeSse(res, 'step', step),\n });\n const skillContext = resourceContextMessage(skillResult);\n const historyRows = statements.listMessages.all(conversationId).slice(-14);\n const modelMessages = [\n { role: 'system', content: systemPrompt },\n ...(clientContext ? [{ role: 'system', content: clientContext }] : []),\n ...(skillContext ? [{ role: 'system', content: skillContext }] : []),\n ...normalizeMessages(historyRows),\n ];\n\n let aiResult;\n const openClawRuntime = initialOpenClawRuntime;\n try {\n if (openClawRuntime.enabled) {\n aiResult = await callOpenClawAgent({\n messages: modelMessages,\n user,\n conversationId,\n mode: normalizeAgentMode(req.body?.mode),\n });\n provider = aiResult.provider || provider;\n responseModel = aiResult.model || responseModel;\n writeSse(res, 'delta', { text: aiResult.message });\n } else if (libreChatRuntime.enabled) {\n aiResult = await callLibreChatAgentStream(modelMessages, {\n onToken: (token) => writeSse(res, 'delta', { text: token }),\n });\n } else if (codexRuntime.enabled) {\n aiResult = await callCodexSdkDeepSeek(modelMessages, {\n onToken: (token) => writeSse(res, 'delta', { text: token }),\n onStep: (step) => writeSse(res, 'step', step),\n });\n } else {\n aiResult = await callDeepSeekStream(modelMessages, {\n model: requestedModel,\n maxCompletionTokens,\n onToken: (token) => writeSse(res, 'delta', { text: token }),\n });\n }\n } catch (error) {\n if (libreChatRuntime.enabled) {\n logger.warn({ error: String(error) }, 'LibreChat Agent streaming chat failed, falling back to Codex SDK or DeepSeek direct');\n provider = codexRuntime.enabled ? codexRuntime.provider : 'deepseek';\n responseModel = requestedModel;\n writeSse(res, 'meta', {\n conversationId,\n provider,\n model: responseModel,\n });\n if (codexRuntime.enabled) {\n try {\n aiResult = await callCodexSdkDeepSeek(modelMessages, {\n onToken: (token) => writeSse(res, 'delta', { text: token }),\n onStep: (step) => writeSse(res, 'step', step),\n });\n } catch (codexError) {\n if (isDeepSeekQuotaError(codexError)) throw codexError;\n logger.warn({ error: String(codexError) }, 'Codex SDK streaming chat failed, falling back to DeepSeek direct');\n provider = 'deepseek';\n responseModel = requestedModel;\n writeSse(res, 'meta', {\n conversationId,\n provider,\n model: responseModel,\n });\n aiResult = await callDeepSeekStream(modelMessages, {\n model: requestedModel,\n maxCompletionTokens,\n onToken: (token) => writeSse(res, 'delta', { text: token }),\n });\n }\n } else {\n aiResult = await callDeepSeekStream(modelMessages, {\n model: requestedModel,\n maxCompletionTokens,\n onToken: (token) => writeSse(res, 'delta', { text: token }),\n });\n }\n } else {\n if (!codexRuntime.enabled || isDeepSeekQuotaError(error)) throw error;\n logger.warn({ error: String(error) }, 'Codex SDK streaming chat failed, falling back to DeepSeek direct');\n provider = 'deepseek';\n responseModel = requestedModel;\n writeSse(res, 'meta', {\n conversationId,\n provider,\n model: responseModel,\n });\n aiResult = await callDeepSeekStream(modelMessages, {\n model: requestedModel,\n maxCompletionTokens,\n onToken: (token) => writeSse(res, 'delta', { text: token }),\n });\n }\n }\n\n const usage = {\n promptTokens: aiResult.usage.promptTokens || estimateTokens(JSON.stringify(modelMessages)),\n completionTokens: aiResult.usage.completionTokens || estimateTokens(aiResult.message),\n totalTokens:\n aiResult.usage.totalTokens ||\n estimateTokens(JSON.stringify(modelMessages)) + estimateTokens(aiResult.message),\n };\n recordUsage(user.userId, usage.totalTokens);\n\n const answeredAt = nowIso();\n statements.insertMessage.run({\n id: crypto.randomUUID(),\n conversationId,\n role: 'assistant',\n content: aiResult.message,\n promptTokens: usage.promptTokens,\n completionTokens: usage.completionTokens,\n totalTokens: usage.totalTokens,\n createdAt: answeredAt,\n });\n statements.updateConversation.run({\n id: conversationId,\n userId: user.userId,\n title: isNewConversation ? titleFromMessage(message) : conversation.title,\n updatedAt: answeredAt,\n });\n\n const latestUsage = usageFor(user.userId);\n const monthlyLimit = budget.limit;\n const action = determineClientAction(skillResult, message);\n writeSse(res, 'done', {\n conversationId,\n provider,\n model: responseModel,\n message: aiResult.message,\n usage: {\n ...usage,\n monthlyLimit,\n remainingTokens: Math.max(0, monthlyLimit - latestUsage.used),\n },\n ...(action ? { clientAction: action } : {}),\n });\n res.end();\n } catch (error) {\n logger.error({ error: error.stack || String(error) }, 'streaming chat failed');\n writeSse(res, 'error', {\n message: publicAiErrorMessage(error),\n details: error.details,\n });\n res.end();\n }\n});\n\napp.get(\n '/api/ai/conversations',\n asyncHandler(async (req, res) => {\n const user = await resolveUser(req, { username: req.query.username });\n const items = statements.listConversations.all(user.userId).map((item) => ({\n id: item.id,\n title: item.title,\n updatedAt: item.updatedAt,\n }));\n jsonResponse(res, 200, { success: true, items });\n }),\n);\n\napp.get(\n '/api/ai/conversations/:id',\n asyncHandler(async (req, res) => {\n const user = await resolveUser(req);\n const conversation = statements.getConversation.get(req.params.id, user.userId);\n if (!conversation) {\n return jsonResponse(res, 404, { success: false, message: 'conversation not found' });\n }\n const messages = statements.listMessages.all(req.params.id).map((item) => ({\n role: item.role,\n content: item.content,\n createdAt: item.createdAt,\n }));\n jsonResponse(res, 200, {\n success: true,\n conversation: {\n id: conversation.id,\n title: conversation.title,\n updatedAt: conversation.updated_at,\n },\n messages,\n });\n }),\n);\n\nfunction buildUrl(base, endpoint, params = {}) {\n const url = new URL(endpoint, `${base}/`);\n for (const [key, value] of Object.entries(params)) {\n if (Array.isArray(value)) {\n for (const item of value) {\n if (item !== undefined && item !== null && item !== '') {\n url.searchParams.append(key, String(item));\n }\n }\n } else if (value !== undefined && value !== null && value !== '') {\n url.searchParams.set(key, String(value));\n }\n }\n return url;\n}\n\nasync function fetchJson(url) {\n const response = await fetch(url, {\n headers: { Accept: 'application/json' },\n signal: AbortSignal.timeout(15_000),\n });\n if (!response.ok) {\n throw new Error(`Request failed: ${response.status}`);\n }\n return response.json();\n}\n\nfunction stripHtml(value) {\n return String(value || '')\n .replace(/<script[\\s\\S]*?<\\/script>/gi, ' ')\n .replace(/<style[\\s\\S]*?<\\/style>/gi, ' ')\n .replace(/<[^>]+>/g, ' ')\n .replace(/ /g, ' ')\n .replace(/</g, '<')\n .replace(/>/g, '>')\n .replace(/&/g, '&')\n .replace(/&#(\\d+);/g, (_match, code) => String.fromCharCode(Number(code)))\n .replace(/\\s+/g, ' ')\n .trim();\n}\n\nfunction cleanExtractedText(value) {\n let text = String(value || '')\n .replace(/\\b[A-Z]{1,3}\\d{2}n\\d{4,5}[a-z]?_p?\\d{4}[a-z]\\d{2}\\b/g, ' ')\n .replace(/\\uFFFD/g, '')\n .replace(/\\s+([,。;:!?、」』)》】])/g, '$1')\n .replace(/([「『《(【])\\s+/g, '$1');\n for (let i = 0; i < 3; i += 1) {\n text = text.replace(/([\\u3400-\\u9fff])\\s+([\\u3400-\\u9fff])/g, '$1$2');\n }\n return text.replace(/[ \\t]{2,}/g, ' ').trim();\n}\n\nfunction compactText(value, limit = 140) {\n const text = stripHtml(value);\n return text.length > limit ? `${text.slice(0, limit)}...` : text;\n}\n\nfunction collectSearchResults(data) {\n if (!data || typeof data !== 'object') return [];\n if (Array.isArray(data.results)) return data.results;\n if (Array.isArray(data?.results?.docs)) return data.results.docs;\n if (Array.isArray(data?.response?.docs)) return data.response.docs;\n if (Array.isArray(data.docs)) return data.docs;\n return [];\n}\n\nfunction firstText(item, keys) {\n for (const key of keys) {\n const value = item?.[key];\n if (Array.isArray(value)) {\n const first = value.map((entry) => safeUserText(String(entry))).find(Boolean);\n if (first) return first;\n }\n if (typeof value === 'string' && value.trim()) return value.trim();\n if (typeof value === 'number') return String(value);\n }\n return '';\n}\n\nfunction firstNumber(item, keys, fallback = undefined) {\n for (const key of keys) {\n const value = item?.[key];\n if (typeof value === 'number') return value;\n if (typeof value === 'string' && value.trim() && Number.isFinite(Number(value))) {\n return Number(value);\n }\n }\n return fallback;\n}\n\nasync function searchCbetaTitle(query, limit) {\n const url = buildUrl(cbetaApiRoot, 'search/title', { q: query, start: 0, rows: limit });\n const data = await fetchJson(url);\n return collectSearchResults(data).flatMap((item) => {\n const work = firstText(item, ['work', 'work_id', 'workId']);\n const title = firstText(item, ['content', 'title', 'work_title', 'workTitle']);\n if (!work || !title) return [];\n const juan = firstNumber(item, ['juan', 'juan_num', 'juanNum'], 1);\n return [\n {\n id: `cbeta-title-${work}-${juan}`,\n title,\n sourceName: 'CBETA 佛典',\n url: `cbeta:${work}:${juan}`,\n snippet: firstText(item, ['byline', 'creators', 'time_dynasty']) || '可下载经文正文并用于全球法布施。',\n resourceType: 'scripture',\n work,\n juan,\n },\n ];\n });\n}\n\nasync function searchCbetaContent(query, limit) {\n const endpoints = ['search/fulltext', 'search/content', 'search/all_in_one'];\n const settled = await Promise.allSettled(\n endpoints.map((endpoint) =>\n fetchJson(buildUrl(cbetaApiRoot, endpoint, { q: query, start: 0, rows: limit, field: 'content' })),\n ),\n );\n const results = [];\n for (const response of settled) {\n if (response.status !== 'fulfilled') continue;\n for (const item of collectSearchResults(response.value)) {\n const work = firstText(item, ['work', 'work_id', 'workId', 'sutra', 'id']);\n const snippet = compactText(firstText(item, ['kwic', 'highlight', 'snippet', 'content', 'text', 'body', 'p']));\n if (!work || !snippet) continue;\n const juan = firstNumber(item, ['juan', 'juan_num', 'juanNum', '卷'], 1);\n const title = firstText(item, ['title', 'work_title', 'workTitle', 'sutra_name', 'book', 'name']) || work;\n results.push({\n id: `cbeta-content-${work}-${juan}-${sha256(snippet).slice(0, 8)}`,\n title,\n sourceName: 'CBETA 佛典',\n url: `cbeta:${work}:${juan}`,\n snippet,\n resourceType: 'scripture',\n work,\n juan,\n });\n if (results.length >= limit) return results;\n }\n }\n return results;\n}\n\nfunction directUrlResource(query) {\n const url = safeUserText(query);\n let parsed;\n try {\n parsed = new URL(url);\n } catch {\n return null;\n }\n if (!['http:', 'https:'].includes(parsed.protocol)) return null;\n return {\n id: `url-${sha256(url).slice(0, 12)}`,\n title: parsed.hostname,\n sourceName: '网页资源',\n url,\n snippet: '直接下载并提取这个链接的可读正文。',\n resourceType: 'web',\n };\n}\n\nfunction looksLikeAudioQuery(query) {\n return /音乐|音樂|音频|音訊|mp3|audio|念诵|念誦|诵经|誦經|唱诵|唱誦|读诵|讀誦/.test(\n safeUserText(query).toLowerCase(),\n );\n}\n\nfunction archiveSearchTerm(query) {\n return (\n safeUserText(query)\n .replace(/下载|查找|寻找|找一?个|资源|音频|音訊|音乐|音樂|mp3|audio|念诵|念誦|诵经|誦經|唱诵|唱誦|读诵|讀誦/gi, ' ')\n .replace(/\\s+/g, ' ')\n .trim() || safeUserText(query)\n );\n}\n\nfunction solrPhrase(value) {\n return String(value).replace(/\\\\/g, '\\\\\\\\').replace(/\"/g, '\\\\\"');\n}\n\nasync function searchInternetArchiveAudio(query, limit) {\n if (!looksLikeAudioQuery(query)) return [];\n const term = archiveSearchTerm(query);\n if (!term) return [];\n const variants = Array.from(\n new Set([\n term,\n term.replaceAll('金刚经', '金剛經'),\n term.replaceAll('金剛經', '金刚经'),\n ].filter(Boolean)),\n );\n const phraseQuery = variants\n .flatMap((item) => [`title:\"${solrPhrase(item)}\"`, `description:\"${solrPhrase(item)}\"`])\n .join(' OR ');\n const url = buildUrl('https://archive.org', 'advancedsearch.php', {\n q: `(${phraseQuery}) AND mediatype:audio`,\n rows: Math.min(Math.max(limit, 1), 10),\n output: 'json',\n 'fl[]': ['identifier', 'title', 'description'],\n });\n const data = await fetchJson(url);\n const docs = Array.isArray(data?.response?.docs) ? data.response.docs : [];\n return docs\n .map((item) => {\n const identifier = firstText(item, ['identifier']);\n const title = firstText(item, ['title']) || identifier;\n if (!identifier || !title) return null;\n return {\n id: `ia-audio-${identifier}`,\n title,\n sourceName: 'Internet Archive 音频',\n url: `ia:${identifier}`,\n snippet: compactText(firstText(item, ['description'])) || '公开可访问的音频资源,将下载第一个可用 MP3 文件。',\n resourceType: 'audio',\n identifier,\n };\n })\n .filter(Boolean);\n}\n\nconst curatedCbetaWorks = [\n {\n work: 'T0235',\n juan: 1,\n title: '金剛般若波羅蜜經',\n keywords: ['金剛般若波羅蜜經', '金刚般若波罗蜜经', '金剛經', '金刚经'],\n snippet: '姚秦 鳩摩羅什譯。常用于读诵、共修与法布施的般若经典。',\n },\n {\n work: 'T0251',\n juan: 1,\n title: '般若波羅蜜多心經',\n keywords: ['般若波羅蜜多心經', '般若波罗蜜多心经', '心經', '心经'],\n snippet: '唐 玄奘譯。篇幅短,适合加入禅室功课本与日常持诵。',\n },\n {\n work: 'T0262',\n juan: 1,\n title: '妙法蓮華經',\n keywords: ['妙法蓮華經', '妙法莲华经', '法華經', '法华经'],\n snippet: '姚秦 鳩摩羅什譯。可按卷下载正文,用于长期功课与分享。',\n },\n {\n work: 'T0279',\n juan: 1,\n title: '大方廣佛華嚴經',\n keywords: ['大方廣佛華嚴經', '大方广佛华严经', '華嚴經', '华严经'],\n snippet: '唐 實叉難陀譯。可按卷下载正文,用于系统研读与法布施。',\n },\n {\n work: 'T0360',\n juan: 1,\n title: '佛說無量壽經',\n keywords: ['佛說無量壽經', '佛说无量寿经', '無量壽經', '无量寿经'],\n snippet: '净土经典。可下载正文,用于净土功课本与全球法布施。',\n },\n {\n work: 'T0365',\n juan: 1,\n title: '佛說觀無量壽佛經',\n keywords: ['佛說觀無量壽佛經', '佛说观无量寿佛经', '觀無量壽經', '观无量寿经'],\n snippet: '净土三经之一。适合查找下载后加入净土功课。',\n },\n {\n work: 'T0366',\n juan: 1,\n title: '佛說阿彌陀經',\n keywords: ['佛說阿彌陀經', '佛说阿弥陀经', '阿彌陀經', '阿弥陀经'],\n snippet: '姚秦 鳩摩羅什譯。篇幅适中,适合共修、持诵与分享。',\n },\n {\n work: 'T0475',\n juan: 1,\n title: '維摩詰所說經',\n keywords: ['維摩詰所說經', '维摩诘所说经', '維摩詰經', '维摩诘经'],\n snippet: '姚秦 鳩摩羅什譯。大乘经典,可按卷下载用于研读。',\n },\n {\n work: 'T0676',\n juan: 1,\n title: '解深密經',\n keywords: ['解深密經', '解深密经'],\n snippet: '唯识相关大乘经典。适合研读、整理笔记与法布施。',\n },\n {\n work: 'T1666',\n juan: 1,\n title: '大乘起信論',\n keywords: ['大乘起信論', '大乘起信论', '起信論', '起信论'],\n snippet: '大乘论典。适合下载后作为研习功课资料。',\n },\n];\n\nfunction localCbetaResults(query, limit) {\n const normalizedQuery = safeUserText(query).toLowerCase();\n if (!normalizedQuery) return [];\n return curatedCbetaWorks\n .filter((item) =>\n item.keywords.some((keyword) => {\n const normalizedKeyword = keyword.toLowerCase();\n return normalizedKeyword.includes(normalizedQuery) || normalizedQuery.includes(normalizedKeyword);\n }),\n )\n .slice(0, limit)\n .map((item) => ({\n id: `cbeta-curated-${item.work}-${item.juan}`,\n title: item.title,\n sourceName: 'CBETA 佛典',\n url: `cbeta:${item.work}:${item.juan}`,\n snippet: item.snippet,\n resourceType: 'scripture',\n work: item.work,\n juan: item.juan,\n }));\n}\n\nfunction uniqueResources(items, limit) {\n const seen = new Set();\n const results = [];\n for (const item of items) {\n const key = `${item.url}:${item.title}`;\n if (seen.has(key)) continue;\n seen.add(key);\n results.push(item);\n if (results.length >= limit) break;\n }\n return results;\n}\n\nasync function findResourceCandidates(query, limit) {\n const direct = directUrlResource(query);\n const results = [];\n if (direct) results.push(direct);\n try {\n results.push(...(await searchInternetArchiveAudio(query, limit)));\n } catch (error) {\n logger.warn({ error: String(error), query }, 'Internet Archive audio search skipped');\n }\n results.push(...localCbetaResults(query, limit));\n\n const [titleResults, contentResults] = await Promise.allSettled([\n searchCbetaTitle(query, limit),\n searchCbetaContent(query, limit),\n ]);\n if (titleResults.status === 'fulfilled') results.push(...titleResults.value);\n if (contentResults.status === 'fulfilled') results.push(...contentResults.value);\n\n return uniqueResources(results, limit);\n}\n\nfunction looksLikeResourceTask(message) {\n const text = safeUserText(message).toLowerCase();\n if (!text) return false;\n return /下载|查找|寻找|找一?个|资源|經文|经文|佛经|佛經|仪轨|儀軌|音乐|音樂|mp3|audio|念诵|念誦|诵经|誦經/.test(text);\n}\n\nfunction extractResourceQuery(message) {\n const text = safeUserText(message)\n .replace(/请|請|帮我|幫我|麻烦|麻煩|可以|能不能|需要|我要|想要/g, ' ')\n .replace(/下载|下載|查找|寻找|尋找|找一个|找一個|找|资源|資源|并|並|然后|然後|用于|用於|全球法布施|加入功课本|加入功課本/g, ' ')\n .replace(/\\s+/g, ' ')\n .trim();\n return text || safeUserText(message);\n}\n\nfunction resourceContextMessage(skillResult) {\n if (!skillResult) return '';\n const lines = [\n '本轮资源检索/下载已经由大乘 App 后端完成。',\n '不要再次调用 search_dharma_resources、download_dharma_resource 或其他资源 MCP 工具;请直接基于以下结果回答用户。',\n `后端已调用 skill: ${skillResult.skillName}`,\n `执行目标: ${skillResult.query}`,\n ];\n if (skillResult.selected) {\n lines.push(`候选资源: ${skillResult.selected.title}`);\n lines.push(`来源: ${skillResult.selected.sourceName}`);\n lines.push(`资源地址: ${skillResult.selected.url}`);\n }\n if (skillResult.downloaded) {\n lines.push(`下载标题: ${skillResult.downloaded.title}`);\n lines.push(`下载文件: ${skillResult.downloaded.fileName}`);\n const text = safeUserText(skillResult.downloaded.contentText);\n if (text) {\n lines.push('下载正文摘录:');\n lines.push(text.slice(0, 8000));\n }\n }\n return lines.join('\\n');\n}\n\nfunction determineClientAction(skillResult, message) {\n if (skillResult && skillResult.downloaded) {\n const text = safeUserText(message);\n if (/法布施|全球法布施|分享/.test(text)) {\n return {\n type: 'prepare_dharma_share_text',\n title: skillResult.downloaded.title,\n text: skillResult.downloaded.contentText || '',\n };\n }\n }\n return null;\n}\n\nasync function runResourceFinderSkill({ message, user, onStep }) {\n if (!looksLikeResourceTask(message)) return null;\n\n const skillName = 'resource-finder-downloader';\n const query = extractResourceQuery(message);\n const audioTask = looksLikeAudioQuery(message) || looksLikeAudioQuery(query);\n onStep?.({\n type: 'skill.step',\n skillName,\n stage: 'start',\n title: '调用资源查找下载 skill',\n message: `识别到资源任务: ${query}`,\n });\n\n onStep?.({\n type: 'skill.step',\n skillName,\n stage: 'search',\n title: '搜索可分享资源',\n message: audioTask\n ? '优先检索公开可访问音频资源,失败时再回落到佛典正文资源。'\n : '优先检索 CBETA 佛典正文接口,搜索索引不可用时使用内置经典映射。',\n });\n const candidates = await findResourceCandidates(query, 8);\n if (candidates.length === 0) {\n onStep?.({\n type: 'skill.step',\n skillName,\n stage: 'empty',\n title: '没有找到可下载资源',\n message: '后端没有找到明确可下载且可分享的资源。',\n });\n return {\n skillName,\n query,\n candidates: [],\n selected: null,\n downloaded: null,\n };\n }\n\n const selected = candidates[0];\n onStep?.({\n type: 'skill.step',\n skillName,\n stage: 'select',\n title: '选择可信候选资源',\n message: `${selected.title} / ${selected.sourceName}`,\n });\n\n onStep?.({\n type: 'skill.step',\n skillName,\n stage: 'download',\n title: '下载并提取资源',\n message: selected.url,\n });\n const cbeta = parseCbetaResource(selected);\n const internetArchive = parseInternetArchiveResource(selected);\n const downloaded = cbeta\n ? await downloadCbetaResource({ ...selected, ...cbeta })\n : internetArchive\n ? await downloadInternetArchiveAudio({ ...selected, ...internetArchive })\n : await downloadWebResource(selected);\n const persisted = persistDownloadedResource(user, downloaded);\n\n onStep?.({\n type: 'skill.step',\n skillName,\n stage: 'done',\n title: '资源已准备完成',\n message: downloaded.fileName,\n });\n\n return {\n skillName,\n query,\n candidates,\n selected,\n downloaded: {\n ...downloaded,\n downloadId: persisted.id,\n },\n };\n}\n\napp.post(\n '/api/resources/search',\n asyncHandler(async (req, res) => {\n const query = safeUserText(req.body?.query);\n const limit = Math.min(Math.max(Number(req.body?.limit || 12), 1), 20);\n if (query.length < 2) {\n return jsonResponse(res, 400, { success: false, message: 'query is required' });\n }\n\n const items = await findResourceCandidates(query, limit);\n\n jsonResponse(res, 200, {\n success: true,\n source: 'resource-index',\n items,\n });\n }),\n);\n\nfunction parseCbetaResource(input) {\n const work = safeUserText(input.work);\n const juan = Number(input.juan || 1);\n if (work) return { work, juan: Number.isFinite(juan) ? juan : 1 };\n\n const url = safeUserText(input.url);\n const match = /^cbeta:([^:]+):(\\d+)$/i.exec(url);\n if (!match) return null;\n return {\n work: match[1].toUpperCase(),\n juan: Number(match[2] || 1),\n };\n}\n\nfunction parseInternetArchiveResource(input) {\n const url = safeUserText(input.url);\n const match = /^ia:([^:]+)$/i.exec(url);\n const identifier = match?.[1] || safeUserText(input.identifier);\n if (!identifier) return null;\n return { identifier };\n}\n\nfunction safeFileName(value) {\n const normalized = String(value || 'dharma-resource')\n .replace(/[\\\\/:*?\"<>|]+/g, '-')\n .replace(/\\s+/g, '-')\n .replace(/-+/g, '-')\n .replace(/^-|-$/g, '')\n .slice(0, 80);\n return normalized || 'dharma-resource';\n}\n\nfunction extractCbetaHtml(data) {\n const first = Array.isArray(data?.results) ? data.results[0] : null;\n if (typeof first === 'string') return first;\n if (first && typeof first === 'object' && typeof first.html === 'string') return first.html;\n if (typeof data?.html === 'string') return data.html;\n return '';\n}\n\nasync function downloadCbetaResource(resource) {\n const url = buildUrl(cbetaApiRoot, 'juans', {\n work: resource.work,\n juan: resource.juan,\n work_info: 1,\n toc: 1,\n });\n const data = await fetchJson(url);\n const workInfo = data.work_info || {};\n const title = safeUserText(workInfo.title) || safeUserText(resource.title) || resource.work;\n const html = extractCbetaHtml(data);\n const text = cleanExtractedText(stripHtml(html));\n if (!text) throw new Error('CBETA resource returned empty content');\n return {\n title,\n sourceName: 'CBETA 佛典',\n url: `cbeta:${resource.work}:${resource.juan}`,\n contentText: text.slice(0, maxResourceTextChars),\n fileName: `${resource.work}_${resource.juan}_${safeFileName(title)}.txt`,\n };\n}\n\nasync function downloadInternetArchiveAudio(resource) {\n const identifier = safeUserText(resource.identifier);\n if (!identifier) throw new Error('Internet Archive identifier is required');\n const metadata = await fetchJson(`https://archive.org/metadata/${encodeURIComponent(identifier)}`);\n const title = safeUserText(metadata?.metadata?.title) || safeUserText(resource.title) || identifier;\n const files = Array.isArray(metadata?.files) ? metadata.files : [];\n const audioFile = files.find((file) => {\n const name = safeUserText(file?.name);\n const format = safeUserText(file?.format).toLowerCase();\n return /\\.(mp3|m4a|ogg)$/i.test(name) || format.includes('mp3');\n });\n if (!audioFile?.name) {\n throw new Error('Internet Archive audio item has no downloadable audio file');\n }\n const sourceFileName = audioFile.name;\n const fileUrl = `https://archive.org/download/${encodeURIComponent(identifier)}/${sourceFileName\n .split('/')\n .map((part) => encodeURIComponent(part))\n .join('/')}`;\n const response = await fetch(fileUrl, {\n headers: {\n Accept: 'audio/mpeg, audio/*;q=0.9, */*;q=0.5',\n 'User-Agent': 'DachengResourceDownloader/1.0 (+https://ombhrum.com)',\n },\n signal: AbortSignal.timeout(60_000),\n });\n if (!response.ok) {\n throw new Error(`Internet Archive audio download failed: ${response.status}`);\n }\n const binaryContent = Buffer.from(await response.arrayBuffer());\n if (!binaryContent.length) throw new Error('Internet Archive audio returned empty content');\n const sizeMb = (binaryContent.length / 1024 / 1024).toFixed(1);\n return {\n title,\n sourceName: 'Internet Archive 音频',\n url: fileUrl,\n contentText: `已下载音频资源: ${title}\\n文件名: ${sourceFileName}\\n大小: ${sizeMb} MB\\n来源: ${fileUrl}`,\n binaryContent,\n fileName: safeFileName(sourceFileName),\n };\n}\n\nasync function downloadWebResource(input) {\n const url = safeUserText(input.url);\n const parsed = new URL(url);\n if (!['http:', 'https:'].includes(parsed.protocol)) {\n throw new Error('Only http/https resources can be downloaded');\n }\n\n const response = await fetch(parsed, {\n headers: {\n Accept: 'text/html, text/plain, application/xhtml+xml, application/xml;q=0.8, */*;q=0.5',\n 'User-Agent': 'DachengResourceDownloader/1.0 (+https://ombhrum.com)',\n },\n signal: AbortSignal.timeout(25_000),\n });\n if (!response.ok) throw new Error(`Resource download failed: ${response.status}`);\n const contentType = response.headers.get('content-type') || '';\n const body = await response.text();\n const title = safeUserText(input.title) || parsed.hostname;\n const contentText = contentType.includes('html') ? cleanExtractedText(stripHtml(body)) : body.trim();\n if (!contentText) throw new Error('Resource returned empty content');\n return {\n title,\n sourceName: safeUserText(input.sourceName) || parsed.hostname,\n url,\n contentText: contentText.slice(0, maxResourceTextChars),\n fileName: `${safeFileName(title)}.txt`,\n };\n}\n\nfunction persistDownloadedResource(user, content) {\n const id = crypto.randomUUID();\n const fileName = `${id}-${safeFileName(content.fileName)}`;\n const filePath = path.join(resourcesDir, fileName);\n if (content.binaryContent) {\n fs.writeFileSync(filePath, content.binaryContent);\n } else {\n fs.writeFileSync(filePath, content.contentText, 'utf8');\n }\n statements.insertResourceDownload.run({\n id,\n userId: user.userId,\n title: content.title,\n sourceName: content.sourceName,\n sourceUrl: content.url,\n fileName: content.fileName,\n filePath,\n createdAt: nowIso(),\n });\n return { id, fileName, filePath };\n}\n\nfunction createLibreChatMcpServer() {\n const server = new McpServer({\n name: 'dacheng-ai-tools',\n version: '0.1.0',\n });\n\n server.registerTool(\n 'search_dharma_resources',\n {\n title: 'Search dharma resources',\n description:\n 'Search for legally shareable Buddhist scripture, text, web, or audio resources that can be prepared for Fabushi sharing.',\n inputSchema: {\n query: z.string().min(2).max(200),\n limit: z.number().int().min(1).max(12).optional(),\n },\n outputSchema: {\n query: z.string(),\n items: z.array(\n z.object({\n id: z.string(),\n title: z.string(),\n sourceName: z.string(),\n url: z.string(),\n snippet: z.string(),\n resourceType: z.string(),\n work: z.string().optional(),\n juan: z.number().optional(),\n }),\n ),\n },\n annotations: {\n readOnlyHint: true,\n destructiveHint: false,\n openWorldHint: true,\n },\n },\n async ({ query, limit }) => {\n const items = await findResourceCandidates(query, limit ?? 8);\n return textToolResult(`Found ${items.length} candidate resources for \"${query}\".`, {\n query,\n items,\n });\n },\n );\n\n server.registerTool(\n 'download_dharma_resource',\n {\n title: 'Download dharma resource',\n description:\n 'Download and extract a selected resource. Use the url/work/juan returned by search_dharma_resources whenever possible.',\n inputSchema: {\n url: z.string().min(1).max(1000),\n title: z.string().max(300).optional(),\n sourceName: z.string().max(120).optional(),\n work: z.string().max(40).optional(),\n juan: z.number().int().min(1).optional(),\n identifier: z.string().max(300).optional(),\n },\n outputSchema: {\n title: z.string(),\n sourceName: z.string(),\n url: z.string(),\n fileName: z.string(),\n downloadId: z.string(),\n contentText: z.string(),\n },\n annotations: {\n readOnlyHint: false,\n destructiveHint: false,\n openWorldHint: true,\n },\n },\n async (input) => {\n const user = { userId: 'librechat:mcp' };\n const cbeta = parseCbetaResource(input);\n const internetArchive = parseInternetArchiveResource(input);\n const content = cbeta\n ? await downloadCbetaResource({ ...input, ...cbeta })\n : internetArchive\n ? await downloadInternetArchiveAudio({ ...input, ...internetArchive })\n : await downloadWebResource(input);\n const persisted = persistDownloadedResource(user, content);\n const { binaryContent: _binaryContent, ...publicContent } = content;\n return textToolResult(`Downloaded ${publicContent.title} as ${publicContent.fileName}.`, {\n ...publicContent,\n downloadId: persisted.id,\n });\n },\n );\n\n server.registerTool(\n 'prepare_dharma_share_text',\n {\n title: 'Prepare dharma sharing text',\n description:\n 'Prepare a text payload for the Fabushi app to confirm and add to global dharma sharing. This returns a clientAction; it does not start sending.',\n inputSchema: {\n title: z.string().min(1).max(120),\n text: z.string().min(1).max(20000),\n },\n outputSchema: {\n clientAction: z.object({\n type: z.literal('prepare_dharma_share_text'),\n title: z.string(),\n text: z.string(),\n }),\n },\n annotations: {\n readOnlyHint: false,\n destructiveHint: false,\n openWorldHint: false,\n },\n },\n async ({ title, text }) =>\n textToolResult(`Prepared \"${title}\" for user confirmation in the Fabushi app.`, {\n clientAction: {\n type: 'prepare_dharma_share_text',\n title: safeUserText(title).slice(0, 120),\n text: String(text || '').trim().slice(0, 20000),\n },\n }),\n );\n\n server.registerTool(\n 'prepare_practice_book_item',\n {\n title: 'Prepare practice book item',\n description:\n 'Prepare a scripture, chant, or practice item for the Fabushi app practice book. This returns a clientAction; it does not mutate the app by itself.',\n inputSchema: {\n title: z.string().min(1).max(120),\n sourceName: z.string().max(120).optional(),\n text: z.string().min(1).max(20000),\n },\n outputSchema: {\n clientAction: z.object({\n type: z.literal('prepare_practice_book_item'),\n title: z.string(),\n sourceName: z.string(),\n text: z.string(),\n }),\n },\n annotations: {\n readOnlyHint: false,\n destructiveHint: false,\n openWorldHint: false,\n },\n },\n async ({ title, sourceName, text }) =>\n textToolResult(`Prepared \"${title}\" for the Fabushi practice book confirmation flow.`, {\n clientAction: {\n type: 'prepare_practice_book_item',\n title: safeUserText(title).slice(0, 120),\n sourceName: safeUserText(sourceName || '大乘 AI').slice(0, 120),\n text: String(text || '').trim().slice(0, 20000),\n },\n }),\n );\n\n return server;\n}\n\nasync function handleMcpRequest(req, res) {\n const server = createLibreChatMcpServer();\n const transport = new StreamableHTTPServerTransport({\n sessionIdGenerator: undefined,\n enableJsonResponse: true,\n });\n\n res.on('close', () => {\n transport.close();\n server.close();\n });\n\n await server.connect(transport);\n await transport.handleRequest(req, res, req.body);\n}\n\napp.all('/mcp', async (req, res) => {\n try {\n await handleMcpRequest(req, res);\n } catch (error) {\n logger.error({ error: error.stack || String(error) }, 'MCP request failed');\n if (!res.headersSent) {\n res.status(500).json({\n error: 'MCP request failed',\n message: publicAiErrorMessage(error),\n });\n }\n }\n});\n\napp.post(\n '/api/resources/download',\n asyncHandler(async (req, res) => {\n const user = await resolveUser(req, req.body || {});\n const cbeta = parseCbetaResource(req.body || {});\n const internetArchive = parseInternetArchiveResource(req.body || {});\n const content = cbeta\n ? await downloadCbetaResource({ ...req.body, ...cbeta })\n : internetArchive\n ? await downloadInternetArchiveAudio({ ...req.body, ...internetArchive })\n : await downloadWebResource(req.body || {});\n const persisted = persistDownloadedResource(user, content);\n const { binaryContent: _binaryContent, ...publicContent } = content;\n jsonResponse(res, 200, {\n success: true,\n downloadId: persisted.id,\n ...publicContent,\n });\n }),\n);\n\napp.post(\n '/api/codex/resource-task',\n asyncHandler(async (req, res) => {\n if (process.env.ENABLE_CODEX_SDK !== 'true') {\n return jsonResponse(res, 403, {\n success: false,\n message: 'Codex SDK task runner is disabled. Set ENABLE_CODEX_SDK=true on the server to enable it.',\n });\n }\n const prompt = safeUserText(req.body?.prompt);\n if (!prompt) {\n return jsonResponse(res, 400, { success: false, message: 'prompt is required' });\n }\n const { Codex } = await import('@openai/codex-sdk');\n const codexRuntime = createCodexDeepSeekRuntime();\n const codex = new Codex(codexRuntime.enabled ? codexRuntime.options : {});\n const thread = codex.startThread(\n codexRuntime.enabled\n ? codexRuntime.threadOptions\n : { skipGitRepoCheck: true, networkAccessEnabled: true },\n );\n const result = await thread.run(prompt);\n jsonResponse(res, 200, {\n success: true,\n threadId: thread.id,\n provider: codexRuntime.provider,\n model: deepseekModel,\n result,\n });\n }),\n);\n\napp.use((error, _req, res, _next) => {\n const status = Number(error.statusCode || error.status || 500);\n const safeStatus = status >= 400 && status < 600 ? status : 500;\n logger.error({ error: error.stack || String(error) }, 'request failed');\n jsonResponse(res, safeStatus, {\n success: false,\n message: error.message || 'Internal Server Error',\n details: error.details,\n });\n});\n\nconst port = Number(process.env.PORT || 8788);\napp.listen(port, '0.0.0.0', () => {\n logger.info(\n {\n port,\n dbPath,\n dataDir,\n deepseekModel,\n cbetaApiRoot,\n },\n 'Dacheng AI backend started',\n );\n});\n"}, "eabfd4ea3e2cd4625bf4eeec4b45947c3f2f00a3": {"t": 1785757972.430743, "n": 145304, "h": "dd107e71463a37d8", "c": "import crypto from 'node:crypto';\nimport fs from 'node:fs';\nimport path from 'node:path';\nimport process from 'node:process';\nimport { fileURLToPath } from 'node:url';\n\nimport { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';\nimport { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';\nimport Database from 'better-sqlite3';\nimport cors from 'cors';\nimport dotenv from 'dotenv';\nimport express from 'express';\nimport rateLimit from 'express-rate-limit';\nimport helmet from 'helmet';\nimport pino from 'pino';\nimport { z } from 'zod';\n\ndotenv.config();\n\nconst __dirname = path.dirname(fileURLToPath(import.meta.url));\nconst rootDir = path.resolve(__dirname, '..');\nconst dataDir = process.env.DATA_DIR || path.join(rootDir, 'data');\nconst resourcesDir = path.join(dataDir, 'resources');\nconst miniAppsDir = path.join(dataDir, 'miniapps');\nconst miniAppsStorePath = path.join(miniAppsDir, 'sandbox-miniapps.json');\nconst openClawRuntimeUpdatesDir =\n process.env.OPENCLAW_RUNTIME_UPDATES_DIR || path.join(dataDir, 'openclaw-runtime');\nconst openClawRuntimeEnableMacosUpdates =\n process.env.OPENCLAW_RUNTIME_ENABLE_MACOS_UPDATES === 'true';\nconst codexHomeDir = process.env.CODEX_HOME || path.join(dataDir, 'codex-home');\nconst codexTempDir = process.env.CODEX_TMPDIR || path.join(dataDir, 'codex-tmp');\nconst codexRuntimeDir = process.env.XDG_RUNTIME_DIR || path.join(dataDir, 'codex-runtime');\nconst dbPath = process.env.SQLITE_PATH || path.join(dataDir, 'dacheng-ai.sqlite');\n\nfs.mkdirSync(resourcesDir, { recursive: true });\nfs.mkdirSync(miniAppsDir, { recursive: true });\nfs.mkdirSync(openClawRuntimeUpdatesDir, { recursive: true });\nfs.mkdirSync(codexHomeDir, { recursive: true });\nfs.mkdirSync(codexTempDir, { recursive: true });\nfs.mkdirSync(codexRuntimeDir, { recursive: true });\n\nconst logger = pino({\n level: process.env.LOG_LEVEL || 'info',\n});\n\nconst app = express();\napp.disable('x-powered-by');\napp.set('trust proxy', Number(process.env.TRUST_PROXY_HOPS || 1));\napp.use(helmet());\nconst corsOrigin =\n !process.env.CORS_ORIGIN || process.env.CORS_ORIGIN.trim() === '*'\n ? true\n : process.env.CORS_ORIGIN.split(',').map((item) => item.trim()).filter(Boolean);\napp.use(cors({ origin: corsOrigin }));\napp.use(express.json({ limit: '1mb' }));\napp.use(\n rateLimit({\n windowMs: 60_000,\n limit: Number(process.env.RATE_LIMIT_PER_MINUTE || 90),\n standardHeaders: true,\n legacyHeaders: false,\n }),\n);\n\nconst db = new Database(dbPath);\ndb.pragma('journal_mode = WAL');\ndb.pragma('foreign_keys = ON');\n\ndb.exec(`\n CREATE TABLE IF NOT EXISTS conversations (\n id TEXT PRIMARY KEY,\n user_id TEXT NOT NULL,\n title TEXT NOT NULL,\n provider TEXT NOT NULL,\n model TEXT NOT NULL,\n created_at TEXT NOT NULL,\n updated_at TEXT NOT NULL\n );\n\n CREATE INDEX IF NOT EXISTS idx_conversations_user_updated\n ON conversations (user_id, updated_at DESC);\n\n CREATE TABLE IF NOT EXISTS messages (\n id TEXT PRIMARY KEY,\n conversation_id TEXT NOT NULL,\n role TEXT NOT NULL,\n content TEXT NOT NULL,\n prompt_tokens INTEGER NOT NULL DEFAULT 0,\n completion_tokens INTEGER NOT NULL DEFAULT 0,\n total_tokens INTEGER NOT NULL DEFAULT 0,\n created_at TEXT NOT NULL,\n FOREIGN KEY (conversation_id) REFERENCES conversations(id) ON DELETE CASCADE\n );\n\n CREATE INDEX IF NOT EXISTS idx_messages_conversation_created\n ON messages (conversation_id, created_at ASC);\n\n CREATE TABLE IF NOT EXISTS usage_months (\n user_id TEXT NOT NULL,\n month TEXT NOT NULL,\n total_tokens INTEGER NOT NULL DEFAULT 0,\n updated_at TEXT NOT NULL,\n PRIMARY KEY (user_id, month)\n );\n\n CREATE TABLE IF NOT EXISTS resource_downloads (\n id TEXT PRIMARY KEY,\n user_id TEXT NOT NULL,\n title TEXT NOT NULL,\n source_name TEXT NOT NULL,\n source_url TEXT NOT NULL,\n file_name TEXT NOT NULL,\n file_path TEXT NOT NULL,\n created_at TEXT NOT NULL\n );\n\n CREATE TABLE IF NOT EXISTS agent_runs (\n id TEXT PRIMARY KEY,\n user_id TEXT NOT NULL,\n conversation_id TEXT NOT NULL,\n message_id TEXT NOT NULL,\n openclaw_run_id TEXT,\n openclaw_session_key TEXT NOT NULL,\n status TEXT NOT NULL,\n mode TEXT NOT NULL,\n provider TEXT NOT NULL,\n model TEXT,\n input_tokens INTEGER NOT NULL DEFAULT 0,\n output_tokens INTEGER NOT NULL DEFAULT 0,\n tool_call_count INTEGER NOT NULL DEFAULT 0,\n cost_microusd INTEGER NOT NULL DEFAULT 0,\n started_at TEXT NOT NULL,\n completed_at TEXT,\n failed_at TEXT,\n error_code TEXT,\n error_message TEXT\n );\n\n CREATE INDEX IF NOT EXISTS idx_agent_runs_user_started\n ON agent_runs (user_id, started_at DESC);\n\n CREATE INDEX IF NOT EXISTS idx_agent_runs_status_started\n ON agent_runs (status, started_at DESC);\n\n CREATE TABLE IF NOT EXISTS agent_message_feedback (\n id TEXT PRIMARY KEY,\n message_id TEXT NOT NULL,\n user_id TEXT NOT NULL,\n rating TEXT NOT NULL,\n reason TEXT,\n comment TEXT,\n created_at TEXT NOT NULL\n );\n`);\n\nconst statements = {\n insertConversation: db.prepare(`\n INSERT INTO conversations (id, user_id, title, provider, model, created_at, updated_at)\n VALUES (@id, @userId, @title, @provider, @model, @createdAt, @updatedAt)\n `),\n updateConversation: db.prepare(`\n UPDATE conversations\n SET title = @title, updated_at = @updatedAt\n WHERE id = @id AND user_id = @userId\n `),\n getConversation: db.prepare(`\n SELECT * FROM conversations WHERE id = ? AND user_id = ? LIMIT 1\n `),\n listConversations: db.prepare(`\n SELECT id, title, updated_at AS updatedAt\n FROM conversations\n WHERE user_id = ?\n ORDER BY updated_at DESC\n LIMIT 30\n `),\n insertMessage: db.prepare(`\n INSERT INTO messages (\n id, conversation_id, role, content,\n prompt_tokens, completion_tokens, total_tokens, created_at\n )\n VALUES (\n @id, @conversationId, @role, @content,\n @promptTokens, @completionTokens, @totalTokens, @createdAt\n )\n `),\n listMessages: db.prepare(`\n SELECT role, content, prompt_tokens AS promptTokens,\n completion_tokens AS completionTokens, total_tokens AS totalTokens,\n created_at AS createdAt\n FROM messages\n WHERE conversation_id = ?\n ORDER BY created_at ASC\n `),\n getUsage: db.prepare(`\n SELECT total_tokens AS totalTokens FROM usage_months WHERE user_id = ? AND month = ?\n `),\n upsertUsage: db.prepare(`\n INSERT INTO usage_months (user_id, month, total_tokens, updated_at)\n VALUES (@userId, @month, @totalTokens, @updatedAt)\n ON CONFLICT(user_id, month)\n DO UPDATE SET total_tokens = usage_months.total_tokens + excluded.total_tokens,\n updated_at = excluded.updated_at\n `),\n insertResourceDownload: db.prepare(`\n INSERT INTO resource_downloads (\n id, user_id, title, source_name, source_url, file_name, file_path, created_at\n )\n VALUES (\n @id, @userId, @title, @sourceName, @sourceUrl, @fileName, @filePath, @createdAt\n )\n `),\n insertAgentRun: db.prepare(`\n INSERT INTO agent_runs (\n id, user_id, conversation_id, message_id, openclaw_run_id, openclaw_session_key,\n status, mode, provider, model, started_at\n )\n VALUES (\n @id, @userId, @conversationId, @messageId, @openClawRunId, @openClawSessionKey,\n @status, @mode, @provider, @model, @startedAt\n )\n `),\n getAgentRun: db.prepare(`\n SELECT * FROM agent_runs WHERE id = ? AND user_id = ? LIMIT 1\n `),\n updateAgentRunStatus: db.prepare(`\n UPDATE agent_runs\n SET status = @status,\n openclaw_run_id = COALESCE(@openClawRunId, openclaw_run_id),\n input_tokens = COALESCE(@inputTokens, input_tokens),\n output_tokens = COALESCE(@outputTokens, output_tokens),\n tool_call_count = COALESCE(@toolCallCount, tool_call_count),\n completed_at = @completedAt,\n failed_at = @failedAt,\n error_code = @errorCode,\n error_message = @errorMessage\n WHERE id = @id AND user_id = @userId\n `),\n insertAgentFeedback: db.prepare(`\n INSERT INTO agent_message_feedback (id, message_id, user_id, rating, reason, comment, created_at)\n VALUES (@id, @messageId, @userId, @rating, @reason, @comment, @createdAt)\n `),\n};\n\nconst deepseekApiKey = process.env.DEEPSEEK_API_KEY || '';\nconst deepseekBaseUrl = (process.env.DEEPSEEK_BASE_URL || 'https://api.deepseek.com').replace(/\\/+$/, '');\nconst deepseekModel = process.env.DEEPSEEK_MODEL || 'deepseek-chat';\nconst deepseekAvailableModels = Array.from(\n new Set(\n (process.env.DEEPSEEK_MODELS || `${deepseekModel},deepseek-chat,deepseek-reasoner`)\n .split(',')\n .map((item) => normalizeDeepSeekModelName(item))\n .filter(Boolean),\n ),\n);\nconst cbetaApiRoot = (process.env.CBETA_API_ROOT || 'https://api.ombhrum.com/api/cbeta').replace(/\\/+$/, '');\nconst fabushiApiBaseUrl = (process.env.FABUSHI_API_BASE_URL || 'https://api.ombhrum.com').replace(/\\/+$/, '');\nconst memberMonthlyLimit = Number(process.env.MEMBER_MONTHLY_TOKEN_LIMIT || 1_000_000);\nconst freeMonthlyLimit = Number(process.env.FREE_MONTHLY_TOKEN_LIMIT || 1_000);\nconst requireMemberForAi = process.env.REQUIRE_MEMBER_FOR_AI === 'true';\nconst deepseekMaxCompletionTokens = Math.max(1, Number(process.env.DEEPSEEK_MAX_COMPLETION_TOKENS || 1400));\nconst maxResourceTextChars = Number(process.env.MAX_RESOURCE_TEXT_CHARS || 80_000);\nconst openClawRuntimeManifestPath =\n process.env.OPENCLAW_RUNTIME_MANIFEST_PATH || path.join(openClawRuntimeUpdatesDir, 'manifest.json');\nconst openClawRuntimePublicBaseUrl = (process.env.OPENCLAW_RUNTIME_PUBLIC_BASE_URL || '').replace(/\\/+$/, '');\nconst enableLibreChatAgentChat = process.env.ENABLE_LIBRECHAT_AGENT_CHAT === 'true';\nconst libreChatAgentsBaseUrl = (\n process.env.LIBRECHAT_AGENTS_BASE_URL || 'http://127.0.0.1:3080/api/agents/v1'\n).replace(/\\/+$/, '');\nconst libreChatAgentApiKey = process.env.LIBRECHAT_AGENT_API_KEY || '';\nconst libreChatAgentId = process.env.LIBRECHAT_AGENT_ID || '';\nconst enableCodexSdkChat = process.env.ENABLE_CODEX_SDK_CHAT === 'true';\nconst enableOpenClawAgentChat = process.env.ENABLE_OPENCLAW_AGENT_CHAT === 'true';\nconst openClawGatewayUrl = (process.env.OPENCLAW_GATEWAY_URL || 'http://127.0.0.1:18789').replace(/\\/+$/, '');\nconst openClawGatewayToken = process.env.OPENCLAW_GATEWAY_TOKEN || '';\nconst openClawAgentId = process.env.OPENCLAW_AGENT_ID || 'fabushi-public-agent';\nconst openClawRunsEndpoint = process.env.OPENCLAW_RUNS_ENDPOINT || '';\nconst codexDeepSeekProviderId = 'deepseek-chat-completions';\nconst codexResponsesBaseUrl = (\n process.env.CODEX_DEEPSEEK_RESPONSES_BASE_URL ||\n `http://127.0.0.1:${process.env.PORT || 8788}/codex-deepseek/v1`\n).replace(/\\/+$/, '');\n\nconst systemPrompt = [\n '你是“大乘”App 的 AI 助手。',\n '你的核心任务是帮助用户查找、整理、理解并全球法布施合法可分享的佛法资源。',\n '回答要庄重、简洁、可执行;涉及经典、仪轨或资源时,提醒用户尊重版权、来源和当地法规。',\n '如果用户想找资源、下载经文、寻找音频或准备可分享资料,你必须先在后端自动执行搜索、验证和下载步骤;不要要求用户去前端 + 菜单手动查找。',\n '你可以把执行进度总结给用户,但不要输出隐藏推理原文。',\n].join('\\n');\n\nfunction nowIso() {\n return new Date().toISOString();\n}\n\nfunction monthKey(date = new Date()) {\n return `${date.getUTCFullYear()}-${String(date.getUTCMonth() + 1).padStart(2, '0')}`;\n}\n\nfunction jsonResponse(res, status, payload) {\n res.status(status).json(payload);\n}\n\nasync function readJsonFile(filePath) {\n const raw = await fs.promises.readFile(filePath, 'utf8');\n return JSON.parse(raw);\n}\n\nfunction requestOrigin(req) {\n if (openClawRuntimePublicBaseUrl) return openClawRuntimePublicBaseUrl;\n const proto = req.get('X-Forwarded-Proto') || req.protocol || 'https';\n const host = req.get('X-Forwarded-Host') || req.get('Host');\n return host ? `${proto}://${host}` : '';\n}\n\nfunction absolutizeOpenClawRuntimeUrl(req, value) {\n const text = readText(value);\n if (!text) return '';\n if (/^https?:\\/\\//i.test(text)) return text;\n const normalized = text.startsWith('/')\n ? text\n : `/api/openclaw/runtime/files/${text.replace(/^\\/+/, '')}`;\n const origin = requestOrigin(req);\n return origin ? new URL(normalized, `${origin}/`).toString() : normalized;\n}\n\nfunction textToolResult(text, structuredContent = {}) {\n return {\n structuredContent,\n content: [{ type: 'text', text }],\n };\n}\n\nfunction sseHeaders(res) {\n res.status(200);\n res.setHeader('Content-Type', 'text/event-stream; charset=utf-8');\n res.setHeader('Cache-Control', 'no-cache, no-transform');\n res.setHeader('Connection', 'keep-alive');\n res.setHeader('X-Accel-Buffering', 'no');\n if (typeof res.flushHeaders === 'function') res.flushHeaders();\n}\n\nfunction writeSse(res, event, payload) {\n res.write(`event: ${event}\\n`);\n res.write(`data: ${JSON.stringify(payload)}\\n\\n`);\n}\n\nfunction asyncHandler(fn) {\n return (req, res, next) => {\n Promise.resolve(fn(req, res, next)).catch(next);\n };\n}\n\nfunction bearerToken(req) {\n const delegated = readText(req.get('x-dacheng-auth-token'));\n if (\n delegated &&\n delegated !== 'DACHENG_AUTH_TOKEN' &&\n !delegated.startsWith('secretref-env:') &&\n delegated !== 'dacheng-openclaw-proxy'\n ) {\n return delegated;\n }\n const header = req.get('Authorization') || '';\n if (!header.startsWith('Bearer ')) return '';\n const token = header.slice('Bearer '.length).trim();\n return token === 'dacheng-openclaw-proxy' ? '' : token;\n}\n\nfunction sha256(value) {\n return crypto.createHash('sha256').update(value).digest('hex');\n}\n\nfunction safeUserText(value, fallback = '') {\n if (typeof value !== 'string') return fallback;\n return value.replace(/\\u0000/g, '').trim();\n}\n\nfunction estimateTokens(text) {\n const normalized = String(text || '').trim();\n if (!normalized) return 0;\n return Math.max(1, Math.ceil(normalized.length / 3));\n}\n\nfunction readText(value) {\n if (value === undefined || value === null) return '';\n return String(value).replace(/\\u0000/g, '').trim();\n}\n\nconst miniAppHostApiVersion = '1.4';\nconst miniAppHighRiskPermissions = new Set([\n 'auth.token',\n 'payments.alipay',\n 'payments.fudeGold',\n 'desktop.control',\n 'wifi.hotspot',\n 'local.loopback',\n 'fs.readWrite',\n 'shell.execute',\n 'browser.external',\n 'files.write',\n 'projects.write',\n 'openclaw.restart',\n]);\n\nfunction officialMiniAppRegistry(req) {\n const webBase = process.env.MINIAPP_WEB_BASE_URL || 'https://fabushi.ombhrum.com';\n const official = [\n {\n botId: 'bot.global-dharma',\n miniAppId: 'official.global-dharma',\n title: '全球法布施',\n subtitle: '地区、循环、场能都在对话框上方设置',\n initials: '法',\n icon: 'public',\n avatarColor: '#4CAF7A',\n kind: 'global_dharma',\n entryUrl: process.env.GLOBAL_DHARMA_MINIAPP_URL,\n greeting: '把要分享的文字、链接或素材发给我,我会按上方选择的地区与模式启动全球法布施。',\n inputHint: '输入要法布施的文字/链接,或点 + 添加素材',\n permissions: [\n 'app.context',\n 'bot.chat',\n 'auth.session',\n 'wallet.balance',\n 'payments.entitlement',\n 'payments.fudeGold',\n 'payments.alipay',\n 'dharma.share',\n 'files.pick',\n 'wifi.hotspot',\n 'openclaw.status',\n 'openclaw.chat',\n 'local.loopback',\n 'desktop.control',\n ],\n },\n {\n botId: 'bot.flashcards',\n miniAppId: 'official.flashcards',\n title: '背诵闪卡制作',\n subtitle: '随机挖空 / AI 制卡从顶部模式按钮选择',\n initials: '卡',\n icon: 'flashcards',\n avatarColor: '#7E57C2',\n kind: 'flashcards',\n entryUrl: process.env.FLASHCARDS_MINIAPP_URL,\n greeting: '粘贴经文、文章正文或链接即可制作背诵闪卡。制卡模式请在上方按钮切换。',\n inputHint: '粘贴链接或正文,发送后制作闪卡',\n permissions: ['app.context', 'bot.chat', 'flashcards.create', 'openclaw.status', 'openclaw.chat'],\n },\n {\n botId: 'bot.platform-publish',\n miniAppId: 'official.platform-publish',\n title: '法布施到平台',\n subtitle: '选择平台后生成发布草稿并打开入口',\n initials: '发',\n icon: 'publish',\n avatarColor: '#FF9F43',\n kind: 'platform_publish',\n entryUrl: process.env.PLATFORM_PUBLISH_MINIAPP_URL,\n greeting: '把要发布的正文或链接发给我,上方选择平台后,我会生成发布草稿并打开对应平台入口。',\n inputHint: '输入要发布到平台的正文/链接',\n permissions: [\n 'app.context',\n 'bot.chat',\n 'platform.publish',\n 'files.pick',\n 'fs.readWrite',\n 'shell.execute',\n 'browser.external',\n ],\n },\n {\n botId: 'bot.father',\n miniAppId: 'official.bot-father',\n title: '机器人之父',\n subtitle: '用对话生成个人沙箱小程序',\n initials: '父',\n icon: 'bot_father',\n avatarColor: '#3D8BFF',\n kind: 'bot_father',\n entryUrl: process.env.BOT_FATHER_MINIAPP_URL,\n greeting: '告诉我你想要的小程序,我会生成 manifest、界面和权限声明,并放进你的个人沙箱。',\n inputHint: '描述你想创建的小程序',\n permissions: ['app.context', 'bot.chat', 'miniapps.dev'],\n },\n ];\n return {\n bots: official.map((bot, index) => ({\n ...bot,\n destinationIndex: index,\n source: 'official',\n })),\n miniApps: official.map((bot) => ({\n miniAppId: bot.miniAppId,\n botId: bot.botId,\n title: bot.title,\n subtitle: bot.subtitle,\n entryUrl:\n bot.entryUrl ||\n `${webBase.replace(/\\/+$/, '')}/miniapps/${encodeURIComponent(bot.miniAppId)}`,\n version: '1.4.0',\n permissions: bot.permissions,\n surfaces: ['homePinned', 'chatPanel'],\n theme: 'telegramDark',\n signature: 'official',\n reviewStatus: 'trusted',\n source: 'official',\n })),\n };\n}\n\nasync function readMiniAppStore() {\n try {\n const raw = await fs.promises.readFile(miniAppsStorePath, 'utf8');\n const decoded = JSON.parse(raw);\n return {\n apps: Array.isArray(decoded.apps) ? decoded.apps : [],\n };\n } catch (error) {\n if (error?.code === 'ENOENT') return { apps: [] };\n throw error;\n }\n}\n\nasync function writeMiniAppStore(store) {\n await fs.promises.mkdir(miniAppsDir, { recursive: true });\n await fs.promises.writeFile(\n miniAppsStorePath,\n JSON.stringify({ apps: store.apps || [] }, null, 2),\n );\n}\n\nfunction miniAppEntryUrl(req, miniAppId) {\n const origin = requestOrigin(req) || '';\n const pathName = `/api/miniapps/dev/${encodeURIComponent(miniAppId)}/index.html`;\n return origin ? new URL(pathName, `${origin}/`).toString() : pathName;\n}\n\nfunction sanitizeMiniAppPermissions(value, { allowHighRisk = false } = {}) {\n const raw = Array.isArray(value) ? value : [];\n const normalized = raw\n .map((item) => readText(item).toLowerCase())\n .filter((item) => /^[a-z][a-z0-9_.:-]{0,64}$/.test(item));\n const deduped = [...new Set(['app.context', 'bot.chat', ...normalized])];\n return deduped.filter((permission) => allowHighRisk || !miniAppHighRiskPermissions.has(permission));\n}\n\nfunction miniAppScan(permissions, sourceHtml = '') {\n const highRisk = permissions.filter((permission) => miniAppHighRiskPermissions.has(permission));\n const blockedPatterns = [\n /authorization\\s*:\\s*['\"`]bearer/i,\n /desktop_control_bridge_token/i,\n /openclaw_gateway_token/i,\n /eval\\s*\\(/i,\n ];\n const blocked = blockedPatterns\n .filter((pattern) => pattern.test(sourceHtml))\n .map((pattern) => pattern.toString());\n return {\n passed: blocked.length === 0,\n highRiskPermissions: highRisk,\n blockedPatterns: blocked,\n };\n}\n\nfunction inferMiniAppPermissionsFromText(...values) {\n const text = values.map((value) => readText(value)).join('\\n').toLowerCase();\n const rules = [\n ['auth.session', [/auth\\.(getsession|requirelogin)/, /登录|用户信息|宿主账号/]],\n ['auth.token', [/auth\\.getaccesstoken/, /access token|访问 token/]],\n ['payments.alipay', [/payments\\.alipay/, /支付宝|支付|购买|订单/]],\n ['payments.entitlement', [/payments\\.checkentitlement|权益|解锁状态|购买状态/]],\n ['payments.fudeGold', [/payments\\.requestpayment|福德金|代币|钱包|余额/]],\n ['wallet.balance', [/wallet\\.getbalance|福德金|代币|钱包|余额/]],\n ['dharma.share', [/dharma\\./, /全球法布施|法布施发送/]],\n ['wifi.hotspot', [/wifihotspot\\./, /wifi|wi-fi|热点|本地场能/]],\n ['local.loopback', [/localloopback\\./, /localhost|127\\.0\\.0\\.1|本地回环|本地转经轮/]],\n ['flashcards.create', [/flashcards\\./, /闪卡|背诵卡|挖空/]],\n ['platform.publish', [/platformpublish\\./, /发布草稿|发布到平台|公众号|小红书|知乎/]],\n ['files.pick', [/files\\.pick/, /选择文件|上传文件|本地素材/]],\n ['fs.readWrite', [/fs\\.(writefile|readfile)/, /本地文件|写文件|读文件|文件处理/]],\n ['shell.execute', [/shell\\.execute/, /终端|命令行|shell|cli|playwright/]],\n ['browser.external', [/browser\\.open/, /浏览器|打开网页|打开平台/]],\n ['desktop.control', [/desktopcontrol\\./, /桌面控制|点击|键盘|鼠标/]],\n ['openclaw.chat', [/openclaw\\.chat/, /openclaw/]],\n ];\n return rules\n .filter(([, patterns]) => patterns.some((pattern) => pattern.test(text)))\n .map(([permission]) => permission);\n}\n\nfunction miniAppRecordToBot(record) {\n return {\n botId: record.botId,\n miniAppId: record.miniAppId,\n title: record.title,\n subtitle: record.subtitle,\n initials: record.initials || '小',\n icon: record.icon || 'code',\n avatarColor: record.avatarColor || '#3D8BFF',\n kind: 'third_party',\n greeting: record.greeting || '你好,我是这个小程序的机器人。',\n inputHint: record.inputHint || '输入消息,或打开小程序面板',\n permissions: record.permissions || ['app.context', 'bot.chat'],\n source: record.source || 'sandbox',\n };\n}\n\nfunction miniAppRecordToManifest(req, record) {\n return {\n miniAppId: record.miniAppId,\n botId: record.botId,\n title: record.title,\n subtitle: record.subtitle,\n entryUrl: miniAppEntryUrl(req, record.miniAppId),\n version: record.version || '0.0.1',\n permissions: record.permissions || ['app.context', 'bot.chat'],\n surfaces: record.surfaces || ['chatPanel'],\n theme: record.theme || 'telegramDark',\n signature: record.signature || `sandbox:${record.ownerId}`,\n reviewStatus: record.reviewStatus || 'sandbox',\n source: record.source || 'sandbox',\n };\n}\n\nfunction safeMiniAppId(value) {\n return readText(value).replace(/[^a-zA-Z0-9_.-]/g, '-').slice(0, 80);\n}\n\nfunction titleFromPromptForMiniApp(prompt) {\n const text = readText(prompt).replace(/\\s+/g, ' ');\n if (!text) return '个人小程序';\n return text.length <= 18 ? text : text.slice(0, 18);\n}\n\nfunction escapeHtml(value) {\n return String(value || '')\n .replace(/&/g, '&')\n .replace(/</g, '<')\n .replace(/>/g, '>')\n .replace(/\"/g, '"')\n .replace(/'/g, ''');\n}\n\nfunction generatedMiniAppHtml({ title, prompt }) {\n const safeTitle = escapeHtml(title);\n const safePrompt = escapeHtml(prompt);\n const promptJson = JSON.stringify(readText(prompt));\n return `<!doctype html>\n<html lang=\"zh-Hans\">\n<head>\n <meta charset=\"utf-8\" />\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n <title>${safeTitle}</title>\n <style>\n body { margin:0; font-family:-apple-system,BlinkMacSystemFont,\"Segoe UI\",sans-serif; background:#0f1722; color:#fff; }\n main { padding:20px; display:grid; gap:14px; }\n .card { border:1px solid #263445; background:#17212b; border-radius:16px; padding:16px; }\n button { border:0; border-radius:999px; padding:10px 14px; background:#3390ec; color:white; font-weight:800; }\n pre { white-space:pre-wrap; color:#9ec7ff; }\n </style>\n</head>\n<body>\n <main>\n <section class=\"card\">\n <h1>${safeTitle}</h1>\n <p>这是机器人之父根据你的描述生成的个人沙箱小程序。</p>\n <pre>${safePrompt}</pre>\n </section>\n <section class=\"card\">\n <button id=\"context\">读取宿主上下文</button>\n <button id=\"spec\">读取宿主能力</button>\n <button id=\"chat\">让机器人解释这个小程序</button>\n <pre id=\"output\">等待操作...</pre>\n </section>\n </main>\n <script>\n const out = document.getElementById('output');\n async function invoke(method, params) {\n if (!window.FabushiMiniApp) throw new Error('FabushiMiniApp host SDK not ready');\n const result = await window.FabushiMiniApp.invoke(method, params || {});\n out.textContent = JSON.stringify(result, null, 2);\n }\n document.getElementById('context').onclick = () => invoke('app.getContext');\n document.getElementById('spec').onclick = () => invoke('app.getHostApiSpec');\n const prompt = ${promptJson};\n document.getElementById('chat').onclick = () => invoke('bot.sendMessage', {\n message: '请简要说明这个小程序能做什么:' + prompt\n });\n const seenCommands = new Set();\n function deliverMiniAppCommand(detail) {\n if (!detail || typeof detail !== 'object') return;\n const commandKey = detail.commandId || detail.id || [detail.createdAt, detail.command, detail.rawText || detail.text].filter(Boolean).join(':');\n if (commandKey) {\n if (seenCommands.has(commandKey)) return;\n seenCommands.add(commandKey);\n }\n handleMiniAppCommand(detail);\n }\n async function handleMiniAppCommand(detail) {\n out.textContent = '收到后台命令:' + JSON.stringify(detail, null, 2);\n try {\n await invoke('bot.postMessage', {\n commandId: detail.commandId || detail.id,\n message: '「${safeTitle}」已收到后台命令:' + (detail.args || detail.text || '')\n });\n await invoke('bot.reportCommandResult', {\n commandId: detail.commandId || detail.id,\n status: 'completed',\n message: '「${safeTitle}」后台命令已处理。'\n });\n } catch (error) {\n out.textContent = String(error && error.message || error);\n }\n }\n window.addEventListener('fabushi-miniapp-command', (event) => {\n deliverMiniAppCommand(event.detail || {});\n });\n if (window.__fabushiLastMiniAppCommand) {\n (window.queueMicrotask || ((fn) => setTimeout(fn, 0)))(() => {\n deliverMiniAppCommand(window.__fabushiLastMiniAppCommand);\n });\n }\n </script>\n</body>\n</html>`;\n}\n\nfunction extractMiniAppHtml(value) {\n const text = readText(value);\n if (!text) return '';\n const fenced = text.match(/```(?:html)?\\s*([\\s\\S]*?)```/i);\n const candidate = (fenced ? fenced[1] : text).trim();\n if (!/<html[\\s>]/i.test(candidate) || !/<script[\\s>]/i.test(candidate)) return '';\n return candidate.startsWith('<!doctype') || candidate.startsWith('<html')\n ? candidate\n : `<!doctype html>\\n${candidate}`;\n}\n\nasync function generateBotFatherMiniAppHtml({ title, prompt, user }) {\n const fallback = generatedMiniAppHtml({ title, prompt });\n try {\n const result = await runAgentModel({\n user,\n mode: 'miniapp_generation',\n maxCompletionTokens: deepseekMaxCompletionTokens,\n messages: [\n {\n role: 'system',\n content: [\n '你是 Fabushi App 的小程序生成器。',\n '只输出一个完整静态 HTML 文件,不要 Markdown,不要解释。',\n '小程序只能通过 window.FabushiMiniApp.invoke(method, params) 调用宿主能力。',\n '先调用 app.getHostApiSpec 或 app.getCapabilities 判断宿主版本和已授权权限,再显示对应 UI。',\n '聊天后台调用协议:优先使用 window.FabushiMiniApp.bot.onCommand(\"/start\", handler) 或 bot.onAnyCommand(handler);如果手写监听 window 的 fabushi-miniapp-command 事件,注册后也要读取 window.__fabushiLastMiniAppCommand 并按 commandId 去重补处理。detail.command 默认为 /start,detail.args 是用户文本;处理完成后调用 bot.postMessage 或 bot.reportCommandResult 把进度/结果写回聊天框。',\n '标准能力:auth.getSession/auth.requireLogin 复用宿主登录;payments.requestPayment 使用福德金支付并由宿主登记权益;payments.checkEntitlement 查询商品是否已解锁;payments.alipay.pay/createOrder 仅作为宿主官方现金支付兜底;dharma.prepareContent/dharma.startGlobalSend 接入全球法布施;wifiHotspot.enable 接入本地场能热点;flashcards.createDeck 接入背诵闪卡;platformPublish.createDraft 接入发布草稿;fs.writeFile/fs.readFile、shell.execute、browser.open 用于本地文件处理、终端和浏览器。',\n '高危权限包括 auth.token、payments.alipay、payments.fudeGold、wifi.hotspot、local.loopback、fs.readWrite、shell.execute、browser.external、desktop.control;只有用户需求明确需要时才使用。',\n '禁止外链脚本、eval、Authorization token、桌面桥 token、OpenClaw token。',\n '不要调用 auth.getAccessToken,除非用户明确要求对接需要 token 的受信服务。',\n '优先实现一个可点击、可立即使用的小面板,包含 app.getContext 或 bot.sendMessage 示例。',\n ].join('\\n'),\n },\n {\n role: 'user',\n content: [\n `小程序标题:${title}`,\n `用户需求:${prompt}`,\n '请生成 HTML/CSS/JS 单文件。',\n ].join('\\n'),\n },\n ],\n });\n const sourceHtml = extractMiniAppHtml(result.message);\n if (!sourceHtml) {\n return {\n sourceHtml: fallback,\n generation: { provider: 'template', fallbackReason: 'ai_output_not_html' },\n };\n }\n return {\n sourceHtml,\n generation: {\n provider: result.provider || 'ai',\n model: result.model || '',\n },\n };\n } catch (error) {\n return {\n sourceHtml: fallback,\n generation: {\n provider: 'template',\n fallbackReason: readText(error?.message || error),\n },\n };\n }\n}\n\nfunction clientContextMessage(client) {\n if (!client || typeof client !== 'object') return '';\n const selectedModel = readText(client.selectedModel || client.model);\n const project = client.project && typeof client.project === 'object' ? client.project : {};\n const projectName = readText(project.name);\n const projectPath = readText(project.path);\n const lines = ['桌面端上下文:'];\n if (selectedModel) lines.push(`用户选择的 DeepSeek 模型:${normalizeDeepSeekModelName(selectedModel)}`);\n if (projectPath) {\n lines.push(`当前项目名称:${projectName || projectPath}`);\n lines.push(`当前项目路径:${projectPath}`);\n lines.push('如果本轮任务涉及读取、修改、运行、分析或整理项目,请优先围绕当前项目路径工作;不要擅自操作其他目录。');\n }\n return lines.length > 1 ? lines.join('\\n') : '';\n}\nfunction unwrapPayload(payload) {\n if (!payload || typeof payload !== 'object') return {};\n return payload.data && typeof payload.data === 'object' ? payload.data : payload;\n}\n\nfunction readFirstText(values) {\n for (const value of values) {\n const text = readText(value);\n if (text) return text;\n }\n return '';\n}\n\nfunction activeFlag(value) {\n if (value === true) return true;\n if (value === 1) return true;\n if (typeof value !== 'string') return false;\n return ['true', '1', 'yes', 'active'].includes(value.trim().toLowerCase());\n}\n\nfunction futureDate(value) {\n const text = readText(value);\n if (!text) return false;\n const timestamp = Date.parse(text);\n return Number.isFinite(timestamp) && timestamp > Date.now();\n}\n\nfunction normalizeMembershipPayload(payload) {\n if (!payload || typeof payload !== 'object') return false;\n const data = unwrapPayload(payload);\n const membership = data.membership && typeof data.membership === 'object' ? data.membership : data;\n const type = readText(membership.type || membership.membershipType || data.membershipType).toLowerCase();\n const status = readText(membership.status || data.status).toLowerCase();\n const expiry = membership.expiresAt ||\n membership.expires_at ||\n membership.membershipExpiry ||\n membership.membership_expires_at ||\n data.membershipExpiry ||\n data.membership_expires_at;\n const hasExplicitActiveFlag =\n membership.isActive !== undefined ||\n membership.active !== undefined ||\n membership.hasPremiumAccess !== undefined ||\n membership.hasPremiumMembership !== undefined ||\n membership.isPremium !== undefined;\n\n if (hasExplicitActiveFlag) {\n return Boolean(\n activeFlag(membership.isActive) ||\n activeFlag(membership.active) ||\n activeFlag(membership.hasPremiumAccess) ||\n activeFlag(membership.hasPremiumMembership) ||\n activeFlag(membership.isPremium),\n );\n }\n\n return Boolean(\n status === 'active' ||\n type === 'paid' ||\n type === 'premium' ||\n type === 'trial' ||\n futureDate(expiry),\n );\n}\n\nfunction extractRemoteAccount(payload) {\n const data = unwrapPayload(payload);\n const nestedUser = data.user && typeof data.user === 'object' ? data.user : {};\n const id = readFirstText([\n data.userId,\n data.user_id,\n data.userNo,\n data.user_no,\n data.id,\n nestedUser.userId,\n nestedUser.user_id,\n nestedUser.userNo,\n nestedUser.user_no,\n nestedUser.id,\n ]);\n const username = readFirstText([data.username, data.name, nestedUser.username, nestedUser.name]);\n const email = readFirstText([data.email, nestedUser.email]);\n const membership = data.membership && typeof data.membership === 'object' ? data.membership : null;\n return {\n authenticated: Boolean(id || username || email || membership || data.success === true),\n id,\n username,\n email,\n isMember: normalizeMembershipPayload(payload),\n membership,\n };\n}\n\nasync function resolveRemoteAccount(token) {\n const fallback = {\n authenticated: false,\n id: '',\n username: '',\n email: '',\n isMember: false,\n membership: null,\n };\n if (!token || !fabushiApiBaseUrl) return fallback;\n\n const endpoints = [\n '/api/stripe/membership-status',\n '/api/alipay/check-membership',\n '/api/auth/user-info',\n ];\n\n for (const endpoint of endpoints) {\n try {\n const response = await fetch(`${fabushiApiBaseUrl}${endpoint}`, {\n headers: {\n Accept: 'application/json',\n Authorization: `Bearer ${token}`,\n },\n signal: AbortSignal.timeout(8_000),\n });\n if (response.status === 401 || response.status === 403) return fallback;\n if (!response.ok) continue;\n const payload = await response.json();\n const account = extractRemoteAccount(payload);\n if (account.authenticated || account.isMember) {\n return {\n ...fallback,\n ...account,\n authenticated: true,\n };\n }\n } catch (error) {\n logger.debug({ endpoint, error: String(error) }, 'membership lookup skipped');\n }\n }\n\n return fallback;\n}\n\nasync function resolveUser(req, body = {}) {\n const token = bearerToken(req);\n const usernameHint = safeUserText(body.username || req.query.username);\n\n const testToken = process.env.TEST_ACCOUNT_TOKEN || 'dacheng-test-token-bypass-limits';\n if (token && token === testToken) {\n return {\n userId: 'user:test_account',\n username: 'TestAccount',\n tokenHash: sha256(token).slice(0, 24),\n isAuthenticated: true,\n isMember: true,\n isTestAccount: true,\n membership: { type: 'lifetime', active: true },\n };\n }\n\n const tokenHash = token ? sha256(token).slice(0, 24) : '';\n const anonymousHash = sha256(`${req.ip}|${req.get('User-Agent') || ''}`).slice(0, 24);\n const remoteAccount = await resolveRemoteAccount(token);\n const remoteIdentity = remoteAccount.id ||\n remoteAccount.username ||\n remoteAccount.email ||\n (remoteAccount.authenticated ? tokenHash : '');\n const userId = remoteAccount.authenticated\n ? `user:${remoteIdentity}`\n : tokenHash\n ? `token:${tokenHash}`\n : `anon:${anonymousHash}`;\n return {\n userId,\n username: remoteAccount.username || usernameHint,\n tokenHash,\n isAuthenticated: remoteAccount.authenticated,\n isMember: remoteAccount.isMember,\n membership: remoteAccount.membership,\n };\n}\n\nfunction usageFor(userId) {\n const month = monthKey();\n const row = statements.getUsage.get(userId, month);\n return {\n month,\n used: Number(row?.totalTokens || 0),\n };\n}\n\nfunction enforceTokenBudget(user, estimatedTokensForRequest) {\n if (user && user.isTestAccount) {\n return { limit: 999999999, usage: { month: monthKey(), used: 0 } };\n }\n if (requireMemberForAi && !user.isMember) {\n const error = new Error('大乘 AI 需要开通会员后使用');\n error.statusCode = 403;\n error.code = 'AI_MEMBERSHIP_REQUIRED';\n error.details = {\n requireMemberForAi,\n monthlyLimit: memberMonthlyLimit,\n remainingTokens: 0,\n };\n throw error;\n }\n\n const limit = user.isMember ? memberMonthlyLimit : freeMonthlyLimit;\n const usage = usageFor(user.userId);\n if (usage.used + estimatedTokensForRequest > limit) {\n const error = new Error('本月 AI token 额度已不足');\n error.statusCode = 429;\n error.code = 'USER_TOKEN_QUOTA_EXCEEDED';\n error.details = {\n monthlyLimit: limit,\n usedTokens: usage.used,\n remainingTokens: Math.max(0, limit - usage.used),\n };\n throw error;\n }\n return { limit, usage };\n}\n\nfunction completionBudgetFor(budget, estimatedPromptTokens) {\n const remainingAfterPrompt = budget.limit - budget.usage.used - estimatedPromptTokens;\n return Math.max(1, Math.min(deepseekMaxCompletionTokens, Math.floor(remainingAfterPrompt)));\n}\n\nfunction recordUsage(userId, totalTokens) {\n if (!totalTokens || totalTokens < 1) return;\n statements.upsertUsage.run({\n userId,\n month: monthKey(),\n totalTokens,\n updatedAt: nowIso(),\n });\n}\n\nfunction titleFromMessage(message) {\n const compact = message.replace(/\\s+/g, ' ').trim();\n if (!compact) return '新对话';\n return compact.length > 22 ? `${compact.slice(0, 22)}...` : compact;\n}\n\nfunction normalizeMessages(rows) {\n return rows.map((row) => ({\n role: row.role,\n content: row.content,\n }));\n}\n\nfunction normalizeMaxCompletionTokens(value) {\n const numeric = Number(value || deepseekMaxCompletionTokens);\n if (!Number.isFinite(numeric) || numeric < 1) return deepseekMaxCompletionTokens;\n return Math.max(1, Math.min(deepseekMaxCompletionTokens, Math.floor(numeric)));\n}\n\nfunction normalizeDeepSeekModelName(value) {\n const raw = readText(value || deepseekModel);\n if (!raw) return deepseekModel;\n const modelId = raw.includes('/') ? raw.split('/').pop() : raw;\n return modelId || deepseekModel;\n}\n\nasync function callDeepSeek(messages, options = {}) {\n if (!deepseekApiKey) {\n const error = new Error('DeepSeek API key is not configured');\n error.statusCode = 503;\n throw error;\n }\n\n const model = normalizeDeepSeekModelName(options.model);\n const response = await fetch(`${deepseekBaseUrl}/chat/completions`, {\n method: 'POST',\n headers: {\n Accept: 'application/json',\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${deepseekApiKey}`,\n },\n body: JSON.stringify({\n model,\n messages,\n temperature: 0.4,\n max_tokens: normalizeMaxCompletionTokens(options.maxCompletionTokens),\n stream: false,\n }),\n signal: AbortSignal.timeout(60_000),\n });\n\n const bodyText = await response.text();\n let payload;\n try {\n payload = bodyText ? JSON.parse(bodyText) : {};\n } catch {\n payload = { error: bodyText };\n }\n\n if (!response.ok) {\n const upstreamMessage = payload?.error?.message || payload?.message || '';\n const friendlyMessage =\n response.status === 402 || /insufficient\\s+balance/i.test(upstreamMessage)\n ? '大乘 AI 服务额度暂不可用,请联系管理员充值 DeepSeek 账户后重试。'\n : upstreamMessage || `DeepSeek request failed: ${response.status}`;\n const error = new Error(friendlyMessage);\n error.statusCode = response.status;\n if (response.status === 402 || /insufficient\\s+balance/i.test(upstreamMessage)) {\n error.code = 'DEEPSEEK_PROVIDER_QUOTA';\n }\n throw error;\n }\n\n const message = payload?.choices?.[0]?.message?.content?.trim();\n if (!message) {\n const error = new Error('DeepSeek returned an empty response');\n error.statusCode = 502;\n throw error;\n }\n\n return {\n message,\n model,\n usage: {\n promptTokens: Number(payload?.usage?.prompt_tokens || 0),\n completionTokens: Number(payload?.usage?.completion_tokens || 0),\n totalTokens: Number(payload?.usage?.total_tokens || 0),\n },\n };\n}\n\nasync function callDeepSeekStream(messages, callbacks = {}) {\n if (!deepseekApiKey) {\n const error = new Error('DeepSeek API key is not configured');\n error.statusCode = 503;\n throw error;\n }\n\n const model = normalizeDeepSeekModelName(callbacks.model);\n const response = await fetch(`${deepseekBaseUrl}/chat/completions`, {\n method: 'POST',\n headers: {\n Accept: 'text/event-stream',\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${deepseekApiKey}`,\n },\n body: JSON.stringify({\n model,\n messages,\n temperature: 0.4,\n max_tokens: normalizeMaxCompletionTokens(callbacks.maxCompletionTokens),\n stream: true,\n stream_options: { include_usage: true },\n }),\n signal: callbacks.signal || AbortSignal.timeout(90_000),\n });\n\n if (!response.ok) {\n const bodyText = await response.text();\n let payload;\n try {\n payload = bodyText ? JSON.parse(bodyText) : {};\n } catch {\n payload = { error: bodyText };\n }\n const upstreamMessage = payload?.error?.message || payload?.message || '';\n const friendlyMessage =\n response.status === 402 || /insufficient\\s+balance/i.test(upstreamMessage)\n ? '大乘 AI 服务额度暂不可用,请联系管理员充值 DeepSeek 账户后重试。'\n : upstreamMessage || `DeepSeek request failed: ${response.status}`;\n const error = new Error(friendlyMessage);\n error.statusCode = response.status;\n if (response.status === 402 || /insufficient\\s+balance/i.test(upstreamMessage)) {\n error.code = 'DEEPSEEK_PROVIDER_QUOTA';\n }\n throw error;\n }\n\n let buffer = '';\n let message = '';\n let usage = {\n promptTokens: 0,\n completionTokens: 0,\n totalTokens: 0,\n };\n const decoder = new TextDecoder();\n\n for await (const chunk of response.body) {\n buffer += decoder.decode(chunk, { stream: true });\n const lines = buffer.split(/\\r?\\n/);\n buffer = lines.pop() || '';\n\n for (const rawLine of lines) {\n const line = rawLine.trim();\n if (!line.startsWith('data:')) continue;\n const data = line.slice('data:'.length).trim();\n if (!data || data === '[DONE]') continue;\n\n let payload;\n try {\n payload = JSON.parse(data);\n } catch {\n continue;\n }\n\n const delta = payload?.choices?.[0]?.delta?.content || '';\n if (delta) {\n message += delta;\n callbacks.onToken?.(delta);\n }\n\n if (payload?.usage) {\n usage = {\n promptTokens: Number(payload.usage.prompt_tokens || usage.promptTokens || 0),\n completionTokens: Number(payload.usage.completion_tokens || usage.completionTokens || 0),\n totalTokens: Number(payload.usage.total_tokens || usage.totalTokens || 0),\n };\n }\n }\n }\n\n message = message.trim();\n if (!message) {\n const error = new Error('DeepSeek returned an empty response');\n error.statusCode = 502;\n throw error;\n }\n\n return { message, model, usage };\n}\n\nfunction createCodexDeepSeekRuntime() {\n if (!enableCodexSdkChat || !deepseekApiKey) {\n return {\n enabled: false,\n provider: 'deepseek-direct',\n reason: enableCodexSdkChat ? 'DeepSeek API key is not configured' : 'ENABLE_CODEX_SDK_CHAT is not true',\n };\n }\n\n return {\n enabled: true,\n provider: 'codex-sdk-deepseek',\n options: {\n apiKey: deepseekApiKey,\n baseUrl: deepseekBaseUrl,\n env: {\n ...process.env,\n DEEPSEEK_API_KEY: deepseekApiKey,\n HOME: codexHomeDir,\n CODEX_HOME: codexHomeDir,\n TMPDIR: codexTempDir,\n XDG_RUNTIME_DIR: codexRuntimeDir,\n XDG_CACHE_HOME: path.join(codexHomeDir, '.cache'),\n XDG_CONFIG_HOME: path.join(codexHomeDir, '.config'),\n XDG_DATA_HOME: path.join(codexHomeDir, '.local', 'share'),\n PATH: process.env.PATH || '/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin',\n },\n config: {\n model_provider: codexDeepSeekProviderId,\n model_providers: {\n [codexDeepSeekProviderId]: {\n name: 'DeepSeek Chat Completions',\n base_url: codexResponsesBaseUrl,\n env_key: 'DEEPSEEK_API_KEY',\n wire_api: 'responses',\n query_params: {},\n },\n },\n },\n },\n threadOptions: {\n model: deepseekModel,\n approvalPolicy: 'never',\n sandboxMode: 'read-only',\n skipGitRepoCheck: true,\n networkAccessEnabled: true,\n },\n };\n}\n\nfunction codexPromptFromMessages(messages) {\n return messages\n .map((message) => {\n const role = String(message.role || 'user').toUpperCase();\n return `${role}:\\n${message.content}`;\n })\n .join('\\n\\n');\n}\n\nfunction responseContentText(content) {\n if (typeof content === 'string') return content;\n if (!Array.isArray(content)) return '';\n return content\n .map((part) => {\n if (typeof part === 'string') return part;\n if (!part || typeof part !== 'object') return '';\n return String(part.text || part.input_text || part.output_text || '');\n })\n .filter(Boolean)\n .join('\\n');\n}\n\nfunction codexResponsesPrompt(body) {\n const input = Array.isArray(body?.input) ? body.input : [];\n const userItems = input.filter((item) => item?.role === 'user');\n const source = userItems[userItems.length - 1] || input[input.length - 1];\n const prompt = responseContentText(source?.content).trim();\n if (prompt) return prompt;\n if (typeof body?.input === 'string') return body.input.trim();\n return '';\n}\n\nfunction writeResponsesEvent(res, event, data) {\n res.write(`event: ${event}\\n`);\n res.write(`data: ${JSON.stringify(data)}\\n\\n`);\n}\n\nfunction responseUsage(usage = {}) {\n return {\n input_tokens: Number(usage.promptTokens || usage.input_tokens || 0),\n cached_input_tokens: 0,\n output_tokens: Number(usage.completionTokens || usage.output_tokens || 0),\n reasoning_output_tokens: 0,\n total_tokens: Number(usage.totalTokens || usage.total_tokens || 0),\n };\n}\n\nfunction createLibreChatAgentRuntime() {\n if (!enableLibreChatAgentChat || !libreChatAgentApiKey || !libreChatAgentId) {\n return {\n enabled: false,\n provider: 'librechat-agent',\n reason: !enableLibreChatAgentChat\n ? 'ENABLE_LIBRECHAT_AGENT_CHAT is not true'\n : !libreChatAgentApiKey\n ? 'LIBRECHAT_AGENT_API_KEY is not configured'\n : 'LIBRECHAT_AGENT_ID is not configured',\n };\n }\n\n return {\n enabled: true,\n provider: 'librechat-agent',\n model: libreChatAgentId,\n chatCompletionsUrl: `${libreChatAgentsBaseUrl}/chat/completions`,\n };\n}\n\nfunction libreChatUsage(usage = {}) {\n return {\n promptTokens: Number(usage.prompt_tokens || usage.input_tokens || usage.promptTokens || 0),\n completionTokens: Number(usage.completion_tokens || usage.output_tokens || usage.completionTokens || 0),\n totalTokens: Number(usage.total_tokens || usage.totalTokens || 0),\n };\n}\n\nasync function callLibreChatAgent(messages) {\n const runtime = createLibreChatAgentRuntime();\n if (!runtime.enabled) {\n const error = new Error(runtime.reason || 'LibreChat Agent chat is not enabled');\n error.statusCode = 503;\n throw error;\n }\n\n const response = await fetch(runtime.chatCompletionsUrl, {\n method: 'POST',\n headers: {\n Accept: 'application/json',\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${libreChatAgentApiKey}`,\n },\n body: JSON.stringify({\n model: runtime.model,\n messages,\n temperature: 0.4,\n stream: false,\n }),\n signal: AbortSignal.timeout(120_000),\n });\n\n const bodyText = await response.text();\n let payload;\n try {\n payload = bodyText ? JSON.parse(bodyText) : {};\n } catch {\n payload = { error: bodyText };\n }\n\n if (!response.ok) {\n const upstreamMessage = payload?.error?.message || payload?.message || bodyText || '';\n const error = new Error(upstreamMessage || `LibreChat Agent request failed: ${response.status}`);\n error.statusCode = response.status;\n throw error;\n }\n\n const message = payload?.choices?.[0]?.message?.content?.trim();\n if (!message) {\n const error = new Error('LibreChat Agent returned an empty response');\n error.statusCode = 502;\n throw error;\n }\n\n return {\n message,\n usage: libreChatUsage(payload?.usage),\n };\n}\n\nasync function callLibreChatAgentStream(messages, callbacks = {}) {\n const runtime = createLibreChatAgentRuntime();\n if (!runtime.enabled) {\n const error = new Error(runtime.reason || 'LibreChat Agent chat is not enabled');\n error.statusCode = 503;\n throw error;\n }\n\n const response = await fetch(runtime.chatCompletionsUrl, {\n method: 'POST',\n headers: {\n Accept: 'text/event-stream',\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${libreChatAgentApiKey}`,\n },\n body: JSON.stringify({\n model: runtime.model,\n messages,\n temperature: 0.4,\n stream: true,\n }),\n signal: callbacks.signal || AbortSignal.timeout(180_000),\n });\n\n if (!response.ok) {\n const bodyText = await response.text();\n let payload;\n try {\n payload = bodyText ? JSON.parse(bodyText) : {};\n } catch {\n payload = { error: bodyText };\n }\n const upstreamMessage = payload?.error?.message || payload?.message || bodyText || '';\n const error = new Error(upstreamMessage || `LibreChat Agent request failed: ${response.status}`);\n error.statusCode = response.status;\n throw error;\n }\n\n let buffer = '';\n let message = '';\n let usage = { promptTokens: 0, completionTokens: 0, totalTokens: 0 };\n const decoder = new TextDecoder();\n\n for await (const chunk of response.body) {\n buffer += decoder.decode(chunk, { stream: true });\n const lines = buffer.split(/\\r?\\n/);\n buffer = lines.pop() || '';\n\n for (const rawLine of lines) {\n const line = rawLine.trim();\n if (!line.startsWith('data:')) continue;\n const data = line.slice('data:'.length).trim();\n if (!data || data === '[DONE]') continue;\n\n let payload;\n try {\n payload = JSON.parse(data);\n } catch {\n continue;\n }\n\n const delta = payload?.choices?.[0]?.delta?.content || '';\n if (delta) {\n message += delta;\n callbacks.onToken?.(delta);\n }\n\n if (payload?.usage) {\n usage = libreChatUsage(payload.usage);\n }\n }\n }\n\n message = message.trim();\n if (!message) {\n const error = new Error('LibreChat Agent returned an empty response');\n error.statusCode = 502;\n throw error;\n }\n\n return { message, usage };\n}\n\nfunction isDeepSeekQuotaError(error) {\n if (error?.code === 'DEEPSEEK_PROVIDER_QUOTA') return true;\n if (error?.code === 'USER_TOKEN_QUOTA_EXCEEDED' || error?.code === 'AI_MEMBERSHIP_REQUIRED') return false;\n const message = String(error?.message || error || '');\n return /insufficient\\s+balance|balance/i.test(message);\n}\n\nfunction publicAiErrorMessage(error) {\n if (isDeepSeekQuotaError(error)) {\n return '大乘 AI 服务额度暂不可用,请联系管理员充值 DeepSeek 账户后重试。';\n }\n return error?.message || 'Internal Server Error';\n}\n\nfunction responsesPayload({\n responseId,\n itemId,\n status,\n model,\n text = '',\n usage = null,\n error = null,\n}) {\n return {\n id: responseId,\n object: 'response',\n status,\n model,\n output:\n status === 'completed'\n ? [\n {\n id: itemId,\n type: 'message',\n role: 'assistant',\n status: 'completed',\n content: [{ type: 'output_text', text }],\n },\n ]\n : [],\n ...(usage ? { usage: responseUsage(usage) } : {}),\n ...(error\n ? {\n error: {\n message: error.message || String(error),\n type: 'server_error',\n },\n }\n : {}),\n };\n}\n\nasync function callCodexSdkDeepSeek(messages, callbacks = {}) {\n const codexRuntime = createCodexDeepSeekRuntime();\n if (!codexRuntime.enabled) {\n const error = new Error(codexRuntime.reason || 'Codex SDK chat is not enabled');\n error.statusCode = 503;\n throw error;\n }\n\n const { Codex } = await import('@openai/codex-sdk');\n const codex = new Codex(codexRuntime.options);\n const thread = codex.startThread(codexRuntime.threadOptions);\n const prompt = codexPromptFromMessages(messages);\n\n if (callbacks.onToken || callbacks.onStep) {\n const { events } = await thread.runStreamed(prompt, { signal: callbacks.signal });\n let finalResponse = '';\n let usage = null;\n\n for await (const event of events) {\n if (event.type === 'item.completed') {\n const item = event.item;\n if (item.type === 'agent_message') {\n finalResponse = item.text || finalResponse;\n callbacks.onToken?.(item.text || '');\n } else if (item.type === 'todo_list') {\n callbacks.onStep?.({\n title: 'Codex SDK 执行计划',\n message: item.items.map((todo) => `${todo.completed ? '✓' : '•'} ${todo.text}`).join('\\n'),\n });\n } else if (item.type === 'command_execution') {\n callbacks.onStep?.({\n title: 'Codex SDK 执行命令',\n message: item.command,\n });\n } else if (item.type === 'mcp_tool_call') {\n callbacks.onStep?.({\n title: 'Codex SDK 调用工具',\n message: `${item.server}.${item.tool}`,\n });\n }\n } else if (event.type === 'turn.completed') {\n usage = event.usage;\n } else if (event.type === 'turn.failed') {\n throw new Error(event.error?.message || 'Codex SDK turn failed');\n } else if (event.type === 'error') {\n throw new Error(event.message || 'Codex SDK stream failed');\n }\n }\n\n if (!finalResponse.trim()) {\n const error = new Error('Codex SDK returned an empty response');\n error.statusCode = 502;\n throw error;\n }\n\n return {\n message: finalResponse.trim(),\n usage: {\n promptTokens: Number(usage?.input_tokens || 0),\n completionTokens: Number(usage?.output_tokens || 0),\n totalTokens: Number((usage?.input_tokens || 0) + (usage?.output_tokens || 0)),\n },\n };\n }\n\n const turn = await thread.run(prompt);\n if (!turn.finalResponse?.trim()) {\n const error = new Error('Codex SDK returned an empty response');\n error.statusCode = 502;\n throw error;\n }\n\n return {\n message: turn.finalResponse.trim(),\n usage: {\n promptTokens: Number(turn.usage?.input_tokens || 0),\n completionTokens: Number(turn.usage?.output_tokens || 0),\n totalTokens: Number((turn.usage?.input_tokens || 0) + (turn.usage?.output_tokens || 0)),\n },\n };\n}\n\nconst activeAgentRuns = new Map();\n\nfunction normalizeAgentMode(value) {\n const mode = safeUserText(value || 'dharma_guide');\n return /^[a-z][a-z0-9_:-]{0,48}$/i.test(mode) ? mode : 'dharma_guide';\n}\n\nfunction openClawSessionKey(user, conversationId) {\n const env = process.env.NODE_ENV === 'production' ? 'prod' : 'dev';\n return `fabushi:env:${env}:user:${user.userId}:conversation:${conversationId}`;\n}\n\nfunction createOpenClawRuntime() {\n if (!enableOpenClawAgentChat || !openClawGatewayToken || !openClawGatewayUrl) {\n return {\n enabled: false,\n provider: 'openclaw-gateway',\n reason: !enableOpenClawAgentChat\n ? 'ENABLE_OPENCLAW_AGENT_CHAT is not true'\n : !openClawGatewayToken\n ? 'OPENCLAW_GATEWAY_TOKEN is not configured'\n : 'OPENCLAW_GATEWAY_URL is not configured',\n };\n }\n\n return {\n enabled: true,\n provider: 'openclaw-gateway',\n model: openClawAgentId,\n runsUrl: openClawRunsEndpoint || `${openClawGatewayUrl}/v1/agents/${encodeURIComponent(openClawAgentId)}/runs`,\n };\n}\n\nfunction extractOpenClawText(payload) {\n if (!payload || typeof payload !== 'object') return '';\n const candidates = [\n payload.message,\n payload.content,\n payload.output_text,\n payload.finalResponse,\n payload.final_response,\n payload.result?.message,\n payload.result?.content,\n payload.output?.message,\n payload.output?.content,\n payload.choices?.[0]?.message?.content,\n ];\n for (const candidate of candidates) {\n if (typeof candidate === 'string' && candidate.trim()) return candidate.trim();\n }\n return '';\n}\n\nfunction extractOpenClawUsage(payload) {\n const usage = payload?.usage || payload?.result?.usage || {};\n return {\n promptTokens: Number(usage.input_tokens || usage.prompt_tokens || usage.promptTokens || 0),\n completionTokens: Number(usage.output_tokens || usage.completion_tokens || usage.completionTokens || 0),\n totalTokens: Number(usage.total_tokens || usage.totalTokens || 0),\n };\n}\n\nasync function callOpenClawAgent({ messages, user, conversationId, mode, signal }) {\n const runtime = createOpenClawRuntime();\n if (!runtime.enabled) {\n const error = new Error(runtime.reason || 'OpenClaw Agent chat is not enabled');\n error.statusCode = 503;\n throw error;\n }\n\n const sessionKey = openClawSessionKey(user, conversationId);\n const response = await fetch(runtime.runsUrl, {\n method: 'POST',\n headers: {\n Accept: 'application/json',\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${openClawGatewayToken}`,\n },\n body: JSON.stringify({\n agentId: openClawAgentId,\n sessionKey,\n mode,\n stream: false,\n input: { messages },\n messages,\n metadata: {\n app: 'fabushi',\n userId: user.userId,\n conversationId,\n toolPolicy: 'public-user-read-mostly',\n },\n }),\n signal: signal || AbortSignal.timeout(180_000),\n });\n\n const bodyText = await response.text();\n let payload;\n try {\n payload = bodyText ? JSON.parse(bodyText) : {};\n } catch {\n payload = { message: bodyText };\n }\n\n if (!response.ok) {\n const error = new Error(payload?.error?.message || payload?.message || `OpenClaw request failed: ${response.status}`);\n error.statusCode = response.status;\n throw error;\n }\n\n const message = extractOpenClawText(payload);\n if (!message) {\n const error = new Error('OpenClaw returned an empty response');\n error.statusCode = 502;\n throw error;\n }\n\n return {\n message,\n runId: payload.runId || payload.id || payload.result?.runId || null,\n provider: runtime.provider,\n model: runtime.model,\n usage: extractOpenClawUsage(payload),\n };\n}\n\nasync function runAgentModel({ messages, user, conversationId, mode, callbacks = {}, signal, maxCompletionTokens }) {\n const openClawRuntime = createOpenClawRuntime();\n if (openClawRuntime.enabled && !callbacks.onToken) {\n return await callOpenClawAgent({ messages, user, conversationId, mode, signal });\n }\n\n const codexRuntime = createCodexDeepSeekRuntime();\n const libreChatRuntime = createLibreChatAgentRuntime();\n let provider = libreChatRuntime.enabled\n ? libreChatRuntime.provider\n : codexRuntime.enabled\n ? codexRuntime.provider\n : 'deepseek';\n let model = libreChatRuntime.enabled ? libreChatRuntime.model : deepseekModel;\n\n try {\n if (libreChatRuntime.enabled) {\n const result = callbacks.onToken\n ? await callLibreChatAgentStream(messages, { ...callbacks, signal })\n : await callLibreChatAgent(messages);\n return { ...result, provider, model };\n }\n if (codexRuntime.enabled) {\n const result = await callCodexSdkDeepSeek(messages, { ...callbacks, signal });\n return { ...result, provider, model };\n }\n const result = callbacks.onToken\n ? await callDeepSeekStream(messages, { ...callbacks, signal, maxCompletionTokens })\n : await callDeepSeek(messages, { maxCompletionTokens });\n return { ...result, provider, model };\n } catch (error) {\n if (!codexRuntime.enabled || isDeepSeekQuotaError(error)) throw error;\n logger.warn({ error: String(error) }, 'Agent model failed, falling back to DeepSeek direct');\n const result = callbacks.onToken\n ? await callDeepSeekStream(messages, { ...callbacks, signal, maxCompletionTokens })\n : await callDeepSeek(messages, { maxCompletionTokens });\n return { ...result, provider: 'deepseek', model: deepseekModel };\n }\n}\n\nfunction updateAgentRunStatus(run, status, fields = {}) {\n statements.updateAgentRunStatus.run({\n id: run.id,\n userId: run.user_id || run.userId,\n status,\n openClawRunId: fields.openClawRunId ?? null,\n inputTokens: fields.inputTokens ?? null,\n outputTokens: fields.outputTokens ?? null,\n toolCallCount: fields.toolCallCount ?? null,\n completedAt: status === 'completed' || status === 'cancelled' ? nowIso() : null,\n failedAt: status === 'failed' ? nowIso() : null,\n errorCode: fields.errorCode ?? null,\n errorMessage: fields.errorMessage ?? null,\n });\n}\n\nfunction buildAgentMessages(conversationId) {\n const historyRows = statements.listMessages.all(conversationId).slice(-14);\n return [{ role: 'system', content: systemPrompt }, ...normalizeMessages(historyRows)];\n}\n\nfunction agentUsagePayload(usage, user, budget) {\n const promptTokens = usage.promptTokens || usage.inputTokens || 0;\n const completionTokens = usage.completionTokens || usage.outputTokens || 0;\n const totalTokens = usage.totalTokens || promptTokens + completionTokens;\n const latestUsage = usageFor(user.userId);\n return {\n inputTokens: promptTokens,\n outputTokens: completionTokens,\n totalTokens,\n monthlyLimit: budget.limit,\n remainingTokens: Math.max(0, budget.limit - latestUsage.used),\n };\n}\n\nfunction openAiContentToText(content) {\n if (typeof content === 'string') return content;\n if (Array.isArray(content)) {\n return content\n .map((part) => {\n if (typeof part === 'string') return part;\n if (!part || typeof part !== 'object') return '';\n return readText(part.text || part.content || part.value);\n })\n .filter(Boolean)\n .join('\\n');\n }\n return readText(content);\n}\n\nfunction normalizeOpenAiMessages(value) {\n if (!Array.isArray(value)) return [];\n const allowedRoles = new Set(['system', 'user', 'assistant', 'tool']);\n return value\n .map((item) => {\n if (!item || typeof item !== 'object') return null;\n const role = allowedRoles.has(item.role) ? item.role : 'user';\n const content = openAiContentToText(item.content);\n if (!content) return null;\n return { role, content };\n })\n .filter(Boolean);\n}\n\nfunction truncateForDeepSeekMessage(text, maxChars = 12_000) {\n const value = readText(text);\n if (value.length <= maxChars) return value;\n return `${value.slice(0, maxChars)}\\n\\n[内容已截断]`;\n}\n\nfunction truncateTailForDeepSeekMessage(text, maxChars = 2400) {\n const value = readText(text);\n if (value.length <= maxChars) return value;\n return `[前文已省略]\\n\\n${value.slice(-maxChars)}`;\n}\n\nfunction stripOpenClawPromptNoise(text) {\n const value = readText(text);\n if (!value) return '';\n\n const finalPromptMatch = value.match(/finalPromptText[\"'\\s::]+([\\s\\S]{1,4000})$/i);\n if (finalPromptMatch?.[1]) {\n return readText(finalPromptMatch[1].replace(/^[\"'`]+|[\"'`,}\\]]+$/g, ''));\n }\n\n const markers = [\n 'final prompt:',\n 'final user prompt:',\n 'user prompt:',\n '用户问题:',\n '用户消息:',\n '用户输入:',\n ];\n const lower = value.toLowerCase();\n for (const marker of markers) {\n const index = lower.lastIndexOf(marker.toLowerCase());\n if (index >= 0) {\n const tail = value.slice(index + marker.length).trim();\n if (tail) return tail;\n }\n }\n\n return value;\n}\n\nfunction compactOpenClawMessagesForDeepSeek(messages) {\n const conversationalMessages = messages\n .filter((message) => message.role === 'user' || message.role === 'assistant')\n .map((message) => ({\n role: message.role,\n content: truncateTailForDeepSeekMessage(stripOpenClawPromptNoise(message.content)),\n }))\n .filter((message) => message.content);\n\n const recent = conversationalMessages.slice(-6);\n while (recent.length > 0 && recent[0].role !== 'user') {\n recent.shift();\n }\n\n if (recent.length === 0) {\n const fallback = messages\n .slice()\n .reverse()\n .map((message) => truncateTailForDeepSeekMessage(stripOpenClawPromptNoise(message.content), 2400))\n .find(Boolean);\n if (!fallback) return messages;\n recent.push({ role: 'user', content: fallback });\n }\n\n return [\n {\n role: 'system',\n content: '你是大乘 App 的桌面 AI 助手。请直接回答用户问题,默认使用简洁、准确的中文。',\n },\n ...recent,\n ];\n}\n\nfunction isDeepSeekBlockedError(error) {\n return (\n Number(error?.statusCode) === 403 &&\n /blocked|被拦截|forbidden/i.test(String(error?.message || error || ''))\n );\n}\n\nfunction shouldRetryWithCompactedOpenClawMessages(error, messages, compactedMessages) {\n if (!isDeepSeekBlockedError(error)) return false;\n return JSON.stringify(messages) !== JSON.stringify(compactedMessages);\n}\n\nfunction openClawBlockedFallbackText(messages) {\n const lastUser = messages\n .slice()\n .reverse()\n .find((message) => message.role === 'user')?.content || '';\n const localActionIntent =\n /(file|folder|directory|project|workspace|path|文件|文件夹|目录|项目|工程|路径|本地)/i.test(lastUser) &&\n /(create|make|mkdir|write|edit|modify|rename|delete|move|copy|save|download|read|list|生成|创建|新建|写入|编辑|修改|重命名|删除|移动|复制|保存|下载|读取|列出)/i.test(lastUser);\n\n if (localActionIntent) {\n return [\n '本机工具规划被模型服务拦截了,所以这次没有实际读取、创建、修改或下载本机文件。',\n '如果只是新建文件夹,请先在底部选择项目后重新发送“新建文件夹”;桌面端会直接在已选项目里创建。',\n '如果是更复杂的本地文件操作,请在“连应用”里重启本机 AI 后重试,或把要操作的文件路径和目标说得更具体。',\n ].join('\\n');\n }\n\n return '模型服务拦截了这次回复,未能生成结果。请换一种更具体、更短的表述后重试。';\n}\nfunction usageWithFallback(usage, messages, text) {\n const promptTokens = Number(usage?.promptTokens || usage?.inputTokens || 0);\n const completionTokens = Number(usage?.completionTokens || usage?.outputTokens || 0);\n const totalTokens = Number(usage?.totalTokens || 0);\n if (totalTokens > 0) {\n return {\n promptTokens,\n completionTokens,\n totalTokens,\n };\n }\n const estimatedPrompt = promptTokens || estimateTokens(JSON.stringify(messages));\n const estimatedCompletion = completionTokens || estimateTokens(text);\n return {\n promptTokens: estimatedPrompt,\n completionTokens: estimatedCompletion,\n totalTokens: estimatedPrompt + estimatedCompletion,\n };\n}\n\nfunction openAiUsagePayload(usage, user, budget) {\n const promptTokens = Number(usage.promptTokens || 0);\n const completionTokens = Number(usage.completionTokens || 0);\n const totalTokens = Number(usage.totalTokens || promptTokens + completionTokens);\n const latestUsage = usageFor(user.userId);\n const remainingTokens = Math.max(0, budget.limit - latestUsage.used);\n return {\n prompt_tokens: promptTokens,\n completion_tokens: completionTokens,\n total_tokens: totalTokens,\n promptTokens,\n completionTokens,\n totalTokens,\n monthlyLimit: budget.limit,\n remainingTokens,\n };\n}\n\nfunction writeOpenAiSse(res, payload) {\n res.write(`data: ${JSON.stringify(payload)}\\n\\n`);\n}\n\napp.post(\n '/api/openclaw/deepseek/v1/chat/completions',\n asyncHandler(async (req, res) => {\n const user = await resolveUser(req, req.body || {});\n const incomingMessages = normalizeOpenAiMessages(req.body?.messages);\n if (incomingMessages.length === 0) {\n res.status(400).json({ error: { message: 'messages is required' } });\n return;\n }\n\n const messages = compactOpenClawMessagesForDeepSeek(incomingMessages);\n const promptEstimate = estimateTokens(JSON.stringify(messages));\n const estimated = promptEstimate + 600;\n const budget = enforceTokenBudget(user, estimated);\n const maxCompletionTokens = Math.min(\n completionBudgetFor(budget, promptEstimate),\n normalizeMaxCompletionTokens(req.body?.max_tokens),\n );\n const model = normalizeDeepSeekModelName(req.body?.model);\n const wantsStream =\n req.body?.stream === true ||\n String(req.get('accept') || '').includes('text/event-stream');\n const id = `chatcmpl_${crypto.randomUUID().replaceAll('-', '')}`;\n const created = Math.floor(Date.now() / 1000);\n const compactedMessages = messages;\n\n if (!wantsStream) {\n let result;\n let usageMessages = messages;\n try {\n result = await callDeepSeek(messages, { model, maxCompletionTokens });\n } catch (error) {\n if (!shouldRetryWithCompactedOpenClawMessages(error, messages, compactedMessages)) {\n throw error;\n }\n logger.warn(\n {\n statusCode: error?.statusCode,\n messageCount: messages.length,\n compactedMessageCount: compactedMessages.length,\n },\n 'OpenClaw DeepSeek proxy retrying with compacted messages',\n );\n usageMessages = compactedMessages;\n result = await callDeepSeek(compactedMessages, { model, maxCompletionTokens });\n }\n const usage = usageWithFallback(result.usage, usageMessages, result.message);\n recordUsage(user.userId, usage.totalTokens);\n res.json({\n id,\n object: 'chat.completion',\n created,\n model: result.model || model,\n choices: [\n {\n index: 0,\n message: { role: 'assistant', content: result.message },\n finish_reason: 'stop',\n },\n ],\n usage: openAiUsagePayload(usage, user, budget),\n });\n return;\n }\n\n const abortController = new AbortController();\n req.on('close', () => abortController.abort());\n\n res.status(200);\n res.set({\n 'Content-Type': 'text/event-stream; charset=utf-8',\n 'Cache-Control': 'no-cache, no-transform',\n Connection: 'keep-alive',\n 'X-Accel-Buffering': 'no',\n });\n res.flushHeaders?.();\n\n const baseChunk = {\n id,\n object: 'chat.completion.chunk',\n created,\n model,\n system_fingerprint: 'fabushi-openclaw-deepseek-proxy',\n };\n writeOpenAiSse(res, {\n ...baseChunk,\n choices: [{ index: 0, delta: { role: 'assistant' }, finish_reason: null }],\n });\n\n let text = '';\n let usageMessages = messages;\n try {\n let result;\n try {\n result = await callDeepSeekStream(messages, {\n model,\n maxCompletionTokens,\n signal: abortController.signal,\n onToken: (delta) => {\n text += delta;\n writeOpenAiSse(res, {\n ...baseChunk,\n choices: [{ index: 0, delta: { content: delta }, finish_reason: null }],\n });\n },\n });\n } catch (error) {\n if (!shouldRetryWithCompactedOpenClawMessages(error, messages, compactedMessages)) {\n throw error;\n }\n logger.warn(\n {\n statusCode: error?.statusCode,\n messageCount: messages.length,\n compactedMessageCount: compactedMessages.length,\n },\n 'OpenClaw DeepSeek proxy stream retrying with compacted messages',\n );\n text = '';\n usageMessages = compactedMessages;\n result = await callDeepSeekStream(compactedMessages, {\n model,\n maxCompletionTokens,\n signal: abortController.signal,\n onToken: (delta) => {\n text += delta;\n writeOpenAiSse(res, {\n ...baseChunk,\n choices: [{ index: 0, delta: { content: delta }, finish_reason: null }],\n });\n },\n });\n }\n text = result.message || text;\n const usage = usageWithFallback(result.usage, usageMessages, text);\n recordUsage(user.userId, usage.totalTokens);\n writeOpenAiSse(res, {\n ...baseChunk,\n model: result.model || model,\n choices: [{ index: 0, delta: {}, finish_reason: 'stop' }],\n });\n writeOpenAiSse(res, {\n ...baseChunk,\n model: result.model || model,\n choices: [],\n usage: openAiUsagePayload(usage, user, budget),\n });\n res.write('data: [DONE]\\n\\n');\n } catch (error) {\n if (isDeepSeekBlockedError(error)) {\n const fallbackMessage = openClawBlockedFallbackText(usageMessages);\n const usage = usageWithFallback(null, usageMessages, fallbackMessage);\n recordUsage(user.userId, usage.totalTokens);\n logger.warn(\n { statusCode: error?.statusCode, code: error?.code },\n 'OpenClaw DeepSeek proxy stream blocked; returning local fallback message',\n );\n writeOpenAiSse(res, {\n ...baseChunk,\n choices: [{ index: 0, delta: { content: fallbackMessage }, finish_reason: null }],\n });\n writeOpenAiSse(res, {\n ...baseChunk,\n choices: [{ index: 0, delta: {}, finish_reason: 'stop' }],\n });\n writeOpenAiSse(res, {\n ...baseChunk,\n choices: [],\n usage: openAiUsagePayload(usage, user, budget),\n });\n res.write('data: [DONE]\\n\\n');\n return;\n }\n logger.error(\n { error: String(error), code: error?.code, statusCode: error?.statusCode },\n 'OpenClaw DeepSeek proxy stream failed',\n );\n writeOpenAiSse(res, {\n ...baseChunk,\n choices: [],\n error: {\n message: publicAiErrorMessage(error),\n code: error?.code || 'OPENCLAW_DEEPSEEK_PROXY_ERROR',\n },\n });\n res.write('data: [DONE]\\n\\n');\n } finally {\n res.end();\n }\n }),\n);\n\napp.post(\n '/codex-deepseek/v1/responses',\n asyncHandler(async (req, res) => {\n const bearer = bearerToken(req);\n if (!deepseekApiKey || bearer !== deepseekApiKey) {\n res.status(401).json({\n error: { message: 'Unauthorized Codex DeepSeek adapter request' },\n });\n return;\n }\n\n const prompt = codexResponsesPrompt(req.body);\n if (!prompt) {\n res.status(400).json({ error: { message: 'Responses input is required' } });\n return;\n }\n\n const model = String(req.body?.model || deepseekModel);\n const responseId = `resp_${crypto.randomUUID().replaceAll('-', '')}`;\n const itemId = `msg_${crypto.randomUUID().replaceAll('-', '')}`;\n const wantsStream =\n req.body?.stream !== false ||\n String(req.get('accept') || '').includes('text/event-stream');\n const messages = [{ role: 'user', content: prompt }];\n\n if (!wantsStream) {\n const result = await callDeepSeek(messages);\n res.json(\n responsesPayload({\n responseId,\n itemId,\n status: 'completed',\n model,\n text: result.message,\n usage: result.usage,\n }),\n );\n return;\n }\n\n res.status(200);\n res.set({\n 'Content-Type': 'text/event-stream; charset=utf-8',\n 'Cache-Control': 'no-cache, no-transform',\n Connection: 'keep-alive',\n 'X-Accel-Buffering': 'no',\n });\n res.flushHeaders?.();\n\n writeResponsesEvent(res, 'response.created', {\n type: 'response.created',\n response: responsesPayload({\n responseId,\n itemId,\n status: 'in_progress',\n model,\n }),\n });\n writeResponsesEvent(res, 'response.output_item.added', {\n type: 'response.output_item.added',\n output_index: 0,\n item: {\n id: itemId,\n type: 'message',\n role: 'assistant',\n status: 'in_progress',\n content: [],\n },\n });\n writeResponsesEvent(res, 'response.content_part.added', {\n type: 'response.content_part.added',\n item_id: itemId,\n output_index: 0,\n content_index: 0,\n part: { type: 'output_text', text: '' },\n });\n\n let text = '';\n try {\n const result = await callDeepSeekStream(messages, {\n onToken: (delta) => {\n text += delta;\n writeResponsesEvent(res, 'response.output_text.delta', {\n type: 'response.output_text.delta',\n item_id: itemId,\n output_index: 0,\n content_index: 0,\n delta,\n });\n },\n });\n text = result.message || text;\n writeResponsesEvent(res, 'response.output_text.done', {\n type: 'response.output_text.done',\n item_id: itemId,\n output_index: 0,\n content_index: 0,\n text,\n });\n writeResponsesEvent(res, 'response.content_part.done', {\n type: 'response.content_part.done',\n item_id: itemId,\n output_index: 0,\n content_index: 0,\n part: { type: 'output_text', text },\n });\n writeResponsesEvent(res, 'response.output_item.done', {\n type: 'response.output_item.done',\n output_index: 0,\n item: {\n id: itemId,\n type: 'message',\n role: 'assistant',\n status: 'completed',\n content: [{ type: 'output_text', text }],\n },\n });\n writeResponsesEvent(res, 'response.completed', {\n type: 'response.completed',\n response: responsesPayload({\n responseId,\n itemId,\n status: 'completed',\n model,\n text,\n usage: result.usage,\n }),\n });\n res.end();\n } catch (error) {\n writeResponsesEvent(res, 'response.failed', {\n type: 'response.failed',\n response: responsesPayload({\n responseId,\n itemId,\n status: 'failed',\n model,\n text,\n error,\n }),\n });\n res.end();\n }\n }),\n);\n\napp.use(\n '/api/openclaw/runtime/files',\n express.static(openClawRuntimeUpdatesDir, {\n fallthrough: false,\n immutable: true,\n maxAge: '7d',\n setHeaders(res) {\n res.setHeader('X-Content-Type-Options', 'nosniff');\n },\n }),\n);\n\napp.get(\n '/api/openclaw/runtime/manifest',\n asyncHandler(async (req, res) => {\n const platform = readText(req.query.platform);\n const currentVersion = readText(req.query.currentVersion);\n const appVersion = readText(req.query.appVersion);\n\n let manifest;\n try {\n manifest = await readJsonFile(openClawRuntimeManifestPath);\n } catch (error) {\n logger.info(\n { manifestPath: openClawRuntimeManifestPath, error: String(error) },\n 'OpenClaw runtime manifest unavailable',\n );\n return jsonResponse(res, 200, {\n success: true,\n schema: 1,\n updateAvailable: false,\n reason: 'manifest-unavailable',\n platform,\n currentVersion,\n appVersion,\n });\n }\n\n const latest = manifest.latest && typeof manifest.latest === 'object'\n ? manifest.latest\n : manifest;\n const platforms = latest.platforms && typeof latest.platforms === 'object'\n ? latest.platforms\n : {};\n const platformRelease = platform ? platforms[platform] : null;\n const version = readText(latest.version || manifest.version);\n\n if (!platform || !platformRelease || typeof platformRelease !== 'object') {\n return jsonResponse(res, 200, {\n success: true,\n schema: Number(manifest.schema || 1),\n channel: readText(manifest.channel || latest.channel || 'stable'),\n updateAvailable: false,\n reason: platform ? 'platform-unavailable' : 'platform-required',\n platform,\n currentVersion,\n appVersion,\n latestVersion: version,\n });\n }\n\n if (platform.startsWith('macos-') && !openClawRuntimeEnableMacosUpdates) {\n return jsonResponse(res, 200, {\n success: true,\n schema: Number(manifest.schema || 1),\n channel: readText(manifest.channel || latest.channel || 'stable'),\n updateAvailable: false,\n reason: 'macos-runtime-updates-disabled',\n platform,\n currentVersion,\n appVersion,\n latestVersion: version,\n });\n }\n\n const downloadUrl = absolutizeOpenClawRuntimeUrl(\n req,\n platformRelease.archiveUrl || platformRelease.url || platformRelease.fileName,\n );\n const release = {\n version,\n channel: readText(manifest.channel || latest.channel || 'stable'),\n minAppVersion: readText(latest.minAppVersion || platformRelease.minAppVersion),\n notes: readText(latest.notes || platformRelease.notes),\n nodeExecutable: readText(\n platformRelease.nodeExecutable || latest.nodeExecutable || 'node/bin/node',\n ),\n cliEntrypoint: readText(\n platformRelease.cliEntrypoint || latest.cliEntrypoint || 'openclaw/openclaw.mjs',\n ),\n defaultPort: Number(latest.defaultPort || manifest.defaultPort || 18789),\n defaultModel: readText(latest.defaultModel || manifest.defaultModel || 'openclaw/default'),\n defaultModelOverride: readText(latest.defaultModelOverride || manifest.defaultModelOverride),\n gatewayArgs: Array.isArray(latest.gatewayArgs)\n ? latest.gatewayArgs.map((item) => String(item))\n : ['gateway', '--port', '{port}', '--force'],\n archiveUrl: downloadUrl,\n sha256: readText(platformRelease.sha256),\n size: Number(platformRelease.size || 0),\n };\n\n jsonResponse(res, 200, {\n success: true,\n schema: Number(manifest.schema || 1),\n updateAvailable: Boolean(version && downloadUrl && version !== currentVersion),\n platform,\n currentVersion,\n appVersion,\n latest: release,\n generatedAt: readText(manifest.generatedAt || latest.generatedAt),\n });\n }),\n);\n\napp.get('/health', (_req, res) => {\n const codexRuntime = createCodexDeepSeekRuntime();\n const libreChatRuntime = createLibreChatAgentRuntime();\n const openClawRuntime = createOpenClawRuntime();\n jsonResponse(res, 200, {\n status: 'ok',\n service: 'dacheng-ai-backend',\n provider: libreChatRuntime.enabled ? libreChatRuntime.provider : 'deepseek',\n model: libreChatRuntime.enabled ? libreChatRuntime.model : deepseekModel,\n deepseekModel,\n deepseekAvailableModels,\n libreChatAgentChatEnabled: libreChatRuntime.enabled,\n libreChatAgentProvider: libreChatRuntime.provider,\n libreChatAgentReason: libreChatRuntime.enabled ? undefined : libreChatRuntime.reason,\n codexSdkAvailable: true,\n codexSdkChatEnabled: codexRuntime.enabled,\n codexSdkProvider: codexRuntime.provider,\n openClawAgentChatEnabled: openClawRuntime.enabled,\n openClawAgentProvider: openClawRuntime.provider,\n openClawAgentReason: openClawRuntime.enabled ? undefined : openClawRuntime.reason,\n requireMemberForAi,\n memberMonthlyLimit,\n freeMonthlyLimit,\n deepseekMaxCompletionTokens,\n openClawRuntimeUpdatesDir,\n openClawRuntimeManifestConfigured: fs.existsSync(openClawRuntimeManifestPath),\n cbetaApiRoot,\n timestamp: nowIso(),\n });\n});\n\napp.get(\n '/api/miniapps/registry',\n asyncHandler(async (req, res) => {\n const user = await resolveUser(req, { username: req.query.username });\n const official = officialMiniAppRegistry(req);\n const store = await readMiniAppStore();\n const visibleSandbox = store.apps.filter((appRecord) => {\n if (appRecord.reviewStatus === 'approved' && appRecord.source === 'marketplace') {\n return true;\n }\n return appRecord.ownerId === user.userId;\n });\n const bots = [\n ...official.bots,\n ...visibleSandbox.map((record) => miniAppRecordToBot(record)),\n ];\n const miniApps = [\n ...official.miniApps,\n ...visibleSandbox.map((record) => miniAppRecordToManifest(req, record)),\n ];\n const registry = {\n schemaVersion: 1,\n hostApiVersion: miniAppHostApiVersion,\n bots,\n miniApps,\n updatedAt: nowIso(),\n };\n registry.signature = sha256(JSON.stringify({ bots, miniApps }));\n jsonResponse(res, 200, { success: true, registry });\n }),\n);\n\napp.get(\n '/api/miniapps/dev/:miniAppId/index.html',\n asyncHandler(async (req, res) => {\n const miniAppId = safeMiniAppId(req.params.miniAppId);\n const store = await readMiniAppStore();\n const record = store.apps.find((appRecord) => appRecord.miniAppId === miniAppId);\n if (!record) {\n res.status(404).type('text/html').send('<h1>Mini app not found</h1>');\n return;\n }\n res.setHeader('Content-Type', 'text/html; charset=utf-8');\n res.setHeader('Cache-Control', 'no-store');\n res.send(record.sourceHtml || generatedMiniAppHtml(record));\n }),\n);\n\napp.post(\n '/api/miniapps/dev/create',\n asyncHandler(async (req, res) => {\n const user = await resolveUser(req, req.body || {});\n const title = safeUserText(req.body?.title, '个人小程序').slice(0, 80);\n const subtitle = safeUserText(req.body?.subtitle, '个人沙箱小程序').slice(0, 120);\n const permissions = sanitizeMiniAppPermissions(req.body?.permissions);\n const prompt = safeUserText(req.body?.prompt || title);\n const idSeed = crypto.randomUUID().replaceAll('-', '').slice(0, 12);\n const miniAppId = `sandbox.${idSeed}`;\n const botId = `bot.${miniAppId}`;\n const sourceHtml = generatedMiniAppHtml({ title, prompt });\n const scan = miniAppScan(permissions, sourceHtml);\n const createdAt = nowIso();\n const record = {\n ownerId: user.userId,\n miniAppId,\n botId,\n title,\n subtitle,\n initials: title.slice(0, 1) || '小',\n icon: 'code',\n avatarColor: '#3D8BFF',\n greeting: '你好,我是这个个人沙箱小程序的机器人。',\n inputHint: '输入消息,或打开小程序面板',\n version: '0.0.1',\n permissions,\n surfaces: ['chatPanel'],\n theme: 'telegramDark',\n source: 'sandbox',\n reviewStatus: 'sandbox',\n sourceHtml,\n scan,\n createdAt,\n updatedAt: createdAt,\n };\n const store = await readMiniAppStore();\n store.apps.push(record);\n await writeMiniAppStore(store);\n jsonResponse(res, 200, {\n success: true,\n miniApp: miniAppRecordToManifest(req, record),\n bot: miniAppRecordToBot(record),\n scan,\n });\n }),\n);\n\napp.post(\n '/api/miniapps/dev/:miniAppId/version',\n asyncHandler(async (req, res) => {\n const user = await resolveUser(req, req.body || {});\n const miniAppId = safeMiniAppId(req.params.miniAppId);\n const store = await readMiniAppStore();\n const index = store.apps.findIndex((appRecord) => appRecord.miniAppId === miniAppId);\n if (index < 0) {\n return jsonResponse(res, 404, { success: false, message: 'mini app not found' });\n }\n const record = store.apps[index];\n if (record.ownerId !== user.userId) {\n return jsonResponse(res, 403, { success: false, message: 'not your mini app' });\n }\n const title = safeUserText(req.body?.title, record.title).slice(0, 80);\n const subtitle = safeUserText(req.body?.subtitle, record.subtitle).slice(0, 120);\n const sourceHtml =\n safeUserText(req.body?.sourceHtml) ||\n generatedMiniAppHtml({ title, prompt: safeUserText(req.body?.prompt || title) });\n const permissions = sanitizeMiniAppPermissions(req.body?.permissions || record.permissions);\n const scan = miniAppScan(permissions, sourceHtml);\n store.apps[index] = {\n ...record,\n title,\n subtitle,\n sourceHtml,\n permissions,\n scan,\n reviewStatus: 'sandbox',\n version: safeUserText(req.body?.version, record.version || '0.0.1'),\n updatedAt: nowIso(),\n };\n await writeMiniAppStore(store);\n jsonResponse(res, 200, {\n success: true,\n miniApp: miniAppRecordToManifest(req, store.apps[index]),\n bot: miniAppRecordToBot(store.apps[index]),\n scan,\n });\n }),\n);\n\napp.post(\n '/api/miniapps/:miniAppId/submit-review',\n asyncHandler(async (req, res) => {\n const user = await resolveUser(req, req.body || {});\n const miniAppId = safeMiniAppId(req.params.miniAppId);\n const store = await readMiniAppStore();\n const index = store.apps.findIndex((appRecord) => appRecord.miniAppId === miniAppId);\n if (index < 0) {\n return jsonResponse(res, 404, { success: false, message: 'mini app not found' });\n }\n const record = store.apps[index];\n if (record.ownerId !== user.userId) {\n return jsonResponse(res, 403, { success: false, message: 'not your mini app' });\n }\n const scan = miniAppScan(record.permissions || [], record.sourceHtml || '');\n if (!scan.passed) {\n return jsonResponse(res, 400, {\n success: false,\n message: 'security scan failed',\n scan,\n });\n }\n store.apps[index] = {\n ...record,\n scan,\n reviewStatus: 'pending_review',\n updatedAt: nowIso(),\n };\n await writeMiniAppStore(store);\n jsonResponse(res, 200, {\n success: true,\n miniApp: miniAppRecordToManifest(req, store.apps[index]),\n scan,\n });\n }),\n);\n\napp.post(\n '/api/botfather/generate-miniapp',\n asyncHandler(async (req, res) => {\n const user = await resolveUser(req, req.body || {});\n const prompt = safeUserText(req.body?.prompt);\n if (!prompt) {\n return jsonResponse(res, 400, { success: false, message: 'prompt is required' });\n }\n const title = titleFromPromptForMiniApp(prompt);\n const subtitle = '机器人之父生成的个人沙箱小程序';\n const requestedPermissions = Array.isArray(req.body?.permissions) ? req.body.permissions : [];\n const generated = await generateBotFatherMiniAppHtml({ title, prompt, user });\n let sourceHtml = generated.sourceHtml;\n let generation = generated.generation;\n const permissions = sanitizeMiniAppPermissions([\n ...requestedPermissions,\n ...inferMiniAppPermissionsFromText(prompt, sourceHtml),\n ]);\n let scan = miniAppScan(permissions, sourceHtml);\n if (!scan.passed) {\n sourceHtml = generatedMiniAppHtml({ title, prompt });\n generation = {\n provider: 'template',\n fallbackReason: 'ai_output_failed_security_scan',\n };\n scan = miniAppScan(permissions, sourceHtml);\n }\n const idSeed = crypto.randomUUID().replaceAll('-', '').slice(0, 12);\n const miniAppId = `sandbox.${idSeed}`;\n const createdAt = nowIso();\n const record = {\n ownerId: user.userId,\n miniAppId,\n botId: `bot.${miniAppId}`,\n title,\n subtitle,\n initials: title.slice(0, 1) || '小',\n icon: 'code',\n avatarColor: '#3D8BFF',\n greeting: `我是「${title}」的机器人。`,\n inputHint: '继续描述修改需求,或打开小程序面板',\n version: '0.0.1',\n permissions,\n surfaces: ['chatPanel'],\n theme: 'telegramDark',\n source: 'sandbox',\n reviewStatus: 'sandbox',\n sourceHtml,\n scan,\n generation,\n prompt,\n createdAt,\n updatedAt: createdAt,\n };\n const store = await readMiniAppStore();\n store.apps.push(record);\n await writeMiniAppStore(store);\n jsonResponse(res, 200, {\n success: true,\n miniApp: miniAppRecordToManifest(req, record),\n bot: miniAppRecordToBot(record),\n scan,\n generation,\n actions: ['openMiniApp', 'continueEditing', 'submitReview'],\n });\n }),\n);\n\napp.get('/api/ai/models', (_req, res) => {\n jsonResponse(res, 200, {\n success: true,\n provider: 'deepseek',\n defaultModel: deepseekModel,\n models: deepseekAvailableModels.map((model) => ({\n id: model,\n label: model === 'deepseek-reasoner' ? 'DeepSeek Reasoner' : 'DeepSeek Chat',\n })),\n });\n});\napp.get(\n '/api/ai/quota',\n asyncHandler(async (req, res) => {\n const user = await resolveUser(req, { username: req.query.username });\n const budget = enforceTokenBudget(user, 0);\n const usage = usageFor(user.userId);\n jsonResponse(res, 200, {\n success: true,\n isMember: user.isMember,\n monthlyLimit: budget.limit,\n usedTokens: usage.used,\n remainingTokens: Math.max(0, budget.limit - usage.used),\n month: usage.month,\n });\n }),\n);\n\napp.post(\n '/api/agent/chat',\n asyncHandler(async (req, res) => {\n const message = safeUserText(req.body?.message);\n if (!message) {\n return jsonResponse(res, 400, { success: false, message: 'message is required' });\n }\n\n const user = await resolveUser(req, req.body || {});\n const promptEstimate = estimateTokens(message);\n const estimated = promptEstimate + 600;\n const budget = enforceTokenBudget(user, estimated);\n const maxCompletionTokens = completionBudgetFor(budget, promptEstimate);\n const createdAt = nowIso();\n const conversationId = safeUserText(req.body?.conversationId) || crypto.randomUUID();\n const mode = normalizeAgentMode(req.body?.mode);\n const stream = req.body?.stream === true;\n let conversation = statements.getConversation.get(conversationId, user.userId);\n const isNewConversation = !conversation;\n\n if (isNewConversation) {\n conversation = { id: conversationId, user_id: user.userId, title: titleFromMessage(message) };\n statements.insertConversation.run({\n id: conversationId,\n userId: user.userId,\n title: conversation.title,\n provider: 'agent',\n model: openClawAgentId,\n createdAt,\n updatedAt: createdAt,\n });\n }\n\n const messageId = safeUserText(req.body?.messageId) || crypto.randomUUID();\n statements.insertMessage.run({\n id: messageId,\n conversationId,\n role: 'user',\n content: message,\n promptTokens: 0,\n completionTokens: 0,\n totalTokens: 0,\n createdAt,\n });\n\n const runId = `run_${crypto.randomUUID().replaceAll('-', '')}`;\n const sessionKey = openClawSessionKey(user, conversationId);\n const openClawRuntime = createOpenClawRuntime();\n statements.insertAgentRun.run({\n id: runId,\n userId: user.userId,\n conversationId,\n messageId,\n openClawRunId: null,\n openClawSessionKey: sessionKey,\n status: stream ? 'queued' : 'running',\n mode,\n provider: openClawRuntime.enabled ? openClawRuntime.provider : 'dacheng-ai-fallback',\n model: openClawRuntime.enabled ? openClawAgentId : deepseekModel,\n startedAt: createdAt,\n });\n\n if (stream) {\n return jsonResponse(res, 200, {\n success: true,\n runId,\n conversationId,\n streamUrl: `/api/agent/runs/${runId}/events`,\n });\n }\n\n const controller = new AbortController();\n activeAgentRuns.set(runId, controller);\n try {\n const skillResult = await runResourceFinderSkill({ message, user });\n const skillContext = resourceContextMessage(skillResult);\n const historyRows = statements.listMessages.all(conversationId).slice(-14);\n const modelMessages = [\n { role: 'system', content: systemPrompt },\n ...(skillContext ? [{ role: 'system', content: skillContext }] : []),\n ...normalizeMessages(historyRows),\n ];\n\n const aiResult = await runAgentModel({\n messages: modelMessages,\n user,\n conversationId,\n mode,\n signal: controller.signal,\n maxCompletionTokens,\n });\n const usage = {\n promptTokens: aiResult.usage?.promptTokens || estimateTokens(JSON.stringify(modelMessages)),\n completionTokens: aiResult.usage?.completionTokens || estimateTokens(aiResult.message),\n totalTokens:\n aiResult.usage?.totalTokens ||\n estimateTokens(JSON.stringify(modelMessages)) + estimateTokens(aiResult.message),\n };\n recordUsage(user.userId, usage.totalTokens);\n const answeredAt = nowIso();\n const assistantMessageId = crypto.randomUUID();\n statements.insertMessage.run({\n id: assistantMessageId,\n conversationId,\n role: 'assistant',\n content: aiResult.message,\n promptTokens: usage.promptTokens,\n completionTokens: usage.completionTokens,\n totalTokens: usage.totalTokens,\n createdAt: answeredAt,\n });\n statements.updateConversation.run({\n id: conversationId,\n userId: user.userId,\n title: isNewConversation ? titleFromMessage(message) : conversation.title,\n updatedAt: answeredAt,\n });\n updateAgentRunStatus({ id: runId, user_id: user.userId }, 'completed', {\n openClawRunId: aiResult.runId,\n inputTokens: usage.promptTokens,\n outputTokens: usage.completionTokens,\n });\n const action = determineClientAction(skillResult, message);\n return jsonResponse(res, 200, {\n success: true,\n runId,\n conversationId,\n messageId: assistantMessageId,\n provider: aiResult.provider,\n model: aiResult.model,\n message: aiResult.message,\n usage: agentUsagePayload(usage, user, budget),\n ...(action ? { clientAction: action } : {}),\n });\n } catch (error) {\n const status = error.name === 'AbortError' ? 'cancelled' : 'failed';\n updateAgentRunStatus({ id: runId, user_id: user.userId }, status, {\n errorCode: error.name || 'AGENT_RUN_FAILED',\n errorMessage: publicAiErrorMessage(error),\n });\n if (status === 'cancelled') {\n return jsonResponse(res, 499, { success: false, runId, status: 'cancelled' });\n }\n throw error;\n } finally {\n activeAgentRuns.delete(runId);\n }\n }),\n);\n\napp.get(\n '/api/agent/runs/:runId/events',\n asyncHandler(async (req, res) => {\n const user = await resolveUser(req, {});\n const run = statements.getAgentRun.get(req.params.runId, user.userId);\n if (!run) {\n return jsonResponse(res, 404, { success: false, message: 'run not found' });\n }\n if (run.status === 'completed' || run.status === 'failed' || run.status === 'cancelled') {\n return jsonResponse(res, 409, { success: false, message: `run is already ${run.status}` });\n }\n\n sseHeaders(res);\n const controller = new AbortController();\n activeAgentRuns.set(run.id, controller);\n updateAgentRunStatus(run, 'running');\n writeSse(res, 'run.started', { runId: run.id, conversationId: run.conversation_id });\n\n try {\n let streamedText = '';\n const modelMessages = buildAgentMessages(run.conversation_id);\n const lastUserMsg = [...modelMessages].reverse().find(m => m.role === 'user');\n const userMsgText = lastUserMsg ? lastUserMsg.content : '';\n const skillResult = userMsgText ? await runResourceFinderSkill({\n message: userMsgText,\n user,\n onStep: (step) => writeSse(res, 'tool.call.completed', {\n tool: 'agent.step',\n displayName: step.title || 'Agent step',\n summary: step.message || '',\n }),\n }) : null;\n const skillContext = resourceContextMessage(skillResult);\n const finalMessages = skillContext ? [\n modelMessages[0],\n { role: 'system', content: skillContext },\n ...modelMessages.slice(1)\n ] : modelMessages;\n\n const promptEstimate = estimateTokens(JSON.stringify(finalMessages));\n const estimated = promptEstimate + 600;\n const budget = enforceTokenBudget(user, estimated);\n const maxCompletionTokens = completionBudgetFor(budget, promptEstimate);\n const aiResult = await runAgentModel({\n messages: finalMessages,\n user,\n conversationId: run.conversation_id,\n mode: run.mode,\n signal: controller.signal,\n maxCompletionTokens,\n callbacks: {\n onStep: (step) => writeSse(res, 'tool.call.completed', {\n tool: 'agent.step',\n displayName: step.title || 'Agent step',\n summary: step.message || '',\n }),\n onToken: (delta) => {\n streamedText += delta;\n writeSse(res, 'assistant.delta', { text: delta });\n },\n },\n });\n const message = aiResult.message || streamedText.trim();\n const usage = {\n promptTokens: aiResult.usage?.promptTokens || estimateTokens(JSON.stringify(finalMessages)),\n completionTokens: aiResult.usage?.completionTokens || estimateTokens(message),\n totalTokens:\n aiResult.usage?.totalTokens || estimateTokens(JSON.stringify(finalMessages)) + estimateTokens(message),\n };\n recordUsage(user.userId, usage.totalTokens);\n const assistantMessageId = crypto.randomUUID();\n const answeredAt = nowIso();\n statements.insertMessage.run({\n id: assistantMessageId,\n conversationId: run.conversation_id,\n role: 'assistant',\n content: message,\n promptTokens: usage.promptTokens,\n completionTokens: usage.completionTokens,\n totalTokens: usage.totalTokens,\n createdAt: answeredAt,\n });\n statements.updateConversation.run({\n id: run.conversation_id,\n userId: user.userId,\n title: titleFromMessage(message),\n updatedAt: answeredAt,\n });\n updateAgentRunStatus(run, 'completed', {\n openClawRunId: aiResult.runId,\n inputTokens: usage.promptTokens,\n outputTokens: usage.completionTokens,\n });\n const action = determineClientAction(skillResult, userMsgText);\n writeSse(res, 'assistant.message', { messageId: assistantMessageId, content: message });\n writeSse(res, 'run.completed', {\n usage: agentUsagePayload(usage, user, budget),\n ...(action ? { clientAction: action } : {}),\n });\n res.end();\n } catch (error) {\n const status = error.name === 'AbortError' ? 'cancelled' : 'failed';\n updateAgentRunStatus(run, status, {\n errorCode: error.name || 'AGENT_RUN_FAILED',\n errorMessage: publicAiErrorMessage(error),\n });\n writeSse(res, status === 'cancelled' ? 'run.cancelled' : 'run.failed', {\n runId: run.id,\n message: publicAiErrorMessage(error),\n });\n res.end();\n } finally {\n activeAgentRuns.delete(run.id);\n }\n }),\n);\n\napp.post(\n '/api/agent/runs/:runId/cancel',\n asyncHandler(async (req, res) => {\n const user = await resolveUser(req, req.body || {});\n const run = statements.getAgentRun.get(req.params.runId, user.userId);\n if (!run) {\n return jsonResponse(res, 404, { success: false, message: 'run not found' });\n }\n const controller = activeAgentRuns.get(run.id);\n if (controller) controller.abort();\n updateAgentRunStatus(run, 'cancelled');\n return jsonResponse(res, 200, { success: true, runId: run.id, status: 'cancelled' });\n }),\n);\n\napp.post(\n '/api/agent/messages/:messageId/feedback',\n asyncHandler(async (req, res) => {\n const user = await resolveUser(req, req.body || {});\n const rating = safeUserText(req.body?.rating);\n if (!['up', 'down'].includes(rating)) {\n return jsonResponse(res, 400, { success: false, message: 'rating must be up or down' });\n }\n statements.insertAgentFeedback.run({\n id: crypto.randomUUID(),\n messageId: req.params.messageId,\n userId: user.userId,\n rating,\n reason: safeUserText(req.body?.reason),\n comment: safeUserText(req.body?.comment),\n createdAt: nowIso(),\n });\n return jsonResponse(res, 200, { success: true });\n }),\n);\n\napp.post(\n '/api/ai/chat',\n asyncHandler(async (req, res) => {\n const message = safeUserText(req.body?.message);\n if (!message) {\n return jsonResponse(res, 400, { success: false, message: 'message is required' });\n }\n\n const user = await resolveUser(req, req.body || {});\n const promptEstimate = estimateTokens(message);\n const estimated = promptEstimate + 600;\n const budget = enforceTokenBudget(user, estimated);\n const maxCompletionTokens = completionBudgetFor(budget, promptEstimate);\n const createdAt = nowIso();\n const conversationId = safeUserText(req.body?.conversationId) || crypto.randomUUID();\n const requestedModel = normalizeDeepSeekModelName(req.body?.model);\n const clientContext = clientContextMessage(req.body?.client);\n let conversation = statements.getConversation.get(conversationId, user.userId);\n const isNewConversation = !conversation;\n\n if (isNewConversation) {\n conversation = {\n id: conversationId,\n user_id: user.userId,\n title: titleFromMessage(message),\n };\n statements.insertConversation.run({\n id: conversationId,\n userId: user.userId,\n title: conversation.title,\n provider: 'deepseek',\n model: requestedModel,\n createdAt,\n updatedAt: createdAt,\n });\n }\n\n statements.insertMessage.run({\n id: crypto.randomUUID(),\n conversationId,\n role: 'user',\n content: message,\n promptTokens: 0,\n completionTokens: 0,\n totalTokens: 0,\n createdAt,\n });\n\n const libreChatRuntime = createLibreChatAgentRuntime();\n const skillResult = await runResourceFinderSkill({ message, user });\n const skillContext = resourceContextMessage(skillResult);\n const historyRows = statements.listMessages.all(conversationId).slice(-14);\n const modelMessages = [\n { role: 'system', content: systemPrompt },\n ...(clientContext ? [{ role: 'system', content: clientContext }] : []),\n ...(skillContext ? [{ role: 'system', content: skillContext }] : []),\n ...normalizeMessages(historyRows),\n ];\n\n const openClawRuntime = createOpenClawRuntime();\n const codexRuntime = createCodexDeepSeekRuntime();\n let provider = openClawRuntime.enabled\n ? openClawRuntime.provider\n : libreChatRuntime.enabled\n ? libreChatRuntime.provider\n : 'deepseek';\n let responseModel = openClawRuntime.enabled\n ? openClawRuntime.model\n : libreChatRuntime.enabled\n ? libreChatRuntime.model\n : requestedModel;\n let aiResult;\n try {\n if (openClawRuntime.enabled) {\n aiResult = await callOpenClawAgent({\n messages: modelMessages,\n user,\n conversationId,\n mode: normalizeAgentMode(req.body?.mode),\n });\n provider = aiResult.provider || provider;\n responseModel = aiResult.model || responseModel;\n } else if (libreChatRuntime.enabled) {\n aiResult = await callLibreChatAgent(modelMessages);\n } else if (codexRuntime.enabled) {\n provider = codexRuntime.provider;\n aiResult = await callCodexSdkDeepSeek(modelMessages);\n } else {\n aiResult = await callDeepSeek(modelMessages, { model: requestedModel, maxCompletionTokens });\n }\n } catch (error) {\n if (libreChatRuntime.enabled) {\n logger.warn({ error: String(error) }, 'LibreChat Agent chat failed, falling back to Codex SDK or DeepSeek direct');\n provider = codexRuntime.enabled ? codexRuntime.provider : 'deepseek';\n responseModel = requestedModel;\n if (codexRuntime.enabled) {\n try {\n aiResult = await callCodexSdkDeepSeek(modelMessages);\n } catch (codexError) {\n if (isDeepSeekQuotaError(codexError)) throw codexError;\n logger.warn({ error: String(codexError) }, 'Codex SDK chat failed, falling back to DeepSeek direct');\n provider = 'deepseek';\n aiResult = await callDeepSeek(modelMessages, { model: requestedModel, maxCompletionTokens });\n }\n } else {\n aiResult = await callDeepSeek(modelMessages, { model: requestedModel, maxCompletionTokens });\n }\n } else {\n if (!codexRuntime.enabled || isDeepSeekQuotaError(error)) throw error;\n logger.warn({ error: String(error) }, 'Codex SDK chat failed, falling back to DeepSeek direct');\n provider = 'deepseek';\n responseModel = requestedModel;\n aiResult = await callDeepSeek(modelMessages, { model: requestedModel, maxCompletionTokens });\n }\n }\n const usage = {\n promptTokens: aiResult.usage.promptTokens || estimateTokens(JSON.stringify(modelMessages)),\n completionTokens: aiResult.usage.completionTokens || estimateTokens(aiResult.message),\n totalTokens:\n aiResult.usage.totalTokens ||\n estimateTokens(JSON.stringify(modelMessages)) + estimateTokens(aiResult.message),\n };\n recordUsage(user.userId, usage.totalTokens);\n\n const answeredAt = nowIso();\n statements.insertMessage.run({\n id: crypto.randomUUID(),\n conversationId,\n role: 'assistant',\n content: aiResult.message,\n promptTokens: usage.promptTokens,\n completionTokens: usage.completionTokens,\n totalTokens: usage.totalTokens,\n createdAt: answeredAt,\n });\n statements.updateConversation.run({\n id: conversationId,\n userId: user.userId,\n title: isNewConversation ? titleFromMessage(message) : conversation.title,\n updatedAt: answeredAt,\n });\n\n const latestUsage = usageFor(user.userId);\n const monthlyLimit = budget.limit;\n const action = determineClientAction(skillResult, message);\n jsonResponse(res, 200, {\n success: true,\n conversationId,\n provider,\n model: responseModel,\n message: aiResult.message,\n usage: {\n ...usage,\n monthlyLimit,\n remainingTokens: Math.max(0, monthlyLimit - latestUsage.used),\n },\n ...(action ? { clientAction: action } : {}),\n });\n }),\n);\n\napp.post('/api/ai/chat/stream', async (req, res) => {\n sseHeaders(res);\n\n try {\n const message = safeUserText(req.body?.message);\n if (!message) {\n writeSse(res, 'error', { message: 'message is required' });\n res.end();\n return;\n }\n\n const user = await resolveUser(req, req.body || {});\n const promptEstimate = estimateTokens(message);\n const estimated = promptEstimate + 600;\n const budget = enforceTokenBudget(user, estimated);\n const maxCompletionTokens = completionBudgetFor(budget, promptEstimate);\n const createdAt = nowIso();\n const conversationId = safeUserText(req.body?.conversationId) || crypto.randomUUID();\n const requestedModel = normalizeDeepSeekModelName(req.body?.model);\n const clientContext = clientContextMessage(req.body?.client);\n let conversation = statements.getConversation.get(conversationId, user.userId);\n const isNewConversation = !conversation;\n\n if (isNewConversation) {\n conversation = {\n id: conversationId,\n user_id: user.userId,\n title: titleFromMessage(message),\n };\n statements.insertConversation.run({\n id: conversationId,\n userId: user.userId,\n title: conversation.title,\n provider: 'deepseek',\n model: requestedModel,\n createdAt,\n updatedAt: createdAt,\n });\n }\n\n statements.insertMessage.run({\n id: crypto.randomUUID(),\n conversationId,\n role: 'user',\n content: message,\n promptTokens: 0,\n completionTokens: 0,\n totalTokens: 0,\n createdAt,\n });\n\n const codexRuntime = createCodexDeepSeekRuntime();\n const libreChatRuntime = createLibreChatAgentRuntime();\n const initialOpenClawRuntime = createOpenClawRuntime();\n let provider = initialOpenClawRuntime.enabled\n ? initialOpenClawRuntime.provider\n : libreChatRuntime.enabled\n ? libreChatRuntime.provider\n : codexRuntime.enabled\n ? codexRuntime.provider\n : 'deepseek';\n let responseModel = initialOpenClawRuntime.enabled\n ? initialOpenClawRuntime.model\n : libreChatRuntime.enabled\n ? libreChatRuntime.model\n : requestedModel;\n writeSse(res, 'meta', {\n conversationId,\n provider,\n model: responseModel,\n });\n const skillResult = await runResourceFinderSkill({\n message,\n user,\n onStep: (step) => writeSse(res, 'step', step),\n });\n const skillContext = resourceContextMessage(skillResult);\n const historyRows = statements.listMessages.all(conversationId).slice(-14);\n const modelMessages = [\n { role: 'system', content: systemPrompt },\n ...(clientContext ? [{ role: 'system', content: clientContext }] : []),\n ...(skillContext ? [{ role: 'system', content: skillContext }] : []),\n ...normalizeMessages(historyRows),\n ];\n\n let aiResult;\n const openClawRuntime = initialOpenClawRuntime;\n try {\n if (openClawRuntime.enabled) {\n aiResult = await callOpenClawAgent({\n messages: modelMessages,\n user,\n conversationId,\n mode: normalizeAgentMode(req.body?.mode),\n });\n provider = aiResult.provider || provider;\n responseModel = aiResult.model || responseModel;\n writeSse(res, 'delta', { text: aiResult.message });\n } else if (libreChatRuntime.enabled) {\n aiResult = await callLibreChatAgentStream(modelMessages, {\n onToken: (token) => writeSse(res, 'delta', { text: token }),\n });\n } else if (codexRuntime.enabled) {\n aiResult = await callCodexSdkDeepSeek(modelMessages, {\n onToken: (token) => writeSse(res, 'delta', { text: token }),\n onStep: (step) => writeSse(res, 'step', step),\n });\n } else {\n aiResult = await callDeepSeekStream(modelMessages, {\n model: requestedModel,\n maxCompletionTokens,\n onToken: (token) => writeSse(res, 'delta', { text: token }),\n });\n }\n } catch (error) {\n if (libreChatRuntime.enabled) {\n logger.warn({ error: String(error) }, 'LibreChat Agent streaming chat failed, falling back to Codex SDK or DeepSeek direct');\n provider = codexRuntime.enabled ? codexRuntime.provider : 'deepseek';\n responseModel = requestedModel;\n writeSse(res, 'meta', {\n conversationId,\n provider,\n model: responseModel,\n });\n if (codexRuntime.enabled) {\n try {\n aiResult = await callCodexSdkDeepSeek(modelMessages, {\n onToken: (token) => writeSse(res, 'delta', { text: token }),\n onStep: (step) => writeSse(res, 'step', step),\n });\n } catch (codexError) {\n if (isDeepSeekQuotaError(codexError)) throw codexError;\n logger.warn({ error: String(codexError) }, 'Codex SDK streaming chat failed, falling back to DeepSeek direct');\n provider = 'deepseek';\n responseModel = requestedModel;\n writeSse(res, 'meta', {\n conversationId,\n provider,\n model: responseModel,\n });\n aiResult = await callDeepSeekStream(modelMessages, {\n model: requestedModel,\n maxCompletionTokens,\n onToken: (token) => writeSse(res, 'delta', { text: token }),\n });\n }\n } else {\n aiResult = await callDeepSeekStream(modelMessages, {\n model: requestedModel,\n maxCompletionTokens,\n onToken: (token) => writeSse(res, 'delta', { text: token }),\n });\n }\n } else {\n if (!codexRuntime.enabled || isDeepSeekQuotaError(error)) throw error;\n logger.warn({ error: String(error) }, 'Codex SDK streaming chat failed, falling back to DeepSeek direct');\n provider = 'deepseek';\n responseModel = requestedModel;\n writeSse(res, 'meta', {\n conversationId,\n provider,\n model: responseModel,\n });\n aiResult = await callDeepSeekStream(modelMessages, {\n model: requestedModel,\n maxCompletionTokens,\n onToken: (token) => writeSse(res, 'delta', { text: token }),\n });\n }\n }\n\n const usage = {\n promptTokens: aiResult.usage.promptTokens || estimateTokens(JSON.stringify(modelMessages)),\n completionTokens: aiResult.usage.completionTokens || estimateTokens(aiResult.message),\n totalTokens:\n aiResult.usage.totalTokens ||\n estimateTokens(JSON.stringify(modelMessages)) + estimateTokens(aiResult.message),\n };\n recordUsage(user.userId, usage.totalTokens);\n\n const answeredAt = nowIso();\n statements.insertMessage.run({\n id: crypto.randomUUID(),\n conversationId,\n role: 'assistant',\n content: aiResult.message,\n promptTokens: usage.promptTokens,\n completionTokens: usage.completionTokens,\n totalTokens: usage.totalTokens,\n createdAt: answeredAt,\n });\n statements.updateConversation.run({\n id: conversationId,\n userId: user.userId,\n title: isNewConversation ? titleFromMessage(message) : conversation.title,\n updatedAt: answeredAt,\n });\n\n const latestUsage = usageFor(user.userId);\n const monthlyLimit = budget.limit;\n const action = determineClientAction(skillResult, message);\n writeSse(res, 'done', {\n conversationId,\n provider,\n model: responseModel,\n message: aiResult.message,\n usage: {\n ...usage,\n monthlyLimit,\n remainingTokens: Math.max(0, monthlyLimit - latestUsage.used),\n },\n ...(action ? { clientAction: action } : {}),\n });\n res.end();\n } catch (error) {\n logger.error({ error: error.stack || String(error) }, 'streaming chat failed');\n writeSse(res, 'error', {\n message: publicAiErrorMessage(error),\n details: error.details,\n });\n res.end();\n }\n});\n\napp.get(\n '/api/ai/conversations',\n asyncHandler(async (req, res) => {\n const user = await resolveUser(req, { username: req.query.username });\n const items = statements.listConversations.all(user.userId).map((item) => ({\n id: item.id,\n title: item.title,\n updatedAt: item.updatedAt,\n }));\n jsonResponse(res, 200, { success: true, items });\n }),\n);\n\napp.get(\n '/api/ai/conversations/:id',\n asyncHandler(async (req, res) => {\n const user = await resolveUser(req);\n const conversation = statements.getConversation.get(req.params.id, user.userId);\n if (!conversation) {\n return jsonResponse(res, 404, { success: false, message: 'conversation not found' });\n }\n const messages = statements.listMessages.all(req.params.id).map((item) => ({\n role: item.role,\n content: item.content,\n createdAt: item.createdAt,\n }));\n jsonResponse(res, 200, {\n success: true,\n conversation: {\n id: conversation.id,\n title: conversation.title,\n updatedAt: conversation.updated_at,\n },\n messages,\n });\n }),\n);\n\nfunction buildUrl(base, endpoint, params = {}) {\n const url = new URL(endpoint, `${base}/`);\n for (const [key, value] of Object.entries(params)) {\n if (Array.isArray(value)) {\n for (const item of value) {\n if (item !== undefined && item !== null && item !== '') {\n url.searchParams.append(key, String(item));\n }\n }\n } else if (value !== undefined && value !== null && value !== '') {\n url.searchParams.set(key, String(value));\n }\n }\n return url;\n}\n\nasync function fetchJson(url) {\n const response = await fetch(url, {\n headers: { Accept: 'application/json' },\n signal: AbortSignal.timeout(15_000),\n });\n if (!response.ok) {\n throw new Error(`Request failed: ${response.status}`);\n }\n return response.json();\n}\n\nfunction stripHtml(value) {\n return String(value || '')\n .replace(/<script[\\s\\S]*?<\\/script>/gi, ' ')\n .replace(/<style[\\s\\S]*?<\\/style>/gi, ' ')\n .replace(/<[^>]+>/g, ' ')\n .replace(/ /g, ' ')\n .replace(/</g, '<')\n .replace(/>/g, '>')\n .replace(/&/g, '&')\n .replace(/&#(\\d+);/g, (_match, code) => String.fromCharCode(Number(code)))\n .replace(/\\s+/g, ' ')\n .trim();\n}\n\nfunction cleanExtractedText(value) {\n let text = String(value || '')\n .replace(/\\b[A-Z]{1,3}\\d{2}n\\d{4,5}[a-z]?_p?\\d{4}[a-z]\\d{2}\\b/g, ' ')\n .replace(/\\uFFFD/g, '')\n .replace(/\\s+([,。;:!?、」』)》】])/g, '$1')\n .replace(/([「『《(【])\\s+/g, '$1');\n for (let i = 0; i < 3; i += 1) {\n text = text.replace(/([\\u3400-\\u9fff])\\s+([\\u3400-\\u9fff])/g, '$1$2');\n }\n return text.replace(/[ \\t]{2,}/g, ' ').trim();\n}\n\nfunction compactText(value, limit = 140) {\n const text = stripHtml(value);\n return text.length > limit ? `${text.slice(0, limit)}...` : text;\n}\n\nfunction collectSearchResults(data) {\n if (!data || typeof data !== 'object') return [];\n if (Array.isArray(data.results)) return data.results;\n if (Array.isArray(data?.results?.docs)) return data.results.docs;\n if (Array.isArray(data?.response?.docs)) return data.response.docs;\n if (Array.isArray(data.docs)) return data.docs;\n return [];\n}\n\nfunction firstText(item, keys) {\n for (const key of keys) {\n const value = item?.[key];\n if (Array.isArray(value)) {\n const first = value.map((entry) => safeUserText(String(entry))).find(Boolean);\n if (first) return first;\n }\n if (typeof value === 'string' && value.trim()) return value.trim();\n if (typeof value === 'number') return String(value);\n }\n return '';\n}\n\nfunction firstNumber(item, keys, fallback = undefined) {\n for (const key of keys) {\n const value = item?.[key];\n if (typeof value === 'number') return value;\n if (typeof value === 'string' && value.trim() && Number.isFinite(Number(value))) {\n return Number(value);\n }\n }\n return fallback;\n}\n\nasync function searchCbetaTitle(query, limit) {\n const url = buildUrl(cbetaApiRoot, 'search/title', { q: query, start: 0, rows: limit });\n const data = await fetchJson(url);\n return collectSearchResults(data).flatMap((item) => {\n const work = firstText(item, ['work', 'work_id', 'workId']);\n const title = firstText(item, ['content', 'title', 'work_title', 'workTitle']);\n if (!work || !title) return [];\n const juan = firstNumber(item, ['juan', 'juan_num', 'juanNum'], 1);\n return [\n {\n id: `cbeta-title-${work}-${juan}`,\n title,\n sourceName: 'CBETA 佛典',\n url: `cbeta:${work}:${juan}`,\n snippet: firstText(item, ['byline', 'creators', 'time_dynasty']) || '可下载经文正文并用于全球法布施。',\n resourceType: 'scripture',\n work,\n juan,\n },\n ];\n });\n}\n\nasync function searchCbetaContent(query, limit) {\n const endpoints = ['search/fulltext', 'search/content', 'search/all_in_one'];\n const settled = await Promise.allSettled(\n endpoints.map((endpoint) =>\n fetchJson(buildUrl(cbetaApiRoot, endpoint, { q: query, start: 0, rows: limit, field: 'content' })),\n ),\n );\n const results = [];\n for (const response of settled) {\n if (response.status !== 'fulfilled') continue;\n for (const item of collectSearchResults(response.value)) {\n const work = firstText(item, ['work', 'work_id', 'workId', 'sutra', 'id']);\n const snippet = compactText(firstText(item, ['kwic', 'highlight', 'snippet', 'content', 'text', 'body', 'p']));\n if (!work || !snippet) continue;\n const juan = firstNumber(item, ['juan', 'juan_num', 'juanNum', '卷'], 1);\n const title = firstText(item, ['title', 'work_title', 'workTitle', 'sutra_name', 'book', 'name']) || work;\n results.push({\n id: `cbeta-content-${work}-${juan}-${sha256(snippet).slice(0, 8)}`,\n title,\n sourceName: 'CBETA 佛典',\n url: `cbeta:${work}:${juan}`,\n snippet,\n resourceType: 'scripture',\n work,\n juan,\n });\n if (results.length >= limit) return results;\n }\n }\n return results;\n}\n\nfunction directUrlResource(query) {\n const url = safeUserText(query);\n let parsed;\n try {\n parsed = new URL(url);\n } catch {\n return null;\n }\n if (!['http:', 'https:'].includes(parsed.protocol)) return null;\n return {\n id: `url-${sha256(url).slice(0, 12)}`,\n title: parsed.hostname,\n sourceName: '网页资源',\n url,\n snippet: '直接下载并提取这个链接的可读正文。',\n resourceType: 'web',\n };\n}\n\nfunction looksLikeAudioQuery(query) {\n return /音乐|音樂|音频|音訊|mp3|audio|念诵|念誦|诵经|誦經|唱诵|唱誦|读诵|讀誦/.test(\n safeUserText(query).toLowerCase(),\n );\n}\n\nfunction archiveSearchTerm(query) {\n return (\n safeUserText(query)\n .replace(/下载|查找|寻找|找一?个|资源|音频|音訊|音乐|音樂|mp3|audio|念诵|念誦|诵经|誦經|唱诵|唱誦|读诵|讀誦/gi, ' ')\n .replace(/\\s+/g, ' ')\n .trim() || safeUserText(query)\n );\n}\n\nfunction solrPhrase(value) {\n return String(value).replace(/\\\\/g, '\\\\\\\\').replace(/\"/g, '\\\\\"');\n}\n\nasync function searchInternetArchiveAudio(query, limit) {\n if (!looksLikeAudioQuery(query)) return [];\n const term = archiveSearchTerm(query);\n if (!term) return [];\n const variants = Array.from(\n new Set([\n term,\n term.replaceAll('金刚经', '金剛經'),\n term.replaceAll('金剛經', '金刚经'),\n ].filter(Boolean)),\n );\n const phraseQuery = variants\n .flatMap((item) => [`title:\"${solrPhrase(item)}\"`, `description:\"${solrPhrase(item)}\"`])\n .join(' OR ');\n const url = buildUrl('https://archive.org', 'advancedsearch.php', {\n q: `(${phraseQuery}) AND mediatype:audio`,\n rows: Math.min(Math.max(limit, 1), 10),\n output: 'json',\n 'fl[]': ['identifier', 'title', 'description'],\n });\n const data = await fetchJson(url);\n const docs = Array.isArray(data?.response?.docs) ? data.response.docs : [];\n return docs\n .map((item) => {\n const identifier = firstText(item, ['identifier']);\n const title = firstText(item, ['title']) || identifier;\n if (!identifier || !title) return null;\n return {\n id: `ia-audio-${identifier}`,\n title,\n sourceName: 'Internet Archive 音频',\n url: `ia:${identifier}`,\n snippet: compactText(firstText(item, ['description'])) || '公开可访问的音频资源,将下载第一个可用 MP3 文件。',\n resourceType: 'audio',\n identifier,\n };\n })\n .filter(Boolean);\n}\n\nconst curatedCbetaWorks = [\n {\n work: 'T0235',\n juan: 1,\n title: '金剛般若波羅蜜經',\n keywords: ['金剛般若波羅蜜經', '金刚般若波罗蜜经', '金剛經', '金刚经'],\n snippet: '姚秦 鳩摩羅什譯。常用于读诵、共修与法布施的般若经典。',\n },\n {\n work: 'T0251',\n juan: 1,\n title: '般若波羅蜜多心經',\n keywords: ['般若波羅蜜多心經', '般若波罗蜜多心经', '心經', '心经'],\n snippet: '唐 玄奘譯。篇幅短,适合加入禅室功课本与日常持诵。',\n },\n {\n work: 'T0262',\n juan: 1,\n title: '妙法蓮華經',\n keywords: ['妙法蓮華經', '妙法莲华经', '法華經', '法华经'],\n snippet: '姚秦 鳩摩羅什譯。可按卷下载正文,用于长期功课与分享。',\n },\n {\n work: 'T0279',\n juan: 1,\n title: '大方廣佛華嚴經',\n keywords: ['大方廣佛華嚴經', '大方广佛华严经', '華嚴經', '华严经'],\n snippet: '唐 實叉難陀譯。可按卷下载正文,用于系统研读与法布施。',\n },\n {\n work: 'T0360',\n juan: 1,\n title: '佛說無量壽經',\n keywords: ['佛說無量壽經', '佛说无量寿经', '無量壽經', '无量寿经'],\n snippet: '净土经典。可下载正文,用于净土功课本与全球法布施。',\n },\n {\n work: 'T0365',\n juan: 1,\n title: '佛說觀無量壽佛經',\n keywords: ['佛說觀無量壽佛經', '佛说观无量寿佛经', '觀無量壽經', '观无量寿经'],\n snippet: '净土三经之一。适合查找下载后加入净土功课。',\n },\n {\n work: 'T0366',\n juan: 1,\n title: '佛說阿彌陀經',\n keywords: ['佛說阿彌陀經', '佛说阿弥陀经', '阿彌陀經', '阿弥陀经'],\n snippet: '姚秦 鳩摩羅什譯。篇幅适中,适合共修、持诵与分享。',\n },\n {\n work: 'T0475',\n juan: 1,\n title: '維摩詰所說經',\n keywords: ['維摩詰所說經', '维摩诘所说经', '維摩詰經', '维摩诘经'],\n snippet: '姚秦 鳩摩羅什譯。大乘经典,可按卷下载用于研读。',\n },\n {\n work: 'T0676',\n juan: 1,\n title: '解深密經',\n keywords: ['解深密經', '解深密经'],\n snippet: '唯识相关大乘经典。适合研读、整理笔记与法布施。',\n },\n {\n work: 'T1666',\n juan: 1,\n title: '大乘起信論',\n keywords: ['大乘起信論', '大乘起信论', '起信論', '起信论'],\n snippet: '大乘论典。适合下载后作为研习功课资料。',\n },\n];\n\nfunction localCbetaResults(query, limit) {\n const normalizedQuery = safeUserText(query).toLowerCase();\n if (!normalizedQuery) return [];\n return curatedCbetaWorks\n .filter((item) =>\n item.keywords.some((keyword) => {\n const normalizedKeyword = keyword.toLowerCase();\n return normalizedKeyword.includes(normalizedQuery) || normalizedQuery.includes(normalizedKeyword);\n }),\n )\n .slice(0, limit)\n .map((item) => ({\n id: `cbeta-curated-${item.work}-${item.juan}`,\n title: item.title,\n sourceName: 'CBETA 佛典',\n url: `cbeta:${item.work}:${item.juan}`,\n snippet: item.snippet,\n resourceType: 'scripture',\n work: item.work,\n juan: item.juan,\n }));\n}\n\nfunction uniqueResources(items, limit) {\n const seen = new Set();\n const results = [];\n for (const item of items) {\n const key = `${item.url}:${item.title}`;\n if (seen.has(key)) continue;\n seen.add(key);\n results.push(item);\n if (results.length >= limit) break;\n }\n return results;\n}\n\nasync function findResourceCandidates(query, limit) {\n const direct = directUrlResource(query);\n const results = [];\n if (direct) results.push(direct);\n try {\n results.push(...(await searchInternetArchiveAudio(query, limit)));\n } catch (error) {\n logger.warn({ error: String(error), query }, 'Internet Archive audio search skipped');\n }\n results.push(...localCbetaResults(query, limit));\n\n const [titleResults, contentResults] = await Promise.allSettled([\n searchCbetaTitle(query, limit),\n searchCbetaContent(query, limit),\n ]);\n if (titleResults.status === 'fulfilled') results.push(...titleResults.value);\n if (contentResults.status === 'fulfilled') results.push(...contentResults.value);\n\n return uniqueResources(results, limit);\n}\n\nfunction looksLikeResourceTask(message) {\n const text = safeUserText(message).toLowerCase();\n if (!text) return false;\n return /下载|查找|寻找|找一?个|资源|經文|经文|佛经|佛經|仪轨|儀軌|音乐|音樂|mp3|audio|念诵|念誦|诵经|誦經/.test(text);\n}\n\nfunction extractResourceQuery(message) {\n const text = safeUserText(message)\n .replace(/请|請|帮我|幫我|麻烦|麻煩|可以|能不能|需要|我要|想要/g, ' ')\n .replace(/下载|下載|查找|寻找|尋找|找一个|找一個|找|资源|資源|并|並|然后|然後|用于|用於|全球法布施|加入功课本|加入功課本/g, ' ')\n .replace(/\\s+/g, ' ')\n .trim();\n return text || safeUserText(message);\n}\n\nfunction resourceContextMessage(skillResult) {\n if (!skillResult) return '';\n const lines = [\n '本轮资源检索/下载已经由大乘 App 后端完成。',\n '不要再次调用 search_dharma_resources、download_dharma_resource 或其他资源 MCP 工具;请直接基于以下结果回答用户。',\n `后端已调用 skill: ${skillResult.skillName}`,\n `执行目标: ${skillResult.query}`,\n ];\n if (skillResult.selected) {\n lines.push(`候选资源: ${skillResult.selected.title}`);\n lines.push(`来源: ${skillResult.selected.sourceName}`);\n lines.push(`资源地址: ${skillResult.selected.url}`);\n }\n if (skillResult.downloaded) {\n lines.push(`下载标题: ${skillResult.downloaded.title}`);\n lines.push(`下载文件: ${skillResult.downloaded.fileName}`);\n const text = safeUserText(skillResult.downloaded.contentText);\n if (text) {\n lines.push('下载正文摘录:');\n lines.push(text.slice(0, 8000));\n }\n }\n return lines.join('\\n');\n}\n\nfunction determineClientAction(skillResult, message) {\n if (skillResult && skillResult.downloaded) {\n const text = safeUserText(message);\n if (/法布施|全球法布施|分享/.test(text)) {\n return {\n type: 'prepare_dharma_share_text',\n title: skillResult.downloaded.title,\n text: skillResult.downloaded.contentText || '',\n };\n }\n }\n return null;\n}\n\nasync function runResourceFinderSkill({ message, user, onStep }) {\n if (!looksLikeResourceTask(message)) return null;\n\n const skillName = 'resource-finder-downloader';\n const query = extractResourceQuery(message);\n const audioTask = looksLikeAudioQuery(message) || looksLikeAudioQuery(query);\n onStep?.({\n type: 'skill.step',\n skillName,\n stage: 'start',\n title: '调用资源查找下载 skill',\n message: `识别到资源任务: ${query}`,\n });\n\n onStep?.({\n type: 'skill.step',\n skillName,\n stage: 'search',\n title: '搜索可分享资源',\n message: audioTask\n ? '优先检索公开可访问音频资源,失败时再回落到佛典正文资源。'\n : '优先检索 CBETA 佛典正文接口,搜索索引不可用时使用内置经典映射。',\n });\n const candidates = await findResourceCandidates(query, 8);\n if (candidates.length === 0) {\n onStep?.({\n type: 'skill.step',\n skillName,\n stage: 'empty',\n title: '没有找到可下载资源',\n message: '后端没有找到明确可下载且可分享的资源。',\n });\n return {\n skillName,\n query,\n candidates: [],\n selected: null,\n downloaded: null,\n };\n }\n\n const selected = candidates[0];\n onStep?.({\n type: 'skill.step',\n skillName,\n stage: 'select',\n title: '选择可信候选资源',\n message: `${selected.title} / ${selected.sourceName}`,\n });\n\n onStep?.({\n type: 'skill.step',\n skillName,\n stage: 'download',\n title: '下载并提取资源',\n message: selected.url,\n });\n const cbeta = parseCbetaResource(selected);\n const internetArchive = parseInternetArchiveResource(selected);\n const downloaded = cbeta\n ? await downloadCbetaResource({ ...selected, ...cbeta })\n : internetArchive\n ? await downloadInternetArchiveAudio({ ...selected, ...internetArchive })\n : await downloadWebResource(selected);\n const persisted = persistDownloadedResource(user, downloaded);\n\n onStep?.({\n type: 'skill.step',\n skillName,\n stage: 'done',\n title: '资源已准备完成',\n message: downloaded.fileName,\n });\n\n return {\n skillName,\n query,\n candidates,\n selected,\n downloaded: {\n ...downloaded,\n downloadId: persisted.id,\n },\n };\n}\n\napp.post(\n '/api/resources/search',\n asyncHandler(async (req, res) => {\n const query = safeUserText(req.body?.query);\n const limit = Math.min(Math.max(Number(req.body?.limit || 12), 1), 20);\n if (query.length < 2) {\n return jsonResponse(res, 400, { success: false, message: 'query is required' });\n }\n\n const items = await findResourceCandidates(query, limit);\n\n jsonResponse(res, 200, {\n success: true,\n source: 'resource-index',\n items,\n });\n }),\n);\n\nfunction parseCbetaResource(input) {\n const work = safeUserText(input.work);\n const juan = Number(input.juan || 1);\n if (work) return { work, juan: Number.isFinite(juan) ? juan : 1 };\n\n const url = safeUserText(input.url);\n const match = /^cbeta:([^:]+):(\\d+)$/i.exec(url);\n if (!match) return null;\n return {\n work: match[1].toUpperCase(),\n juan: Number(match[2] || 1),\n };\n}\n\nfunction parseInternetArchiveResource(input) {\n const url = safeUserText(input.url);\n const match = /^ia:([^:]+)$/i.exec(url);\n const identifier = match?.[1] || safeUserText(input.identifier);\n if (!identifier) return null;\n return { identifier };\n}\n\nfunction safeFileName(value) {\n const normalized = String(value || 'dharma-resource')\n .replace(/[\\\\/:*?\"<>|]+/g, '-')\n .replace(/\\s+/g, '-')\n .replace(/-+/g, '-')\n .replace(/^-|-$/g, '')\n .slice(0, 80);\n return normalized || 'dharma-resource';\n}\n\nfunction extractCbetaHtml(data) {\n const first = Array.isArray(data?.results) ? data.results[0] : null;\n if (typeof first === 'string') return first;\n if (first && typeof first === 'object' && typeof first.html === 'string') return first.html;\n if (typeof data?.html === 'string') return data.html;\n return '';\n}\n\nasync function downloadCbetaResource(resource) {\n const url = buildUrl(cbetaApiRoot, 'juans', {\n work: resource.work,\n juan: resource.juan,\n work_info: 1,\n toc: 1,\n });\n const data = await fetchJson(url);\n const workInfo = data.work_info || {};\n const title = safeUserText(workInfo.title) || safeUserText(resource.title) || resource.work;\n const html = extractCbetaHtml(data);\n const text = cleanExtractedText(stripHtml(html));\n if (!text) throw new Error('CBETA resource returned empty content');\n return {\n title,\n sourceName: 'CBETA 佛典',\n url: `cbeta:${resource.work}:${resource.juan}`,\n contentText: text.slice(0, maxResourceTextChars),\n fileName: `${resource.work}_${resource.juan}_${safeFileName(title)}.txt`,\n };\n}\n\nasync function downloadInternetArchiveAudio(resource) {\n const identifier = safeUserText(resource.identifier);\n if (!identifier) throw new Error('Internet Archive identifier is required');\n const metadata = await fetchJson(`https://archive.org/metadata/${encodeURIComponent(identifier)}`);\n const title = safeUserText(metadata?.metadata?.title) || safeUserText(resource.title) || identifier;\n const files = Array.isArray(metadata?.files) ? metadata.files : [];\n const audioFile = files.find((file) => {\n const name = safeUserText(file?.name);\n const format = safeUserText(file?.format).toLowerCase();\n return /\\.(mp3|m4a|ogg)$/i.test(name) || format.includes('mp3');\n });\n if (!audioFile?.name) {\n throw new Error('Internet Archive audio item has no downloadable audio file');\n }\n const sourceFileName = audioFile.name;\n const fileUrl = `https://archive.org/download/${encodeURIComponent(identifier)}/${sourceFileName\n .split('/')\n .map((part) => encodeURIComponent(part))\n .join('/')}`;\n const response = await fetch(fileUrl, {\n headers: {\n Accept: 'audio/mpeg, audio/*;q=0.9, */*;q=0.5',\n 'User-Agent': 'DachengResourceDownloader/1.0 (+https://ombhrum.com)',\n },\n signal: AbortSignal.timeout(60_000),\n });\n if (!response.ok) {\n throw new Error(`Internet Archive audio download failed: ${response.status}`);\n }\n const binaryContent = Buffer.from(await response.arrayBuffer());\n if (!binaryContent.length) throw new Error('Internet Archive audio returned empty content');\n const sizeMb = (binaryContent.length / 1024 / 1024).toFixed(1);\n return {\n title,\n sourceName: 'Internet Archive 音频',\n url: fileUrl,\n contentText: `已下载音频资源: ${title}\\n文件名: ${sourceFileName}\\n大小: ${sizeMb} MB\\n来源: ${fileUrl}`,\n binaryContent,\n fileName: safeFileName(sourceFileName),\n };\n}\n\nasync function downloadWebResource(input) {\n const url = safeUserText(input.url);\n const parsed = new URL(url);\n if (!['http:', 'https:'].includes(parsed.protocol)) {\n throw new Error('Only http/https resources can be downloaded');\n }\n\n const response = await fetch(parsed, {\n headers: {\n Accept: 'text/html, text/plain, application/xhtml+xml, application/xml;q=0.8, */*;q=0.5',\n 'User-Agent': 'DachengResourceDownloader/1.0 (+https://ombhrum.com)',\n },\n signal: AbortSignal.timeout(25_000),\n });\n if (!response.ok) throw new Error(`Resource download failed: ${response.status}`);\n const contentType = response.headers.get('content-type') || '';\n const body = await response.text();\n const title = safeUserText(input.title) || parsed.hostname;\n const contentText = contentType.includes('html') ? cleanExtractedText(stripHtml(body)) : body.trim();\n if (!contentText) throw new Error('Resource returned empty content');\n return {\n title,\n sourceName: safeUserText(input.sourceName) || parsed.hostname,\n url,\n contentText: contentText.slice(0, maxResourceTextChars),\n fileName: `${safeFileName(title)}.txt`,\n };\n}\n\nfunction persistDownloadedResource(user, content) {\n const id = crypto.randomUUID();\n const fileName = `${id}-${safeFileName(content.fileName)}`;\n const filePath = path.join(resourcesDir, fileName);\n if (content.binaryContent) {\n fs.writeFileSync(filePath, content.binaryContent);\n } else {\n fs.writeFileSync(filePath, content.contentText, 'utf8');\n }\n statements.insertResourceDownload.run({\n id,\n userId: user.userId,\n title: content.title,\n sourceName: content.sourceName,\n sourceUrl: content.url,\n fileName: content.fileName,\n filePath,\n createdAt: nowIso(),\n });\n return { id, fileName, filePath };\n}\n\nfunction createLibreChatMcpServer() {\n const server = new McpServer({\n name: 'dacheng-ai-tools',\n version: '0.1.0',\n });\n\n server.registerTool(\n 'search_dharma_resources',\n {\n title: 'Search dharma resources',\n description:\n 'Search for legally shareable Buddhist scripture, text, web, or audio resources that can be prepared for Fabushi sharing.',\n inputSchema: {\n query: z.string().min(2).max(200),\n limit: z.number().int().min(1).max(12).optional(),\n },\n outputSchema: {\n query: z.string(),\n items: z.array(\n z.object({\n id: z.string(),\n title: z.string(),\n sourceName: z.string(),\n url: z.string(),\n snippet: z.string(),\n resourceType: z.string(),\n work: z.string().optional(),\n juan: z.number().optional(),\n }),\n ),\n },\n annotations: {\n readOnlyHint: true,\n destructiveHint: false,\n openWorldHint: true,\n },\n },\n async ({ query, limit }) => {\n const items = await findResourceCandidates(query, limit ?? 8);\n return textToolResult(`Found ${items.length} candidate resources for \"${query}\".`, {\n query,\n items,\n });\n },\n );\n\n server.registerTool(\n 'download_dharma_resource',\n {\n title: 'Download dharma resource',\n description:\n 'Download and extract a selected resource. Use the url/work/juan returned by search_dharma_resources whenever possible.',\n inputSchema: {\n url: z.string().min(1).max(1000),\n title: z.string().max(300).optional(),\n sourceName: z.string().max(120).optional(),\n work: z.string().max(40).optional(),\n juan: z.number().int().min(1).optional(),\n identifier: z.string().max(300).optional(),\n },\n outputSchema: {\n title: z.string(),\n sourceName: z.string(),\n url: z.string(),\n fileName: z.string(),\n downloadId: z.string(),\n contentText: z.string(),\n },\n annotations: {\n readOnlyHint: false,\n destructiveHint: false,\n openWorldHint: true,\n },\n },\n async (input) => {\n const user = { userId: 'librechat:mcp' };\n const cbeta = parseCbetaResource(input);\n const internetArchive = parseInternetArchiveResource(input);\n const content = cbeta\n ? await downloadCbetaResource({ ...input, ...cbeta })\n : internetArchive\n ? await downloadInternetArchiveAudio({ ...input, ...internetArchive })\n : await downloadWebResource(input);\n const persisted = persistDownloadedResource(user, content);\n const { binaryContent: _binaryContent, ...publicContent } = content;\n return textToolResult(`Downloaded ${publicContent.title} as ${publicContent.fileName}.`, {\n ...publicContent,\n downloadId: persisted.id,\n });\n },\n );\n\n server.registerTool(\n 'prepare_dharma_share_text',\n {\n title: 'Prepare dharma sharing text',\n description:\n 'Prepare a text payload for the Fabushi app to confirm and add to global dharma sharing. This returns a clientAction; it does not start sending.',\n inputSchema: {\n title: z.string().min(1).max(120),\n text: z.string().min(1).max(20000),\n },\n outputSchema: {\n clientAction: z.object({\n type: z.literal('prepare_dharma_share_text'),\n title: z.string(),\n text: z.string(),\n }),\n },\n annotations: {\n readOnlyHint: false,\n destructiveHint: false,\n openWorldHint: false,\n },\n },\n async ({ title, text }) =>\n textToolResult(`Prepared \"${title}\" for user confirmation in the Fabushi app.`, {\n clientAction: {\n type: 'prepare_dharma_share_text',\n title: safeUserText(title).slice(0, 120),\n text: String(text || '').trim().slice(0, 20000),\n },\n }),\n );\n\n server.registerTool(\n 'prepare_practice_book_item',\n {\n title: 'Prepare practice book item',\n description:\n 'Prepare a scripture, chant, or practice item for the Fabushi app practice book. This returns a clientAction; it does not mutate the app by itself.',\n inputSchema: {\n title: z.string().min(1).max(120),\n sourceName: z.string().max(120).optional(),\n text: z.string().min(1).max(20000),\n },\n outputSchema: {\n clientAction: z.object({\n type: z.literal('prepare_practice_book_item'),\n title: z.string(),\n sourceName: z.string(),\n text: z.string(),\n }),\n },\n annotations: {\n readOnlyHint: false,\n destructiveHint: false,\n openWorldHint: false,\n },\n },\n async ({ title, sourceName, text }) =>\n textToolResult(`Prepared \"${title}\" for the Fabushi practice book confirmation flow.`, {\n clientAction: {\n type: 'prepare_practice_book_item',\n title: safeUserText(title).slice(0, 120),\n sourceName: safeUserText(sourceName || '大乘 AI').slice(0, 120),\n text: String(text || '').trim().slice(0, 20000),\n },\n }),\n );\n\n return server;\n}\n\nasync function handleMcpRequest(req, res) {\n const server = createLibreChatMcpServer();\n const transport = new StreamableHTTPServerTransport({\n sessionIdGenerator: undefined,\n enableJsonResponse: true,\n });\n\n res.on('close', () => {\n transport.close();\n server.close();\n });\n\n await server.connect(transport);\n await transport.handleRequest(req, res, req.body);\n}\n\napp.all('/mcp', async (req, res) => {\n try {\n await handleMcpRequest(req, res);\n } catch (error) {\n logger.error({ error: error.stack || String(error) }, 'MCP request failed');\n if (!res.headersSent) {\n res.status(500).json({\n error: 'MCP request failed',\n message: publicAiErrorMessage(error),\n });\n }\n }\n});\n\napp.post(\n '/api/resources/download',\n asyncHandler(async (req, res) => {\n const user = await resolveUser(req, req.body || {});\n const cbeta = parseCbetaResource(req.body || {});\n const internetArchive = parseInternetArchiveResource(req.body || {});\n const content = cbeta\n ? await downloadCbetaResource({ ...req.body, ...cbeta })\n : internetArchive\n ? await downloadInternetArchiveAudio({ ...req.body, ...internetArchive })\n : await downloadWebResource(req.body || {});\n const persisted = persistDownloadedResource(user, content);\n const { binaryContent: _binaryContent, ...publicContent } = content;\n jsonResponse(res, 200, {\n success: true,\n downloadId: persisted.id,\n ...publicContent,\n });\n }),\n);\n\napp.post(\n '/api/codex/resource-task',\n asyncHandler(async (req, res) => {\n if (process.env.ENABLE_CODEX_SDK !== 'true') {\n return jsonResponse(res, 403, {\n success: false,\n message: 'Codex SDK task runner is disabled. Set ENABLE_CODEX_SDK=true on the server to enable it.',\n });\n }\n const prompt = safeUserText(req.body?.prompt);\n if (!prompt) {\n return jsonResponse(res, 400, { success: false, message: 'prompt is required' });\n }\n const { Codex } = await import('@openai/codex-sdk');\n const codexRuntime = createCodexDeepSeekRuntime();\n const codex = new Codex(codexRuntime.enabled ? codexRuntime.options : {});\n const thread = codex.startThread(\n codexRuntime.enabled\n ? codexRuntime.threadOptions\n : { skipGitRepoCheck: true, networkAccessEnabled: true },\n );\n const result = await thread.run(prompt);\n jsonResponse(res, 200, {\n success: true,\n threadId: thread.id,\n provider: codexRuntime.provider,\n model: deepseekModel,\n result,\n });\n }),\n);\n\napp.use((error, _req, res, _next) => {\n const status = Number(error.statusCode || error.status || 500);\n const safeStatus = status >= 400 && status < 600 ? status : 500;\n logger.error({ error: error.stack || String(error) }, 'request failed');\n jsonResponse(res, safeStatus, {\n success: false,\n message: error.message || 'Internal Server Error',\n details: error.details,\n });\n});\n\nconst port = Number(process.env.PORT || 8788);\napp.listen(port, '0.0.0.0', () => {\n logger.info(\n {\n port,\n dbPath,\n dataDir,\n deepseekModel,\n cbetaApiRoot,\n },\n 'Dacheng AI backend started',\n );\n});\n"}, "34e876c79fc16ba8e0d0e3fb6922fc1f2e8702b2": {"t": 1785757974.2896178, "n": 143244, "h": "27f1e037697a6c3b", "c": "import crypto from 'node:crypto';\nimport fs from 'node:fs';\nimport path from 'node:path';\nimport process from 'node:process';\nimport { fileURLToPath } from 'node:url';\n\nimport { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';\nimport { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';\nimport Database from 'better-sqlite3';\nimport cors from 'cors';\nimport dotenv from 'dotenv';\nimport express from 'express';\nimport rateLimit from 'express-rate-limit';\nimport helmet from 'helmet';\nimport pino from 'pino';\nimport { z } from 'zod';\n\ndotenv.config();\n\nconst __dirname = path.dirname(fileURLToPath(import.meta.url));\nconst rootDir = path.resolve(__dirname, '..');\nconst dataDir = process.env.DATA_DIR || path.join(rootDir, 'data');\nconst resourcesDir = path.join(dataDir, 'resources');\nconst miniAppsDir = path.join(dataDir, 'miniapps');\nconst miniAppsStorePath = path.join(miniAppsDir, 'sandbox-miniapps.json');\nconst openClawRuntimeUpdatesDir =\n process.env.OPENCLAW_RUNTIME_UPDATES_DIR || path.join(dataDir, 'openclaw-runtime');\nconst openClawRuntimeEnableMacosUpdates =\n process.env.OPENCLAW_RUNTIME_ENABLE_MACOS_UPDATES === 'true';\nconst codexHomeDir = process.env.CODEX_HOME || path.join(dataDir, 'codex-home');\nconst codexTempDir = process.env.CODEX_TMPDIR || path.join(dataDir, 'codex-tmp');\nconst codexRuntimeDir = process.env.XDG_RUNTIME_DIR || path.join(dataDir, 'codex-runtime');\nconst dbPath = process.env.SQLITE_PATH || path.join(dataDir, 'dacheng-ai.sqlite');\n\nfs.mkdirSync(resourcesDir, { recursive: true });\nfs.mkdirSync(miniAppsDir, { recursive: true });\nfs.mkdirSync(openClawRuntimeUpdatesDir, { recursive: true });\nfs.mkdirSync(codexHomeDir, { recursive: true });\nfs.mkdirSync(codexTempDir, { recursive: true });\nfs.mkdirSync(codexRuntimeDir, { recursive: true });\n\nconst logger = pino({\n level: process.env.LOG_LEVEL || 'info',\n});\n\nconst app = express();\napp.disable('x-powered-by');\napp.set('trust proxy', Number(process.env.TRUST_PROXY_HOPS || 1));\napp.use(helmet());\nconst corsOrigin =\n !process.env.CORS_ORIGIN || process.env.CORS_ORIGIN.trim() === '*'\n ? true\n : process.env.CORS_ORIGIN.split(',').map((item) => item.trim()).filter(Boolean);\napp.use(cors({ origin: corsOrigin }));\napp.use(express.json({ limit: '1mb' }));\napp.use(\n rateLimit({\n windowMs: 60_000,\n limit: Number(process.env.RATE_LIMIT_PER_MINUTE || 90),\n standardHeaders: true,\n legacyHeaders: false,\n }),\n);\n\nconst db = new Database(dbPath);\ndb.pragma('journal_mode = WAL');\ndb.pragma('foreign_keys = ON');\n\ndb.exec(`\n CREATE TABLE IF NOT EXISTS conversations (\n id TEXT PRIMARY KEY,\n user_id TEXT NOT NULL,\n title TEXT NOT NULL,\n provider TEXT NOT NULL,\n model TEXT NOT NULL,\n created_at TEXT NOT NULL,\n updated_at TEXT NOT NULL\n );\n\n CREATE INDEX IF NOT EXISTS idx_conversations_user_updated\n ON conversations (user_id, updated_at DESC);\n\n CREATE TABLE IF NOT EXISTS messages (\n id TEXT PRIMARY KEY,\n conversation_id TEXT NOT NULL,\n role TEXT NOT NULL,\n content TEXT NOT NULL,\n prompt_tokens INTEGER NOT NULL DEFAULT 0,\n completion_tokens INTEGER NOT NULL DEFAULT 0,\n total_tokens INTEGER NOT NULL DEFAULT 0,\n created_at TEXT NOT NULL,\n FOREIGN KEY (conversation_id) REFERENCES conversations(id) ON DELETE CASCADE\n );\n\n CREATE INDEX IF NOT EXISTS idx_messages_conversation_created\n ON messages (conversation_id, created_at ASC);\n\n CREATE TABLE IF NOT EXISTS usage_months (\n user_id TEXT NOT NULL,\n month TEXT NOT NULL,\n total_tokens INTEGER NOT NULL DEFAULT 0,\n updated_at TEXT NOT NULL,\n PRIMARY KEY (user_id, month)\n );\n\n CREATE TABLE IF NOT EXISTS resource_downloads (\n id TEXT PRIMARY KEY,\n user_id TEXT NOT NULL,\n title TEXT NOT NULL,\n source_name TEXT NOT NULL,\n source_url TEXT NOT NULL,\n file_name TEXT NOT NULL,\n file_path TEXT NOT NULL,\n created_at TEXT NOT NULL\n );\n\n CREATE TABLE IF NOT EXISTS agent_runs (\n id TEXT PRIMARY KEY,\n user_id TEXT NOT NULL,\n conversation_id TEXT NOT NULL,\n message_id TEXT NOT NULL,\n openclaw_run_id TEXT,\n openclaw_session_key TEXT NOT NULL,\n status TEXT NOT NULL,\n mode TEXT NOT NULL,\n provider TEXT NOT NULL,\n model TEXT,\n input_tokens INTEGER NOT NULL DEFAULT 0,\n output_tokens INTEGER NOT NULL DEFAULT 0,\n tool_call_count INTEGER NOT NULL DEFAULT 0,\n cost_microusd INTEGER NOT NULL DEFAULT 0,\n started_at TEXT NOT NULL,\n completed_at TEXT,\n failed_at TEXT,\n error_code TEXT,\n error_message TEXT\n );\n\n CREATE INDEX IF NOT EXISTS idx_agent_runs_user_started\n ON agent_runs (user_id, started_at DESC);\n\n CREATE INDEX IF NOT EXISTS idx_agent_runs_status_started\n ON agent_runs (status, started_at DESC);\n\n CREATE TABLE IF NOT EXISTS agent_message_feedback (\n id TEXT PRIMARY KEY,\n message_id TEXT NOT NULL,\n user_id TEXT NOT NULL,\n rating TEXT NOT NULL,\n reason TEXT,\n comment TEXT,\n created_at TEXT NOT NULL\n );\n`);\n\nconst statements = {\n insertConversation: db.prepare(`\n INSERT INTO conversations (id, user_id, title, provider, model, created_at, updated_at)\n VALUES (@id, @userId, @title, @provider, @model, @createdAt, @updatedAt)\n `),\n updateConversation: db.prepare(`\n UPDATE conversations\n SET title = @title, updated_at = @updatedAt\n WHERE id = @id AND user_id = @userId\n `),\n getConversation: db.prepare(`\n SELECT * FROM conversations WHERE id = ? AND user_id = ? LIMIT 1\n `),\n listConversations: db.prepare(`\n SELECT id, title, updated_at AS updatedAt\n FROM conversations\n WHERE user_id = ?\n ORDER BY updated_at DESC\n LIMIT 30\n `),\n insertMessage: db.prepare(`\n INSERT INTO messages (\n id, conversation_id, role, content,\n prompt_tokens, completion_tokens, total_tokens, created_at\n )\n VALUES (\n @id, @conversationId, @role, @content,\n @promptTokens, @completionTokens, @totalTokens, @createdAt\n )\n `),\n listMessages: db.prepare(`\n SELECT role, content, prompt_tokens AS promptTokens,\n completion_tokens AS completionTokens, total_tokens AS totalTokens,\n created_at AS createdAt\n FROM messages\n WHERE conversation_id = ?\n ORDER BY created_at ASC\n `),\n getUsage: db.prepare(`\n SELECT total_tokens AS totalTokens FROM usage_months WHERE user_id = ? AND month = ?\n `),\n upsertUsage: db.prepare(`\n INSERT INTO usage_months (user_id, month, total_tokens, updated_at)\n VALUES (@userId, @month, @totalTokens, @updatedAt)\n ON CONFLICT(user_id, month)\n DO UPDATE SET total_tokens = usage_months.total_tokens + excluded.total_tokens,\n updated_at = excluded.updated_at\n `),\n insertResourceDownload: db.prepare(`\n INSERT INTO resource_downloads (\n id, user_id, title, source_name, source_url, file_name, file_path, created_at\n )\n VALUES (\n @id, @userId, @title, @sourceName, @sourceUrl, @fileName, @filePath, @createdAt\n )\n `),\n insertAgentRun: db.prepare(`\n INSERT INTO agent_runs (\n id, user_id, conversation_id, message_id, openclaw_run_id, openclaw_session_key,\n status, mode, provider, model, started_at\n )\n VALUES (\n @id, @userId, @conversationId, @messageId, @openClawRunId, @openClawSessionKey,\n @status, @mode, @provider, @model, @startedAt\n )\n `),\n getAgentRun: db.prepare(`\n SELECT * FROM agent_runs WHERE id = ? AND user_id = ? LIMIT 1\n `),\n updateAgentRunStatus: db.prepare(`\n UPDATE agent_runs\n SET status = @status,\n openclaw_run_id = COALESCE(@openClawRunId, openclaw_run_id),\n input_tokens = COALESCE(@inputTokens, input_tokens),\n output_tokens = COALESCE(@outputTokens, output_tokens),\n tool_call_count = COALESCE(@toolCallCount, tool_call_count),\n completed_at = @completedAt,\n failed_at = @failedAt,\n error_code = @errorCode,\n error_message = @errorMessage\n WHERE id = @id AND user_id = @userId\n `),\n insertAgentFeedback: db.prepare(`\n INSERT INTO agent_message_feedback (id, message_id, user_id, rating, reason, comment, created_at)\n VALUES (@id, @messageId, @userId, @rating, @reason, @comment, @createdAt)\n `),\n};\n\nconst deepseekApiKey = process.env.DEEPSEEK_API_KEY || '';\nconst deepseekBaseUrl = (process.env.DEEPSEEK_BASE_URL || 'https://api.deepseek.com').replace(/\\/+$/, '');\nconst deepseekModel = process.env.DEEPSEEK_MODEL || 'deepseek-chat';\nconst deepseekAvailableModels = Array.from(\n new Set(\n (process.env.DEEPSEEK_MODELS || `${deepseekModel},deepseek-chat,deepseek-reasoner`)\n .split(',')\n .map((item) => normalizeDeepSeekModelName(item))\n .filter(Boolean),\n ),\n);\nconst cbetaApiRoot = (process.env.CBETA_API_ROOT || 'https://api.ombhrum.com/api/cbeta').replace(/\\/+$/, '');\nconst fabushiApiBaseUrl = (process.env.FABUSHI_API_BASE_URL || 'https://api.ombhrum.com').replace(/\\/+$/, '');\nconst memberMonthlyLimit = Number(process.env.MEMBER_MONTHLY_TOKEN_LIMIT || 1_000_000);\nconst freeMonthlyLimit = Number(process.env.FREE_MONTHLY_TOKEN_LIMIT || 1_000);\nconst requireMemberForAi = process.env.REQUIRE_MEMBER_FOR_AI === 'true';\nconst deepseekMaxCompletionTokens = Math.max(1, Number(process.env.DEEPSEEK_MAX_COMPLETION_TOKENS || 1400));\nconst maxResourceTextChars = Number(process.env.MAX_RESOURCE_TEXT_CHARS || 80_000);\nconst openClawRuntimeManifestPath =\n process.env.OPENCLAW_RUNTIME_MANIFEST_PATH || path.join(openClawRuntimeUpdatesDir, 'manifest.json');\nconst openClawRuntimePublicBaseUrl = (process.env.OPENCLAW_RUNTIME_PUBLIC_BASE_URL || '').replace(/\\/+$/, '');\nconst enableLibreChatAgentChat = process.env.ENABLE_LIBRECHAT_AGENT_CHAT === 'true';\nconst libreChatAgentsBaseUrl = (\n process.env.LIBRECHAT_AGENTS_BASE_URL || 'http://127.0.0.1:3080/api/agents/v1'\n).replace(/\\/+$/, '');\nconst libreChatAgentApiKey = process.env.LIBRECHAT_AGENT_API_KEY || '';\nconst libreChatAgentId = process.env.LIBRECHAT_AGENT_ID || '';\nconst enableCodexSdkChat = process.env.ENABLE_CODEX_SDK_CHAT === 'true';\nconst enableOpenClawAgentChat = process.env.ENABLE_OPENCLAW_AGENT_CHAT === 'true';\nconst openClawGatewayUrl = (process.env.OPENCLAW_GATEWAY_URL || 'http://127.0.0.1:18789').replace(/\\/+$/, '');\nconst openClawGatewayToken = process.env.OPENCLAW_GATEWAY_TOKEN || '';\nconst openClawAgentId = process.env.OPENCLAW_AGENT_ID || 'fabushi-public-agent';\nconst openClawRunsEndpoint = process.env.OPENCLAW_RUNS_ENDPOINT || '';\nconst codexDeepSeekProviderId = 'deepseek-chat-completions';\nconst codexResponsesBaseUrl = (\n process.env.CODEX_DEEPSEEK_RESPONSES_BASE_URL ||\n `http://127.0.0.1:${process.env.PORT || 8788}/codex-deepseek/v1`\n).replace(/\\/+$/, '');\n\nconst systemPrompt = [\n '你是“大乘”App 的 AI 助手。',\n '你的核心任务是帮助用户查找、整理、理解并全球法布施合法可分享的佛法资源。',\n '回答要庄重、简洁、可执行;涉及经典、仪轨或资源时,提醒用户尊重版权、来源和当地法规。',\n '如果用户想找资源、下载经文、寻找音频或准备可分享资料,你必须先在后端自动执行搜索、验证和下载步骤;不要要求用户去前端 + 菜单手动查找。',\n '你可以把执行进度总结给用户,但不要输出隐藏推理原文。',\n].join('\\n');\n\nfunction nowIso() {\n return new Date().toISOString();\n}\n\nfunction monthKey(date = new Date()) {\n return `${date.getUTCFullYear()}-${String(date.getUTCMonth() + 1).padStart(2, '0')}`;\n}\n\nfunction jsonResponse(res, status, payload) {\n res.status(status).json(payload);\n}\n\nasync function readJsonFile(filePath) {\n const raw = await fs.promises.readFile(filePath, 'utf8');\n return JSON.parse(raw);\n}\n\nfunction requestOrigin(req) {\n if (openClawRuntimePublicBaseUrl) return openClawRuntimePublicBaseUrl;\n const proto = req.get('X-Forwarded-Proto') || req.protocol || 'https';\n const host = req.get('X-Forwarded-Host') || req.get('Host');\n return host ? `${proto}://${host}` : '';\n}\n\nfunction absolutizeOpenClawRuntimeUrl(req, value) {\n const text = readText(value);\n if (!text) return '';\n if (/^https?:\\/\\//i.test(text)) return text;\n const normalized = text.startsWith('/')\n ? text\n : `/api/openclaw/runtime/files/${text.replace(/^\\/+/, '')}`;\n const origin = requestOrigin(req);\n return origin ? new URL(normalized, `${origin}/`).toString() : normalized;\n}\n\nfunction textToolResult(text, structuredContent = {}) {\n return {\n structuredContent,\n content: [{ type: 'text', text }],\n };\n}\n\nfunction sseHeaders(res) {\n res.status(200);\n res.setHeader('Content-Type', 'text/event-stream; charset=utf-8');\n res.setHeader('Cache-Control', 'no-cache, no-transform');\n res.setHeader('Connection', 'keep-alive');\n res.setHeader('X-Accel-Buffering', 'no');\n if (typeof res.flushHeaders === 'function') res.flushHeaders();\n}\n\nfunction writeSse(res, event, payload) {\n res.write(`event: ${event}\\n`);\n res.write(`data: ${JSON.stringify(payload)}\\n\\n`);\n}\n\nfunction asyncHandler(fn) {\n return (req, res, next) => {\n Promise.resolve(fn(req, res, next)).catch(next);\n };\n}\n\nfunction bearerToken(req) {\n const delegated = readText(req.get('x-dacheng-auth-token'));\n if (\n delegated &&\n delegated !== 'DACHENG_AUTH_TOKEN' &&\n !delegated.startsWith('secretref-env:') &&\n delegated !== 'dacheng-openclaw-proxy'\n ) {\n return delegated;\n }\n const header = req.get('Authorization') || '';\n if (!header.startsWith('Bearer ')) return '';\n const token = header.slice('Bearer '.length).trim();\n return token === 'dacheng-openclaw-proxy' ? '' : token;\n}\n\nfunction sha256(value) {\n return crypto.createHash('sha256').update(value).digest('hex');\n}\n\nfunction safeUserText(value, fallback = '') {\n if (typeof value !== 'string') return fallback;\n return value.replace(/\\u0000/g, '').trim();\n}\n\nfunction estimateTokens(text) {\n const normalized = String(text || '').trim();\n if (!normalized) return 0;\n return Math.max(1, Math.ceil(normalized.length / 3));\n}\n\nfunction readText(value) {\n if (value === undefined || value === null) return '';\n return String(value).replace(/\\u0000/g, '').trim();\n}\n\nconst miniAppHostApiVersion = '1.2';\nconst miniAppHighRiskPermissions = new Set([\n 'auth.token',\n 'payments.alipay',\n 'desktop.control',\n 'wifi.hotspot',\n 'local.loopback',\n 'fs.readWrite',\n 'shell.execute',\n 'browser.external',\n 'files.write',\n 'projects.write',\n 'openclaw.restart',\n]);\n\nfunction officialMiniAppRegistry(req) {\n const webBase = process.env.MINIAPP_WEB_BASE_URL || 'https://fabushi.ombhrum.com';\n const official = [\n {\n botId: 'bot.global-dharma',\n miniAppId: 'official.global-dharma',\n title: '全球法布施',\n subtitle: '地区、循环、场能都在对话框上方设置',\n initials: '法',\n icon: 'public',\n avatarColor: '#4CAF7A',\n kind: 'global_dharma',\n entryUrl: process.env.GLOBAL_DHARMA_MINIAPP_URL,\n greeting: '把要分享的文字、链接或素材发给我,我会按上方选择的地区与模式启动全球法布施。',\n inputHint: '输入要法布施的文字/链接,或点 + 添加素材',\n permissions: [\n 'app.context',\n 'bot.chat',\n 'auth.session',\n 'payments.alipay',\n 'dharma.share',\n 'files.pick',\n 'wifi.hotspot',\n 'openclaw.status',\n 'openclaw.chat',\n 'local.loopback',\n 'desktop.control',\n ],\n },\n {\n botId: 'bot.flashcards',\n miniAppId: 'official.flashcards',\n title: '背诵闪卡制作',\n subtitle: '随机挖空 / AI 制卡从顶部模式按钮选择',\n initials: '卡',\n icon: 'flashcards',\n avatarColor: '#7E57C2',\n kind: 'flashcards',\n entryUrl: process.env.FLASHCARDS_MINIAPP_URL,\n greeting: '粘贴经文、文章正文或链接即可制作背诵闪卡。制卡模式请在上方按钮切换。',\n inputHint: '粘贴链接或正文,发送后制作闪卡',\n permissions: ['app.context', 'bot.chat', 'flashcards.create', 'openclaw.status', 'openclaw.chat'],\n },\n {\n botId: 'bot.platform-publish',\n miniAppId: 'official.platform-publish',\n title: '法布施到平台',\n subtitle: '选择平台后生成发布草稿并打开入口',\n initials: '发',\n icon: 'publish',\n avatarColor: '#FF9F43',\n kind: 'platform_publish',\n entryUrl: process.env.PLATFORM_PUBLISH_MINIAPP_URL,\n greeting: '把要发布的正文或链接发给我,上方选择平台后,我会生成发布草稿并打开对应平台入口。',\n inputHint: '输入要发布到平台的正文/链接',\n permissions: [\n 'app.context',\n 'bot.chat',\n 'platform.publish',\n 'files.pick',\n 'fs.readWrite',\n 'shell.execute',\n 'browser.external',\n ],\n },\n {\n botId: 'bot.father',\n miniAppId: 'official.bot-father',\n title: '机器人之父',\n subtitle: '用对话生成个人沙箱小程序',\n initials: '父',\n icon: 'bot_father',\n avatarColor: '#3D8BFF',\n kind: 'bot_father',\n entryUrl: process.env.BOT_FATHER_MINIAPP_URL,\n greeting: '告诉我你想要的小程序,我会生成 manifest、界面和权限声明,并放进你的个人沙箱。',\n inputHint: '描述你想创建的小程序',\n permissions: ['app.context', 'bot.chat', 'miniapps.dev'],\n },\n ];\n return {\n bots: official.map((bot, index) => ({\n ...bot,\n destinationIndex: index,\n source: 'official',\n })),\n miniApps: official.map((bot) => ({\n miniAppId: bot.miniAppId,\n botId: bot.botId,\n title: bot.title,\n subtitle: bot.subtitle,\n entryUrl:\n bot.entryUrl ||\n `${webBase.replace(/\\/+$/, '')}/miniapps/${encodeURIComponent(bot.miniAppId)}`,\n version: '1.2.0',\n permissions: bot.permissions,\n surfaces: ['homePinned', 'chatPanel'],\n theme: 'telegramDark',\n signature: 'official',\n reviewStatus: 'trusted',\n source: 'official',\n })),\n };\n}\n\nasync function readMiniAppStore() {\n try {\n const raw = await fs.promises.readFile(miniAppsStorePath, 'utf8');\n const decoded = JSON.parse(raw);\n return {\n apps: Array.isArray(decoded.apps) ? decoded.apps : [],\n };\n } catch (error) {\n if (error?.code === 'ENOENT') return { apps: [] };\n throw error;\n }\n}\n\nasync function writeMiniAppStore(store) {\n await fs.promises.mkdir(miniAppsDir, { recursive: true });\n await fs.promises.writeFile(\n miniAppsStorePath,\n JSON.stringify({ apps: store.apps || [] }, null, 2),\n );\n}\n\nfunction miniAppEntryUrl(req, miniAppId) {\n const origin = requestOrigin(req) || '';\n const pathName = `/api/miniapps/dev/${encodeURIComponent(miniAppId)}/index.html`;\n return origin ? new URL(pathName, `${origin}/`).toString() : pathName;\n}\n\nfunction sanitizeMiniAppPermissions(value, { allowHighRisk = false } = {}) {\n const raw = Array.isArray(value) ? value : [];\n const normalized = raw\n .map((item) => readText(item).toLowerCase())\n .filter((item) => /^[a-z][a-z0-9_.:-]{0,64}$/.test(item));\n const deduped = [...new Set(['app.context', 'bot.chat', ...normalized])];\n return deduped.filter((permission) => allowHighRisk || !miniAppHighRiskPermissions.has(permission));\n}\n\nfunction miniAppScan(permissions, sourceHtml = '') {\n const highRisk = permissions.filter((permission) => miniAppHighRiskPermissions.has(permission));\n const blockedPatterns = [\n /authorization\\s*:\\s*['\"`]bearer/i,\n /desktop_control_bridge_token/i,\n /openclaw_gateway_token/i,\n /eval\\s*\\(/i,\n ];\n const blocked = blockedPatterns\n .filter((pattern) => pattern.test(sourceHtml))\n .map((pattern) => pattern.toString());\n return {\n passed: blocked.length === 0,\n highRiskPermissions: highRisk,\n blockedPatterns: blocked,\n };\n}\n\nfunction inferMiniAppPermissionsFromText(...values) {\n const text = values.map((value) => readText(value)).join('\\n').toLowerCase();\n const rules = [\n ['auth.session', [/auth\\.(getsession|requirelogin)/, /登录|用户信息|宿主账号/]],\n ['auth.token', [/auth\\.getaccesstoken/, /access token|访问 token/]],\n ['payments.alipay', [/payments\\.alipay/, /支付宝|支付|购买|订单/]],\n ['dharma.share', [/dharma\\./, /全球法布施|法布施发送/]],\n ['wifi.hotspot', [/wifihotspot\\./, /wifi|wi-fi|热点|本地场能/]],\n ['local.loopback', [/localloopback\\./, /localhost|127\\.0\\.0\\.1|本地回环|本地转经轮/]],\n ['flashcards.create', [/flashcards\\./, /闪卡|背诵卡|挖空/]],\n ['platform.publish', [/platformpublish\\./, /发布草稿|发布到平台|公众号|小红书|知乎/]],\n ['files.pick', [/files\\.pick/, /选择文件|上传文件|本地素材/]],\n ['fs.readWrite', [/fs\\.(writefile|readfile)/, /本地文件|写文件|读文件|文件处理/]],\n ['shell.execute', [/shell\\.execute/, /终端|命令行|shell|cli|playwright/]],\n ['browser.external', [/browser\\.open/, /浏览器|打开网页|打开平台/]],\n ['desktop.control', [/desktopcontrol\\./, /桌面控制|点击|键盘|鼠标/]],\n ['openclaw.chat', [/openclaw\\.chat/, /openclaw/]],\n ];\n return rules\n .filter(([, patterns]) => patterns.some((pattern) => pattern.test(text)))\n .map(([permission]) => permission);\n}\n\nfunction miniAppRecordToBot(record) {\n return {\n botId: record.botId,\n miniAppId: record.miniAppId,\n title: record.title,\n subtitle: record.subtitle,\n initials: record.initials || '小',\n icon: record.icon || 'code',\n avatarColor: record.avatarColor || '#3D8BFF',\n kind: 'third_party',\n greeting: record.greeting || '你好,我是这个小程序的机器人。',\n inputHint: record.inputHint || '输入消息,或打开小程序面板',\n permissions: record.permissions || ['app.context', 'bot.chat'],\n source: record.source || 'sandbox',\n };\n}\n\nfunction miniAppRecordToManifest(req, record) {\n return {\n miniAppId: record.miniAppId,\n botId: record.botId,\n title: record.title,\n subtitle: record.subtitle,\n entryUrl: miniAppEntryUrl(req, record.miniAppId),\n version: record.version || '0.0.1',\n permissions: record.permissions || ['app.context', 'bot.chat'],\n surfaces: record.surfaces || ['chatPanel'],\n theme: record.theme || 'telegramDark',\n signature: record.signature || `sandbox:${record.ownerId}`,\n reviewStatus: record.reviewStatus || 'sandbox',\n source: record.source || 'sandbox',\n };\n}\n\nfunction safeMiniAppId(value) {\n return readText(value).replace(/[^a-zA-Z0-9_.-]/g, '-').slice(0, 80);\n}\n\nfunction titleFromPromptForMiniApp(prompt) {\n const text = readText(prompt).replace(/\\s+/g, ' ');\n if (!text) return '个人小程序';\n return text.length <= 18 ? text : text.slice(0, 18);\n}\n\nfunction escapeHtml(value) {\n return String(value || '')\n .replace(/&/g, '&')\n .replace(/</g, '<')\n .replace(/>/g, '>')\n .replace(/\"/g, '"')\n .replace(/'/g, ''');\n}\n\nfunction generatedMiniAppHtml({ title, prompt }) {\n const safeTitle = escapeHtml(title);\n const safePrompt = escapeHtml(prompt);\n const promptJson = JSON.stringify(readText(prompt));\n return `<!doctype html>\n<html lang=\"zh-Hans\">\n<head>\n <meta charset=\"utf-8\" />\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n <title>${safeTitle}</title>\n <style>\n body { margin:0; font-family:-apple-system,BlinkMacSystemFont,\"Segoe UI\",sans-serif; background:#0f1722; color:#fff; }\n main { padding:20px; display:grid; gap:14px; }\n .card { border:1px solid #263445; background:#17212b; border-radius:16px; padding:16px; }\n button { border:0; border-radius:999px; padding:10px 14px; background:#3390ec; color:white; font-weight:800; }\n pre { white-space:pre-wrap; color:#9ec7ff; }\n </style>\n</head>\n<body>\n <main>\n <section class=\"card\">\n <h1>${safeTitle}</h1>\n <p>这是机器人之父根据你的描述生成的个人沙箱小程序。</p>\n <pre>${safePrompt}</pre>\n </section>\n <section class=\"card\">\n <button id=\"context\">读取宿主上下文</button>\n <button id=\"spec\">读取宿主能力</button>\n <button id=\"chat\">让机器人解释这个小程序</button>\n <pre id=\"output\">等待操作...</pre>\n </section>\n </main>\n <script>\n const out = document.getElementById('output');\n async function invoke(method, params) {\n if (!window.FabushiMiniApp) throw new Error('FabushiMiniApp host SDK not ready');\n const result = await window.FabushiMiniApp.invoke(method, params || {});\n out.textContent = JSON.stringify(result, null, 2);\n }\n document.getElementById('context').onclick = () => invoke('app.getContext');\n document.getElementById('spec').onclick = () => invoke('app.getHostApiSpec');\n const prompt = ${promptJson};\n document.getElementById('chat').onclick = () => invoke('bot.sendMessage', {\n message: '请简要说明这个小程序能做什么:' + prompt\n });\n </script>\n</body>\n</html>`;\n}\n\nfunction extractMiniAppHtml(value) {\n const text = readText(value);\n if (!text) return '';\n const fenced = text.match(/```(?:html)?\\s*([\\s\\S]*?)```/i);\n const candidate = (fenced ? fenced[1] : text).trim();\n if (!/<html[\\s>]/i.test(candidate) || !/<script[\\s>]/i.test(candidate)) return '';\n return candidate.startsWith('<!doctype') || candidate.startsWith('<html')\n ? candidate\n : `<!doctype html>\\n${candidate}`;\n}\n\nasync function generateBotFatherMiniAppHtml({ title, prompt, user }) {\n const fallback = generatedMiniAppHtml({ title, prompt });\n try {\n const result = await runAgentModel({\n user,\n mode: 'miniapp_generation',\n maxCompletionTokens: deepseekMaxCompletionTokens,\n messages: [\n {\n role: 'system',\n content: [\n '你是 Fabushi App 的小程序生成器。',\n '只输出一个完整静态 HTML 文件,不要 Markdown,不要解释。',\n '小程序只能通过 window.FabushiMiniApp.invoke(method, params) 调用宿主能力。',\n '先调用 app.getHostApiSpec 或 app.getCapabilities 判断宿主版本和已授权权限,再显示对应 UI。',\n '标准能力:auth.getSession/auth.requireLogin 复用宿主登录;payments.alipay.pay 拉起支付宝并把支付结果传回小程序;payments.alipay.createOrder 仅作为宿主官方订单服务的可选能力;小程序商品、购买记录、权益状态由小程序自己保存;dharma.prepareContent/dharma.startGlobalSend 接入全球法布施;wifiHotspot.enable 接入本地场能热点;flashcards.createDeck 接入背诵闪卡;platformPublish.createDraft 接入发布草稿;fs.writeFile/fs.readFile、shell.execute、browser.open 用于本地文件处理、终端和浏览器。',\n '高危权限包括 auth.token、payments.alipay、wifi.hotspot、local.loopback、fs.readWrite、shell.execute、browser.external、desktop.control;只有用户需求明确需要时才使用。',\n '禁止外链脚本、eval、Authorization token、桌面桥 token、OpenClaw token。',\n '不要调用 auth.getAccessToken,除非用户明确要求对接需要 token 的受信服务。',\n '优先实现一个可点击、可立即使用的小面板,包含 app.getContext 或 bot.sendMessage 示例。',\n ].join('\\n'),\n },\n {\n role: 'user',\n content: [\n `小程序标题:${title}`,\n `用户需求:${prompt}`,\n '请生成 HTML/CSS/JS 单文件。',\n ].join('\\n'),\n },\n ],\n });\n const sourceHtml = extractMiniAppHtml(result.message);\n if (!sourceHtml) {\n return {\n sourceHtml: fallback,\n generation: { provider: 'template', fallbackReason: 'ai_output_not_html' },\n };\n }\n return {\n sourceHtml,\n generation: {\n provider: result.provider || 'ai',\n model: result.model || '',\n },\n };\n } catch (error) {\n return {\n sourceHtml: fallback,\n generation: {\n provider: 'template',\n fallbackReason: readText(error?.message || error),\n },\n };\n }\n}\n\nfunction clientContextMessage(client) {\n if (!client || typeof client !== 'object') return '';\n const selectedModel = readText(client.selectedModel || client.model);\n const project = client.project && typeof client.project === 'object' ? client.project : {};\n const projectName = readText(project.name);\n const projectPath = readText(project.path);\n const lines = ['桌面端上下文:'];\n if (selectedModel) lines.push(`用户选择的 DeepSeek 模型:${normalizeDeepSeekModelName(selectedModel)}`);\n if (projectPath) {\n lines.push(`当前项目名称:${projectName || projectPath}`);\n lines.push(`当前项目路径:${projectPath}`);\n lines.push('如果本轮任务涉及读取、修改、运行、分析或整理项目,请优先围绕当前项目路径工作;不要擅自操作其他目录。');\n }\n return lines.length > 1 ? lines.join('\\n') : '';\n}\nfunction unwrapPayload(payload) {\n if (!payload || typeof payload !== 'object') return {};\n return payload.data && typeof payload.data === 'object' ? payload.data : payload;\n}\n\nfunction readFirstText(values) {\n for (const value of values) {\n const text = readText(value);\n if (text) return text;\n }\n return '';\n}\n\nfunction activeFlag(value) {\n if (value === true) return true;\n if (value === 1) return true;\n if (typeof value !== 'string') return false;\n return ['true', '1', 'yes', 'active'].includes(value.trim().toLowerCase());\n}\n\nfunction futureDate(value) {\n const text = readText(value);\n if (!text) return false;\n const timestamp = Date.parse(text);\n return Number.isFinite(timestamp) && timestamp > Date.now();\n}\n\nfunction normalizeMembershipPayload(payload) {\n if (!payload || typeof payload !== 'object') return false;\n const data = unwrapPayload(payload);\n const membership = data.membership && typeof data.membership === 'object' ? data.membership : data;\n const type = readText(membership.type || membership.membershipType || data.membershipType).toLowerCase();\n const status = readText(membership.status || data.status).toLowerCase();\n const expiry = membership.expiresAt ||\n membership.expires_at ||\n membership.membershipExpiry ||\n membership.membership_expires_at ||\n data.membershipExpiry ||\n data.membership_expires_at;\n const hasExplicitActiveFlag =\n membership.isActive !== undefined ||\n membership.active !== undefined ||\n membership.hasPremiumAccess !== undefined ||\n membership.hasPremiumMembership !== undefined ||\n membership.isPremium !== undefined;\n\n if (hasExplicitActiveFlag) {\n return Boolean(\n activeFlag(membership.isActive) ||\n activeFlag(membership.active) ||\n activeFlag(membership.hasPremiumAccess) ||\n activeFlag(membership.hasPremiumMembership) ||\n activeFlag(membership.isPremium),\n );\n }\n\n return Boolean(\n status === 'active' ||\n type === 'paid' ||\n type === 'premium' ||\n type === 'trial' ||\n futureDate(expiry),\n );\n}\n\nfunction extractRemoteAccount(payload) {\n const data = unwrapPayload(payload);\n const nestedUser = data.user && typeof data.user === 'object' ? data.user : {};\n const id = readFirstText([\n data.userId,\n data.user_id,\n data.userNo,\n data.user_no,\n data.id,\n nestedUser.userId,\n nestedUser.user_id,\n nestedUser.userNo,\n nestedUser.user_no,\n nestedUser.id,\n ]);\n const username = readFirstText([data.username, data.name, nestedUser.username, nestedUser.name]);\n const email = readFirstText([data.email, nestedUser.email]);\n const membership = data.membership && typeof data.membership === 'object' ? data.membership : null;\n return {\n authenticated: Boolean(id || username || email || membership || data.success === true),\n id,\n username,\n email,\n isMember: normalizeMembershipPayload(payload),\n membership,\n };\n}\n\nasync function resolveRemoteAccount(token) {\n const fallback = {\n authenticated: false,\n id: '',\n username: '',\n email: '',\n isMember: false,\n membership: null,\n };\n if (!token || !fabushiApiBaseUrl) return fallback;\n\n const endpoints = [\n '/api/stripe/membership-status',\n '/api/alipay/check-membership',\n '/api/auth/user-info',\n ];\n\n for (const endpoint of endpoints) {\n try {\n const response = await fetch(`${fabushiApiBaseUrl}${endpoint}`, {\n headers: {\n Accept: 'application/json',\n Authorization: `Bearer ${token}`,\n },\n signal: AbortSignal.timeout(8_000),\n });\n if (response.status === 401 || response.status === 403) return fallback;\n if (!response.ok) continue;\n const payload = await response.json();\n const account = extractRemoteAccount(payload);\n if (account.authenticated || account.isMember) {\n return {\n ...fallback,\n ...account,\n authenticated: true,\n };\n }\n } catch (error) {\n logger.debug({ endpoint, error: String(error) }, 'membership lookup skipped');\n }\n }\n\n return fallback;\n}\n\nasync function resolveUser(req, body = {}) {\n const token = bearerToken(req);\n const usernameHint = safeUserText(body.username || req.query.username);\n\n const testToken = process.env.TEST_ACCOUNT_TOKEN || 'dacheng-test-token-bypass-limits';\n if (token && token === testToken) {\n return {\n userId: 'user:test_account',\n username: 'TestAccount',\n tokenHash: sha256(token).slice(0, 24),\n isAuthenticated: true,\n isMember: true,\n isTestAccount: true,\n membership: { type: 'lifetime', active: true },\n };\n }\n\n const tokenHash = token ? sha256(token).slice(0, 24) : '';\n const anonymousHash = sha256(`${req.ip}|${req.get('User-Agent') || ''}`).slice(0, 24);\n const remoteAccount = await resolveRemoteAccount(token);\n const remoteIdentity = remoteAccount.id ||\n remoteAccount.username ||\n remoteAccount.email ||\n (remoteAccount.authenticated ? tokenHash : '');\n const userId = remoteAccount.authenticated\n ? `user:${remoteIdentity}`\n : tokenHash\n ? `token:${tokenHash}`\n : `anon:${anonymousHash}`;\n return {\n userId,\n username: remoteAccount.username || usernameHint,\n tokenHash,\n isAuthenticated: remoteAccount.authenticated,\n isMember: remoteAccount.isMember,\n membership: remoteAccount.membership,\n };\n}\n\nfunction usageFor(userId) {\n const month = monthKey();\n const row = statements.getUsage.get(userId, month);\n return {\n month,\n used: Number(row?.totalTokens || 0),\n };\n}\n\nfunction enforceTokenBudget(user, estimatedTokensForRequest) {\n if (user && user.isTestAccount) {\n return { limit: 999999999, usage: { month: monthKey(), used: 0 } };\n }\n if (requireMemberForAi && !user.isMember) {\n const error = new Error('大乘 AI 需要开通会员后使用');\n error.statusCode = 403;\n error.code = 'AI_MEMBERSHIP_REQUIRED';\n error.details = {\n requireMemberForAi,\n monthlyLimit: memberMonthlyLimit,\n remainingTokens: 0,\n };\n throw error;\n }\n\n const limit = user.isMember ? memberMonthlyLimit : freeMonthlyLimit;\n const usage = usageFor(user.userId);\n if (usage.used + estimatedTokensForRequest > limit) {\n const error = new Error('本月 AI token 额度已不足');\n error.statusCode = 429;\n error.code = 'USER_TOKEN_QUOTA_EXCEEDED';\n error.details = {\n monthlyLimit: limit,\n usedTokens: usage.used,\n remainingTokens: Math.max(0, limit - usage.used),\n };\n throw error;\n }\n return { limit, usage };\n}\n\nfunction completionBudgetFor(budget, estimatedPromptTokens) {\n const remainingAfterPrompt = budget.limit - budget.usage.used - estimatedPromptTokens;\n return Math.max(1, Math.min(deepseekMaxCompletionTokens, Math.floor(remainingAfterPrompt)));\n}\n\nfunction recordUsage(userId, totalTokens) {\n if (!totalTokens || totalTokens < 1) return;\n statements.upsertUsage.run({\n userId,\n month: monthKey(),\n totalTokens,\n updatedAt: nowIso(),\n });\n}\n\nfunction titleFromMessage(message) {\n const compact = message.replace(/\\s+/g, ' ').trim();\n if (!compact) return '新对话';\n return compact.length > 22 ? `${compact.slice(0, 22)}...` : compact;\n}\n\nfunction normalizeMessages(rows) {\n return rows.map((row) => ({\n role: row.role,\n content: row.content,\n }));\n}\n\nfunction normalizeMaxCompletionTokens(value) {\n const numeric = Number(value || deepseekMaxCompletionTokens);\n if (!Number.isFinite(numeric) || numeric < 1) return deepseekMaxCompletionTokens;\n return Math.max(1, Math.min(deepseekMaxCompletionTokens, Math.floor(numeric)));\n}\n\nfunction normalizeDeepSeekModelName(value) {\n const raw = readText(value || deepseekModel);\n if (!raw) return deepseekModel;\n const modelId = raw.includes('/') ? raw.split('/').pop() : raw;\n return modelId || deepseekModel;\n}\n\nasync function callDeepSeek(messages, options = {}) {\n if (!deepseekApiKey) {\n const error = new Error('DeepSeek API key is not configured');\n error.statusCode = 503;\n throw error;\n }\n\n const model = normalizeDeepSeekModelName(options.model);\n const response = await fetch(`${deepseekBaseUrl}/chat/completions`, {\n method: 'POST',\n headers: {\n Accept: 'application/json',\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${deepseekApiKey}`,\n },\n body: JSON.stringify({\n model,\n messages,\n temperature: 0.4,\n max_tokens: normalizeMaxCompletionTokens(options.maxCompletionTokens),\n stream: false,\n }),\n signal: AbortSignal.timeout(60_000),\n });\n\n const bodyText = await response.text();\n let payload;\n try {\n payload = bodyText ? JSON.parse(bodyText) : {};\n } catch {\n payload = { error: bodyText };\n }\n\n if (!response.ok) {\n const upstreamMessage = payload?.error?.message || payload?.message || '';\n const friendlyMessage =\n response.status === 402 || /insufficient\\s+balance/i.test(upstreamMessage)\n ? '大乘 AI 服务额度暂不可用,请联系管理员充值 DeepSeek 账户后重试。'\n : upstreamMessage || `DeepSeek request failed: ${response.status}`;\n const error = new Error(friendlyMessage);\n error.statusCode = response.status;\n if (response.status === 402 || /insufficient\\s+balance/i.test(upstreamMessage)) {\n error.code = 'DEEPSEEK_PROVIDER_QUOTA';\n }\n throw error;\n }\n\n const message = payload?.choices?.[0]?.message?.content?.trim();\n if (!message) {\n const error = new Error('DeepSeek returned an empty response');\n error.statusCode = 502;\n throw error;\n }\n\n return {\n message,\n model,\n usage: {\n promptTokens: Number(payload?.usage?.prompt_tokens || 0),\n completionTokens: Number(payload?.usage?.completion_tokens || 0),\n totalTokens: Number(payload?.usage?.total_tokens || 0),\n },\n };\n}\n\nasync function callDeepSeekStream(messages, callbacks = {}) {\n if (!deepseekApiKey) {\n const error = new Error('DeepSeek API key is not configured');\n error.statusCode = 503;\n throw error;\n }\n\n const model = normalizeDeepSeekModelName(callbacks.model);\n const response = await fetch(`${deepseekBaseUrl}/chat/completions`, {\n method: 'POST',\n headers: {\n Accept: 'text/event-stream',\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${deepseekApiKey}`,\n },\n body: JSON.stringify({\n model,\n messages,\n temperature: 0.4,\n max_tokens: normalizeMaxCompletionTokens(callbacks.maxCompletionTokens),\n stream: true,\n stream_options: { include_usage: true },\n }),\n signal: callbacks.signal || AbortSignal.timeout(90_000),\n });\n\n if (!response.ok) {\n const bodyText = await response.text();\n let payload;\n try {\n payload = bodyText ? JSON.parse(bodyText) : {};\n } catch {\n payload = { error: bodyText };\n }\n const upstreamMessage = payload?.error?.message || payload?.message || '';\n const friendlyMessage =\n response.status === 402 || /insufficient\\s+balance/i.test(upstreamMessage)\n ? '大乘 AI 服务额度暂不可用,请联系管理员充值 DeepSeek 账户后重试。'\n : upstreamMessage || `DeepSeek request failed: ${response.status}`;\n const error = new Error(friendlyMessage);\n error.statusCode = response.status;\n if (response.status === 402 || /insufficient\\s+balance/i.test(upstreamMessage)) {\n error.code = 'DEEPSEEK_PROVIDER_QUOTA';\n }\n throw error;\n }\n\n let buffer = '';\n let message = '';\n let usage = {\n promptTokens: 0,\n completionTokens: 0,\n totalTokens: 0,\n };\n const decoder = new TextDecoder();\n\n for await (const chunk of response.body) {\n buffer += decoder.decode(chunk, { stream: true });\n const lines = buffer.split(/\\r?\\n/);\n buffer = lines.pop() || '';\n\n for (const rawLine of lines) {\n const line = rawLine.trim();\n if (!line.startsWith('data:')) continue;\n const data = line.slice('data:'.length).trim();\n if (!data || data === '[DONE]') continue;\n\n let payload;\n try {\n payload = JSON.parse(data);\n } catch {\n continue;\n }\n\n const delta = payload?.choices?.[0]?.delta?.content || '';\n if (delta) {\n message += delta;\n callbacks.onToken?.(delta);\n }\n\n if (payload?.usage) {\n usage = {\n promptTokens: Number(payload.usage.prompt_tokens || usage.promptTokens || 0),\n completionTokens: Number(payload.usage.completion_tokens || usage.completionTokens || 0),\n totalTokens: Number(payload.usage.total_tokens || usage.totalTokens || 0),\n };\n }\n }\n }\n\n message = message.trim();\n if (!message) {\n const error = new Error('DeepSeek returned an empty response');\n error.statusCode = 502;\n throw error;\n }\n\n return { message, model, usage };\n}\n\nfunction createCodexDeepSeekRuntime() {\n if (!enableCodexSdkChat || !deepseekApiKey) {\n return {\n enabled: false,\n provider: 'deepseek-direct',\n reason: enableCodexSdkChat ? 'DeepSeek API key is not configured' : 'ENABLE_CODEX_SDK_CHAT is not true',\n };\n }\n\n return {\n enabled: true,\n provider: 'codex-sdk-deepseek',\n options: {\n apiKey: deepseekApiKey,\n baseUrl: deepseekBaseUrl,\n env: {\n ...process.env,\n DEEPSEEK_API_KEY: deepseekApiKey,\n HOME: codexHomeDir,\n CODEX_HOME: codexHomeDir,\n TMPDIR: codexTempDir,\n XDG_RUNTIME_DIR: codexRuntimeDir,\n XDG_CACHE_HOME: path.join(codexHomeDir, '.cache'),\n XDG_CONFIG_HOME: path.join(codexHomeDir, '.config'),\n XDG_DATA_HOME: path.join(codexHomeDir, '.local', 'share'),\n PATH: process.env.PATH || '/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin',\n },\n config: {\n model_provider: codexDeepSeekProviderId,\n model_providers: {\n [codexDeepSeekProviderId]: {\n name: 'DeepSeek Chat Completions',\n base_url: codexResponsesBaseUrl,\n env_key: 'DEEPSEEK_API_KEY',\n wire_api: 'responses',\n query_params: {},\n },\n },\n },\n },\n threadOptions: {\n model: deepseekModel,\n approvalPolicy: 'never',\n sandboxMode: 'read-only',\n skipGitRepoCheck: true,\n networkAccessEnabled: true,\n },\n };\n}\n\nfunction codexPromptFromMessages(messages) {\n return messages\n .map((message) => {\n const role = String(message.role || 'user').toUpperCase();\n return `${role}:\\n${message.content}`;\n })\n .join('\\n\\n');\n}\n\nfunction responseContentText(content) {\n if (typeof content === 'string') return content;\n if (!Array.isArray(content)) return '';\n return content\n .map((part) => {\n if (typeof part === 'string') return part;\n if (!part || typeof part !== 'object') return '';\n return String(part.text || part.input_text || part.output_text || '');\n })\n .filter(Boolean)\n .join('\\n');\n}\n\nfunction codexResponsesPrompt(body) {\n const input = Array.isArray(body?.input) ? body.input : [];\n const userItems = input.filter((item) => item?.role === 'user');\n const source = userItems[userItems.length - 1] || input[input.length - 1];\n const prompt = responseContentText(source?.content).trim();\n if (prompt) return prompt;\n if (typeof body?.input === 'string') return body.input.trim();\n return '';\n}\n\nfunction writeResponsesEvent(res, event, data) {\n res.write(`event: ${event}\\n`);\n res.write(`data: ${JSON.stringify(data)}\\n\\n`);\n}\n\nfunction responseUsage(usage = {}) {\n return {\n input_tokens: Number(usage.promptTokens || usage.input_tokens || 0),\n cached_input_tokens: 0,\n output_tokens: Number(usage.completionTokens || usage.output_tokens || 0),\n reasoning_output_tokens: 0,\n total_tokens: Number(usage.totalTokens || usage.total_tokens || 0),\n };\n}\n\nfunction createLibreChatAgentRuntime() {\n if (!enableLibreChatAgentChat || !libreChatAgentApiKey || !libreChatAgentId) {\n return {\n enabled: false,\n provider: 'librechat-agent',\n reason: !enableLibreChatAgentChat\n ? 'ENABLE_LIBRECHAT_AGENT_CHAT is not true'\n : !libreChatAgentApiKey\n ? 'LIBRECHAT_AGENT_API_KEY is not configured'\n : 'LIBRECHAT_AGENT_ID is not configured',\n };\n }\n\n return {\n enabled: true,\n provider: 'librechat-agent',\n model: libreChatAgentId,\n chatCompletionsUrl: `${libreChatAgentsBaseUrl}/chat/completions`,\n };\n}\n\nfunction libreChatUsage(usage = {}) {\n return {\n promptTokens: Number(usage.prompt_tokens || usage.input_tokens || usage.promptTokens || 0),\n completionTokens: Number(usage.completion_tokens || usage.output_tokens || usage.completionTokens || 0),\n totalTokens: Number(usage.total_tokens || usage.totalTokens || 0),\n };\n}\n\nasync function callLibreChatAgent(messages) {\n const runtime = createLibreChatAgentRuntime();\n if (!runtime.enabled) {\n const error = new Error(runtime.reason || 'LibreChat Agent chat is not enabled');\n error.statusCode = 503;\n throw error;\n }\n\n const response = await fetch(runtime.chatCompletionsUrl, {\n method: 'POST',\n headers: {\n Accept: 'application/json',\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${libreChatAgentApiKey}`,\n },\n body: JSON.stringify({\n model: runtime.model,\n messages,\n temperature: 0.4,\n stream: false,\n }),\n signal: AbortSignal.timeout(120_000),\n });\n\n const bodyText = await response.text();\n let payload;\n try {\n payload = bodyText ? JSON.parse(bodyText) : {};\n } catch {\n payload = { error: bodyText };\n }\n\n if (!response.ok) {\n const upstreamMessage = payload?.error?.message || payload?.message || bodyText || '';\n const error = new Error(upstreamMessage || `LibreChat Agent request failed: ${response.status}`);\n error.statusCode = response.status;\n throw error;\n }\n\n const message = payload?.choices?.[0]?.message?.content?.trim();\n if (!message) {\n const error = new Error('LibreChat Agent returned an empty response');\n error.statusCode = 502;\n throw error;\n }\n\n return {\n message,\n usage: libreChatUsage(payload?.usage),\n };\n}\n\nasync function callLibreChatAgentStream(messages, callbacks = {}) {\n const runtime = createLibreChatAgentRuntime();\n if (!runtime.enabled) {\n const error = new Error(runtime.reason || 'LibreChat Agent chat is not enabled');\n error.statusCode = 503;\n throw error;\n }\n\n const response = await fetch(runtime.chatCompletionsUrl, {\n method: 'POST',\n headers: {\n Accept: 'text/event-stream',\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${libreChatAgentApiKey}`,\n },\n body: JSON.stringify({\n model: runtime.model,\n messages,\n temperature: 0.4,\n stream: true,\n }),\n signal: callbacks.signal || AbortSignal.timeout(180_000),\n });\n\n if (!response.ok) {\n const bodyText = await response.text();\n let payload;\n try {\n payload = bodyText ? JSON.parse(bodyText) : {};\n } catch {\n payload = { error: bodyText };\n }\n const upstreamMessage = payload?.error?.message || payload?.message || bodyText || '';\n const error = new Error(upstreamMessage || `LibreChat Agent request failed: ${response.status}`);\n error.statusCode = response.status;\n throw error;\n }\n\n let buffer = '';\n let message = '';\n let usage = { promptTokens: 0, completionTokens: 0, totalTokens: 0 };\n const decoder = new TextDecoder();\n\n for await (const chunk of response.body) {\n buffer += decoder.decode(chunk, { stream: true });\n const lines = buffer.split(/\\r?\\n/);\n buffer = lines.pop() || '';\n\n for (const rawLine of lines) {\n const line = rawLine.trim();\n if (!line.startsWith('data:')) continue;\n const data = line.slice('data:'.length).trim();\n if (!data || data === '[DONE]') continue;\n\n let payload;\n try {\n payload = JSON.parse(data);\n } catch {\n continue;\n }\n\n const delta = payload?.choices?.[0]?.delta?.content || '';\n if (delta) {\n message += delta;\n callbacks.onToken?.(delta);\n }\n\n if (payload?.usage) {\n usage = libreChatUsage(payload.usage);\n }\n }\n }\n\n message = message.trim();\n if (!message) {\n const error = new Error('LibreChat Agent returned an empty response');\n error.statusCode = 502;\n throw error;\n }\n\n return { message, usage };\n}\n\nfunction isDeepSeekQuotaError(error) {\n if (error?.code === 'DEEPSEEK_PROVIDER_QUOTA') return true;\n if (error?.code === 'USER_TOKEN_QUOTA_EXCEEDED' || error?.code === 'AI_MEMBERSHIP_REQUIRED') return false;\n const message = String(error?.message || error || '');\n return /insufficient\\s+balance|balance/i.test(message);\n}\n\nfunction publicAiErrorMessage(error) {\n if (isDeepSeekQuotaError(error)) {\n return '大乘 AI 服务额度暂不可用,请联系管理员充值 DeepSeek 账户后重试。';\n }\n return error?.message || 'Internal Server Error';\n}\n\nfunction responsesPayload({\n responseId,\n itemId,\n status,\n model,\n text = '',\n usage = null,\n error = null,\n}) {\n return {\n id: responseId,\n object: 'response',\n status,\n model,\n output:\n status === 'completed'\n ? [\n {\n id: itemId,\n type: 'message',\n role: 'assistant',\n status: 'completed',\n content: [{ type: 'output_text', text }],\n },\n ]\n : [],\n ...(usage ? { usage: responseUsage(usage) } : {}),\n ...(error\n ? {\n error: {\n message: error.message || String(error),\n type: 'server_error',\n },\n }\n : {}),\n };\n}\n\nasync function callCodexSdkDeepSeek(messages, callbacks = {}) {\n const codexRuntime = createCodexDeepSeekRuntime();\n if (!codexRuntime.enabled) {\n const error = new Error(codexRuntime.reason || 'Codex SDK chat is not enabled');\n error.statusCode = 503;\n throw error;\n }\n\n const { Codex } = await import('@openai/codex-sdk');\n const codex = new Codex(codexRuntime.options);\n const thread = codex.startThread(codexRuntime.threadOptions);\n const prompt = codexPromptFromMessages(messages);\n\n if (callbacks.onToken || callbacks.onStep) {\n const { events } = await thread.runStreamed(prompt, { signal: callbacks.signal });\n let finalResponse = '';\n let usage = null;\n\n for await (const event of events) {\n if (event.type === 'item.completed') {\n const item = event.item;\n if (item.type === 'agent_message') {\n finalResponse = item.text || finalResponse;\n callbacks.onToken?.(item.text || '');\n } else if (item.type === 'todo_list') {\n callbacks.onStep?.({\n title: 'Codex SDK 执行计划',\n message: item.items.map((todo) => `${todo.completed ? '✓' : '•'} ${todo.text}`).join('\\n'),\n });\n } else if (item.type === 'command_execution') {\n callbacks.onStep?.({\n title: 'Codex SDK 执行命令',\n message: item.command,\n });\n } else if (item.type === 'mcp_tool_call') {\n callbacks.onStep?.({\n title: 'Codex SDK 调用工具',\n message: `${item.server}.${item.tool}`,\n });\n }\n } else if (event.type === 'turn.completed') {\n usage = event.usage;\n } else if (event.type === 'turn.failed') {\n throw new Error(event.error?.message || 'Codex SDK turn failed');\n } else if (event.type === 'error') {\n throw new Error(event.message || 'Codex SDK stream failed');\n }\n }\n\n if (!finalResponse.trim()) {\n const error = new Error('Codex SDK returned an empty response');\n error.statusCode = 502;\n throw error;\n }\n\n return {\n message: finalResponse.trim(),\n usage: {\n promptTokens: Number(usage?.input_tokens || 0),\n completionTokens: Number(usage?.output_tokens || 0),\n totalTokens: Number((usage?.input_tokens || 0) + (usage?.output_tokens || 0)),\n },\n };\n }\n\n const turn = await thread.run(prompt);\n if (!turn.finalResponse?.trim()) {\n const error = new Error('Codex SDK returned an empty response');\n error.statusCode = 502;\n throw error;\n }\n\n return {\n message: turn.finalResponse.trim(),\n usage: {\n promptTokens: Number(turn.usage?.input_tokens || 0),\n completionTokens: Number(turn.usage?.output_tokens || 0),\n totalTokens: Number((turn.usage?.input_tokens || 0) + (turn.usage?.output_tokens || 0)),\n },\n };\n}\n\nconst activeAgentRuns = new Map();\n\nfunction normalizeAgentMode(value) {\n const mode = safeUserText(value || 'dharma_guide');\n return /^[a-z][a-z0-9_:-]{0,48}$/i.test(mode) ? mode : 'dharma_guide';\n}\n\nfunction openClawSessionKey(user, conversationId) {\n const env = process.env.NODE_ENV === 'production' ? 'prod' : 'dev';\n return `fabushi:env:${env}:user:${user.userId}:conversation:${conversationId}`;\n}\n\nfunction createOpenClawRuntime() {\n if (!enableOpenClawAgentChat || !openClawGatewayToken || !openClawGatewayUrl) {\n return {\n enabled: false,\n provider: 'openclaw-gateway',\n reason: !enableOpenClawAgentChat\n ? 'ENABLE_OPENCLAW_AGENT_CHAT is not true'\n : !openClawGatewayToken\n ? 'OPENCLAW_GATEWAY_TOKEN is not configured'\n : 'OPENCLAW_GATEWAY_URL is not configured',\n };\n }\n\n return {\n enabled: true,\n provider: 'openclaw-gateway',\n model: openClawAgentId,\n runsUrl: openClawRunsEndpoint || `${openClawGatewayUrl}/v1/agents/${encodeURIComponent(openClawAgentId)}/runs`,\n };\n}\n\nfunction extractOpenClawText(payload) {\n if (!payload || typeof payload !== 'object') return '';\n const candidates = [\n payload.message,\n payload.content,\n payload.output_text,\n payload.finalResponse,\n payload.final_response,\n payload.result?.message,\n payload.result?.content,\n payload.output?.message,\n payload.output?.content,\n payload.choices?.[0]?.message?.content,\n ];\n for (const candidate of candidates) {\n if (typeof candidate === 'string' && candidate.trim()) return candidate.trim();\n }\n return '';\n}\n\nfunction extractOpenClawUsage(payload) {\n const usage = payload?.usage || payload?.result?.usage || {};\n return {\n promptTokens: Number(usage.input_tokens || usage.prompt_tokens || usage.promptTokens || 0),\n completionTokens: Number(usage.output_tokens || usage.completion_tokens || usage.completionTokens || 0),\n totalTokens: Number(usage.total_tokens || usage.totalTokens || 0),\n };\n}\n\nasync function callOpenClawAgent({ messages, user, conversationId, mode, signal }) {\n const runtime = createOpenClawRuntime();\n if (!runtime.enabled) {\n const error = new Error(runtime.reason || 'OpenClaw Agent chat is not enabled');\n error.statusCode = 503;\n throw error;\n }\n\n const sessionKey = openClawSessionKey(user, conversationId);\n const response = await fetch(runtime.runsUrl, {\n method: 'POST',\n headers: {\n Accept: 'application/json',\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${openClawGatewayToken}`,\n },\n body: JSON.stringify({\n agentId: openClawAgentId,\n sessionKey,\n mode,\n stream: false,\n input: { messages },\n messages,\n metadata: {\n app: 'fabushi',\n userId: user.userId,\n conversationId,\n toolPolicy: 'public-user-read-mostly',\n },\n }),\n signal: signal || AbortSignal.timeout(180_000),\n });\n\n const bodyText = await response.text();\n let payload;\n try {\n payload = bodyText ? JSON.parse(bodyText) : {};\n } catch {\n payload = { message: bodyText };\n }\n\n if (!response.ok) {\n const error = new Error(payload?.error?.message || payload?.message || `OpenClaw request failed: ${response.status}`);\n error.statusCode = response.status;\n throw error;\n }\n\n const message = extractOpenClawText(payload);\n if (!message) {\n const error = new Error('OpenClaw returned an empty response');\n error.statusCode = 502;\n throw error;\n }\n\n return {\n message,\n runId: payload.runId || payload.id || payload.result?.runId || null,\n provider: runtime.provider,\n model: runtime.model,\n usage: extractOpenClawUsage(payload),\n };\n}\n\nasync function runAgentModel({ messages, user, conversationId, mode, callbacks = {}, signal, maxCompletionTokens }) {\n const openClawRuntime = createOpenClawRuntime();\n if (openClawRuntime.enabled && !callbacks.onToken) {\n return await callOpenClawAgent({ messages, user, conversationId, mode, signal });\n }\n\n const codexRuntime = createCodexDeepSeekRuntime();\n const libreChatRuntime = createLibreChatAgentRuntime();\n let provider = libreChatRuntime.enabled\n ? libreChatRuntime.provider\n : codexRuntime.enabled\n ? codexRuntime.provider\n : 'deepseek';\n let model = libreChatRuntime.enabled ? libreChatRuntime.model : deepseekModel;\n\n try {\n if (libreChatRuntime.enabled) {\n const result = callbacks.onToken\n ? await callLibreChatAgentStream(messages, { ...callbacks, signal })\n : await callLibreChatAgent(messages);\n return { ...result, provider, model };\n }\n if (codexRuntime.enabled) {\n const result = await callCodexSdkDeepSeek(messages, { ...callbacks, signal });\n return { ...result, provider, model };\n }\n const result = callbacks.onToken\n ? await callDeepSeekStream(messages, { ...callbacks, signal, maxCompletionTokens })\n : await callDeepSeek(messages, { maxCompletionTokens });\n return { ...result, provider, model };\n } catch (error) {\n if (!codexRuntime.enabled || isDeepSeekQuotaError(error)) throw error;\n logger.warn({ error: String(error) }, 'Agent model failed, falling back to DeepSeek direct');\n const result = callbacks.onToken\n ? await callDeepSeekStream(messages, { ...callbacks, signal, maxCompletionTokens })\n : await callDeepSeek(messages, { maxCompletionTokens });\n return { ...result, provider: 'deepseek', model: deepseekModel };\n }\n}\n\nfunction updateAgentRunStatus(run, status, fields = {}) {\n statements.updateAgentRunStatus.run({\n id: run.id,\n userId: run.user_id || run.userId,\n status,\n openClawRunId: fields.openClawRunId ?? null,\n inputTokens: fields.inputTokens ?? null,\n outputTokens: fields.outputTokens ?? null,\n toolCallCount: fields.toolCallCount ?? null,\n completedAt: status === 'completed' || status === 'cancelled' ? nowIso() : null,\n failedAt: status === 'failed' ? nowIso() : null,\n errorCode: fields.errorCode ?? null,\n errorMessage: fields.errorMessage ?? null,\n });\n}\n\nfunction buildAgentMessages(conversationId) {\n const historyRows = statements.listMessages.all(conversationId).slice(-14);\n return [{ role: 'system', content: systemPrompt }, ...normalizeMessages(historyRows)];\n}\n\nfunction agentUsagePayload(usage, user, budget) {\n const promptTokens = usage.promptTokens || usage.inputTokens || 0;\n const completionTokens = usage.completionTokens || usage.outputTokens || 0;\n const totalTokens = usage.totalTokens || promptTokens + completionTokens;\n const latestUsage = usageFor(user.userId);\n return {\n inputTokens: promptTokens,\n outputTokens: completionTokens,\n totalTokens,\n monthlyLimit: budget.limit,\n remainingTokens: Math.max(0, budget.limit - latestUsage.used),\n };\n}\n\nfunction openAiContentToText(content) {\n if (typeof content === 'string') return content;\n if (Array.isArray(content)) {\n return content\n .map((part) => {\n if (typeof part === 'string') return part;\n if (!part || typeof part !== 'object') return '';\n return readText(part.text || part.content || part.value);\n })\n .filter(Boolean)\n .join('\\n');\n }\n return readText(content);\n}\n\nfunction normalizeOpenAiMessages(value) {\n if (!Array.isArray(value)) return [];\n const allowedRoles = new Set(['system', 'user', 'assistant', 'tool']);\n return value\n .map((item) => {\n if (!item || typeof item !== 'object') return null;\n const role = allowedRoles.has(item.role) ? item.role : 'user';\n const content = openAiContentToText(item.content);\n if (!content) return null;\n return { role, content };\n })\n .filter(Boolean);\n}\n\nfunction truncateForDeepSeekMessage(text, maxChars = 12_000) {\n const value = readText(text);\n if (value.length <= maxChars) return value;\n return `${value.slice(0, maxChars)}\\n\\n[内容已截断]`;\n}\n\nfunction truncateTailForDeepSeekMessage(text, maxChars = 2400) {\n const value = readText(text);\n if (value.length <= maxChars) return value;\n return `[前文已省略]\\n\\n${value.slice(-maxChars)}`;\n}\n\nfunction stripOpenClawPromptNoise(text) {\n const value = readText(text);\n if (!value) return '';\n\n const finalPromptMatch = value.match(/finalPromptText[\"'\\s::]+([\\s\\S]{1,4000})$/i);\n if (finalPromptMatch?.[1]) {\n return readText(finalPromptMatch[1].replace(/^[\"'`]+|[\"'`,}\\]]+$/g, ''));\n }\n\n const markers = [\n 'final prompt:',\n 'final user prompt:',\n 'user prompt:',\n '用户问题:',\n '用户消息:',\n '用户输入:',\n ];\n const lower = value.toLowerCase();\n for (const marker of markers) {\n const index = lower.lastIndexOf(marker.toLowerCase());\n if (index >= 0) {\n const tail = value.slice(index + marker.length).trim();\n if (tail) return tail;\n }\n }\n\n return value;\n}\n\nfunction compactOpenClawMessagesForDeepSeek(messages) {\n const conversationalMessages = messages\n .filter((message) => message.role === 'user' || message.role === 'assistant')\n .map((message) => ({\n role: message.role,\n content: truncateTailForDeepSeekMessage(stripOpenClawPromptNoise(message.content)),\n }))\n .filter((message) => message.content);\n\n const recent = conversationalMessages.slice(-6);\n while (recent.length > 0 && recent[0].role !== 'user') {\n recent.shift();\n }\n\n if (recent.length === 0) {\n const fallback = messages\n .slice()\n .reverse()\n .map((message) => truncateTailForDeepSeekMessage(stripOpenClawPromptNoise(message.content), 2400))\n .find(Boolean);\n if (!fallback) return messages;\n recent.push({ role: 'user', content: fallback });\n }\n\n return [\n {\n role: 'system',\n content: '你是大乘 App 的桌面 AI 助手。请直接回答用户问题,默认使用简洁、准确的中文。',\n },\n ...recent,\n ];\n}\n\nfunction isDeepSeekBlockedError(error) {\n return (\n Number(error?.statusCode) === 403 &&\n /blocked|被拦截|forbidden/i.test(String(error?.message || error || ''))\n );\n}\n\nfunction shouldRetryWithCompactedOpenClawMessages(error, messages, compactedMessages) {\n if (!isDeepSeekBlockedError(error)) return false;\n return JSON.stringify(messages) !== JSON.stringify(compactedMessages);\n}\n\nfunction openClawBlockedFallbackText(messages) {\n const lastUser = messages\n .slice()\n .reverse()\n .find((message) => message.role === 'user')?.content || '';\n const localActionIntent =\n /(file|folder|directory|project|workspace|path|文件|文件夹|目录|项目|工程|路径|本地)/i.test(lastUser) &&\n /(create|make|mkdir|write|edit|modify|rename|delete|move|copy|save|download|read|list|生成|创建|新建|写入|编辑|修改|重命名|删除|移动|复制|保存|下载|读取|列出)/i.test(lastUser);\n\n if (localActionIntent) {\n return [\n '本机工具规划被模型服务拦截了,所以这次没有实际读取、创建、修改或下载本机文件。',\n '如果只是新建文件夹,请先在底部选择项目后重新发送“新建文件夹”;桌面端会直接在已选项目里创建。',\n '如果是更复杂的本地文件操作,请在“连应用”里重启本机 AI 后重试,或把要操作的文件路径和目标说得更具体。',\n ].join('\\n');\n }\n\n return '模型服务拦截了这次回复,未能生成结果。请换一种更具体、更短的表述后重试。';\n}\nfunction usageWithFallback(usage, messages, text) {\n const promptTokens = Number(usage?.promptTokens || usage?.inputTokens || 0);\n const completionTokens = Number(usage?.completionTokens || usage?.outputTokens || 0);\n const totalTokens = Number(usage?.totalTokens || 0);\n if (totalTokens > 0) {\n return {\n promptTokens,\n completionTokens,\n totalTokens,\n };\n }\n const estimatedPrompt = promptTokens || estimateTokens(JSON.stringify(messages));\n const estimatedCompletion = completionTokens || estimateTokens(text);\n return {\n promptTokens: estimatedPrompt,\n completionTokens: estimatedCompletion,\n totalTokens: estimatedPrompt + estimatedCompletion,\n };\n}\n\nfunction openAiUsagePayload(usage, user, budget) {\n const promptTokens = Number(usage.promptTokens || 0);\n const completionTokens = Number(usage.completionTokens || 0);\n const totalTokens = Number(usage.totalTokens || promptTokens + completionTokens);\n const latestUsage = usageFor(user.userId);\n const remainingTokens = Math.max(0, budget.limit - latestUsage.used);\n return {\n prompt_tokens: promptTokens,\n completion_tokens: completionTokens,\n total_tokens: totalTokens,\n promptTokens,\n completionTokens,\n totalTokens,\n monthlyLimit: budget.limit,\n remainingTokens,\n };\n}\n\nfunction writeOpenAiSse(res, payload) {\n res.write(`data: ${JSON.stringify(payload)}\\n\\n`);\n}\n\napp.post(\n '/api/openclaw/deepseek/v1/chat/completions',\n asyncHandler(async (req, res) => {\n const user = await resolveUser(req, req.body || {});\n const incomingMessages = normalizeOpenAiMessages(req.body?.messages);\n if (incomingMessages.length === 0) {\n res.status(400).json({ error: { message: 'messages is required' } });\n return;\n }\n\n const messages = compactOpenClawMessagesForDeepSeek(incomingMessages);\n const promptEstimate = estimateTokens(JSON.stringify(messages));\n const estimated = promptEstimate + 600;\n const budget = enforceTokenBudget(user, estimated);\n const maxCompletionTokens = Math.min(\n completionBudgetFor(budget, promptEstimate),\n normalizeMaxCompletionTokens(req.body?.max_tokens),\n );\n const model = normalizeDeepSeekModelName(req.body?.model);\n const wantsStream =\n req.body?.stream === true ||\n String(req.get('accept') || '').includes('text/event-stream');\n const id = `chatcmpl_${crypto.randomUUID().replaceAll('-', '')}`;\n const created = Math.floor(Date.now() / 1000);\n const compactedMessages = messages;\n\n if (!wantsStream) {\n let result;\n let usageMessages = messages;\n try {\n result = await callDeepSeek(messages, { model, maxCompletionTokens });\n } catch (error) {\n if (!shouldRetryWithCompactedOpenClawMessages(error, messages, compactedMessages)) {\n throw error;\n }\n logger.warn(\n {\n statusCode: error?.statusCode,\n messageCount: messages.length,\n compactedMessageCount: compactedMessages.length,\n },\n 'OpenClaw DeepSeek proxy retrying with compacted messages',\n );\n usageMessages = compactedMessages;\n result = await callDeepSeek(compactedMessages, { model, maxCompletionTokens });\n }\n const usage = usageWithFallback(result.usage, usageMessages, result.message);\n recordUsage(user.userId, usage.totalTokens);\n res.json({\n id,\n object: 'chat.completion',\n created,\n model: result.model || model,\n choices: [\n {\n index: 0,\n message: { role: 'assistant', content: result.message },\n finish_reason: 'stop',\n },\n ],\n usage: openAiUsagePayload(usage, user, budget),\n });\n return;\n }\n\n const abortController = new AbortController();\n req.on('close', () => abortController.abort());\n\n res.status(200);\n res.set({\n 'Content-Type': 'text/event-stream; charset=utf-8',\n 'Cache-Control': 'no-cache, no-transform',\n Connection: 'keep-alive',\n 'X-Accel-Buffering': 'no',\n });\n res.flushHeaders?.();\n\n const baseChunk = {\n id,\n object: 'chat.completion.chunk',\n created,\n model,\n system_fingerprint: 'fabushi-openclaw-deepseek-proxy',\n };\n writeOpenAiSse(res, {\n ...baseChunk,\n choices: [{ index: 0, delta: { role: 'assistant' }, finish_reason: null }],\n });\n\n let text = '';\n let usageMessages = messages;\n try {\n let result;\n try {\n result = await callDeepSeekStream(messages, {\n model,\n maxCompletionTokens,\n signal: abortController.signal,\n onToken: (delta) => {\n text += delta;\n writeOpenAiSse(res, {\n ...baseChunk,\n choices: [{ index: 0, delta: { content: delta }, finish_reason: null }],\n });\n },\n });\n } catch (error) {\n if (!shouldRetryWithCompactedOpenClawMessages(error, messages, compactedMessages)) {\n throw error;\n }\n logger.warn(\n {\n statusCode: error?.statusCode,\n messageCount: messages.length,\n compactedMessageCount: compactedMessages.length,\n },\n 'OpenClaw DeepSeek proxy stream retrying with compacted messages',\n );\n text = '';\n usageMessages = compactedMessages;\n result = await callDeepSeekStream(compactedMessages, {\n model,\n maxCompletionTokens,\n signal: abortController.signal,\n onToken: (delta) => {\n text += delta;\n writeOpenAiSse(res, {\n ...baseChunk,\n choices: [{ index: 0, delta: { content: delta }, finish_reason: null }],\n });\n },\n });\n }\n text = result.message || text;\n const usage = usageWithFallback(result.usage, usageMessages, text);\n recordUsage(user.userId, usage.totalTokens);\n writeOpenAiSse(res, {\n ...baseChunk,\n model: result.model || model,\n choices: [{ index: 0, delta: {}, finish_reason: 'stop' }],\n });\n writeOpenAiSse(res, {\n ...baseChunk,\n model: result.model || model,\n choices: [],\n usage: openAiUsagePayload(usage, user, budget),\n });\n res.write('data: [DONE]\\n\\n');\n } catch (error) {\n if (isDeepSeekBlockedError(error)) {\n const fallbackMessage = openClawBlockedFallbackText(usageMessages);\n const usage = usageWithFallback(null, usageMessages, fallbackMessage);\n recordUsage(user.userId, usage.totalTokens);\n logger.warn(\n { statusCode: error?.statusCode, code: error?.code },\n 'OpenClaw DeepSeek proxy stream blocked; returning local fallback message',\n );\n writeOpenAiSse(res, {\n ...baseChunk,\n choices: [{ index: 0, delta: { content: fallbackMessage }, finish_reason: null }],\n });\n writeOpenAiSse(res, {\n ...baseChunk,\n choices: [{ index: 0, delta: {}, finish_reason: 'stop' }],\n });\n writeOpenAiSse(res, {\n ...baseChunk,\n choices: [],\n usage: openAiUsagePayload(usage, user, budget),\n });\n res.write('data: [DONE]\\n\\n');\n return;\n }\n logger.error(\n { error: String(error), code: error?.code, statusCode: error?.statusCode },\n 'OpenClaw DeepSeek proxy stream failed',\n );\n writeOpenAiSse(res, {\n ...baseChunk,\n choices: [],\n error: {\n message: publicAiErrorMessage(error),\n code: error?.code || 'OPENCLAW_DEEPSEEK_PROXY_ERROR',\n },\n });\n res.write('data: [DONE]\\n\\n');\n } finally {\n res.end();\n }\n }),\n);\n\napp.post(\n '/codex-deepseek/v1/responses',\n asyncHandler(async (req, res) => {\n const bearer = bearerToken(req);\n if (!deepseekApiKey || bearer !== deepseekApiKey) {\n res.status(401).json({\n error: { message: 'Unauthorized Codex DeepSeek adapter request' },\n });\n return;\n }\n\n const prompt = codexResponsesPrompt(req.body);\n if (!prompt) {\n res.status(400).json({ error: { message: 'Responses input is required' } });\n return;\n }\n\n const model = String(req.body?.model || deepseekModel);\n const responseId = `resp_${crypto.randomUUID().replaceAll('-', '')}`;\n const itemId = `msg_${crypto.randomUUID().replaceAll('-', '')}`;\n const wantsStream =\n req.body?.stream !== false ||\n String(req.get('accept') || '').includes('text/event-stream');\n const messages = [{ role: 'user', content: prompt }];\n\n if (!wantsStream) {\n const result = await callDeepSeek(messages);\n res.json(\n responsesPayload({\n responseId,\n itemId,\n status: 'completed',\n model,\n text: result.message,\n usage: result.usage,\n }),\n );\n return;\n }\n\n res.status(200);\n res.set({\n 'Content-Type': 'text/event-stream; charset=utf-8',\n 'Cache-Control': 'no-cache, no-transform',\n Connection: 'keep-alive',\n 'X-Accel-Buffering': 'no',\n });\n res.flushHeaders?.();\n\n writeResponsesEvent(res, 'response.created', {\n type: 'response.created',\n response: responsesPayload({\n responseId,\n itemId,\n status: 'in_progress',\n model,\n }),\n });\n writeResponsesEvent(res, 'response.output_item.added', {\n type: 'response.output_item.added',\n output_index: 0,\n item: {\n id: itemId,\n type: 'message',\n role: 'assistant',\n status: 'in_progress',\n content: [],\n },\n });\n writeResponsesEvent(res, 'response.content_part.added', {\n type: 'response.content_part.added',\n item_id: itemId,\n output_index: 0,\n content_index: 0,\n part: { type: 'output_text', text: '' },\n });\n\n let text = '';\n try {\n const result = await callDeepSeekStream(messages, {\n onToken: (delta) => {\n text += delta;\n writeResponsesEvent(res, 'response.output_text.delta', {\n type: 'response.output_text.delta',\n item_id: itemId,\n output_index: 0,\n content_index: 0,\n delta,\n });\n },\n });\n text = result.message || text;\n writeResponsesEvent(res, 'response.output_text.done', {\n type: 'response.output_text.done',\n item_id: itemId,\n output_index: 0,\n content_index: 0,\n text,\n });\n writeResponsesEvent(res, 'response.content_part.done', {\n type: 'response.content_part.done',\n item_id: itemId,\n output_index: 0,\n content_index: 0,\n part: { type: 'output_text', text },\n });\n writeResponsesEvent(res, 'response.output_item.done', {\n type: 'response.output_item.done',\n output_index: 0,\n item: {\n id: itemId,\n type: 'message',\n role: 'assistant',\n status: 'completed',\n content: [{ type: 'output_text', text }],\n },\n });\n writeResponsesEvent(res, 'response.completed', {\n type: 'response.completed',\n response: responsesPayload({\n responseId,\n itemId,\n status: 'completed',\n model,\n text,\n usage: result.usage,\n }),\n });\n res.end();\n } catch (error) {\n writeResponsesEvent(res, 'response.failed', {\n type: 'response.failed',\n response: responsesPayload({\n responseId,\n itemId,\n status: 'failed',\n model,\n text,\n error,\n }),\n });\n res.end();\n }\n }),\n);\n\napp.use(\n '/api/openclaw/runtime/files',\n express.static(openClawRuntimeUpdatesDir, {\n fallthrough: false,\n immutable: true,\n maxAge: '7d',\n setHeaders(res) {\n res.setHeader('X-Content-Type-Options', 'nosniff');\n },\n }),\n);\n\napp.get(\n '/api/openclaw/runtime/manifest',\n asyncHandler(async (req, res) => {\n const platform = readText(req.query.platform);\n const currentVersion = readText(req.query.currentVersion);\n const appVersion = readText(req.query.appVersion);\n\n let manifest;\n try {\n manifest = await readJsonFile(openClawRuntimeManifestPath);\n } catch (error) {\n logger.info(\n { manifestPath: openClawRuntimeManifestPath, error: String(error) },\n 'OpenClaw runtime manifest unavailable',\n );\n return jsonResponse(res, 200, {\n success: true,\n schema: 1,\n updateAvailable: false,\n reason: 'manifest-unavailable',\n platform,\n currentVersion,\n appVersion,\n });\n }\n\n const latest = manifest.latest && typeof manifest.latest === 'object'\n ? manifest.latest\n : manifest;\n const platforms = latest.platforms && typeof latest.platforms === 'object'\n ? latest.platforms\n : {};\n const platformRelease = platform ? platforms[platform] : null;\n const version = readText(latest.version || manifest.version);\n\n if (!platform || !platformRelease || typeof platformRelease !== 'object') {\n return jsonResponse(res, 200, {\n success: true,\n schema: Number(manifest.schema || 1),\n channel: readText(manifest.channel || latest.channel || 'stable'),\n updateAvailable: false,\n reason: platform ? 'platform-unavailable' : 'platform-required',\n platform,\n currentVersion,\n appVersion,\n latestVersion: version,\n });\n }\n\n if (platform.startsWith('macos-') && !openClawRuntimeEnableMacosUpdates) {\n return jsonResponse(res, 200, {\n success: true,\n schema: Number(manifest.schema || 1),\n channel: readText(manifest.channel || latest.channel || 'stable'),\n updateAvailable: false,\n reason: 'macos-runtime-updates-disabled',\n platform,\n currentVersion,\n appVersion,\n latestVersion: version,\n });\n }\n\n const downloadUrl = absolutizeOpenClawRuntimeUrl(\n req,\n platformRelease.archiveUrl || platformRelease.url || platformRelease.fileName,\n );\n const release = {\n version,\n channel: readText(manifest.channel || latest.channel || 'stable'),\n minAppVersion: readText(latest.minAppVersion || platformRelease.minAppVersion),\n notes: readText(latest.notes || platformRelease.notes),\n nodeExecutable: readText(\n platformRelease.nodeExecutable || latest.nodeExecutable || 'node/bin/node',\n ),\n cliEntrypoint: readText(\n platformRelease.cliEntrypoint || latest.cliEntrypoint || 'openclaw/openclaw.mjs',\n ),\n defaultPort: Number(latest.defaultPort || manifest.defaultPort || 18789),\n defaultModel: readText(latest.defaultModel || manifest.defaultModel || 'openclaw/default'),\n defaultModelOverride: readText(latest.defaultModelOverride || manifest.defaultModelOverride),\n gatewayArgs: Array.isArray(latest.gatewayArgs)\n ? latest.gatewayArgs.map((item) => String(item))\n : ['gateway', '--port', '{port}', '--force'],\n archiveUrl: downloadUrl,\n sha256: readText(platformRelease.sha256),\n size: Number(platformRelease.size || 0),\n };\n\n jsonResponse(res, 200, {\n success: true,\n schema: Number(manifest.schema || 1),\n updateAvailable: Boolean(version && downloadUrl && version !== currentVersion),\n platform,\n currentVersion,\n appVersion,\n latest: release,\n generatedAt: readText(manifest.generatedAt || latest.generatedAt),\n });\n }),\n);\n\napp.get('/health', (_req, res) => {\n const codexRuntime = createCodexDeepSeekRuntime();\n const libreChatRuntime = createLibreChatAgentRuntime();\n const openClawRuntime = createOpenClawRuntime();\n jsonResponse(res, 200, {\n status: 'ok',\n service: 'dacheng-ai-backend',\n provider: libreChatRuntime.enabled ? libreChatRuntime.provider : 'deepseek',\n model: libreChatRuntime.enabled ? libreChatRuntime.model : deepseekModel,\n deepseekModel,\n deepseekAvailableModels,\n libreChatAgentChatEnabled: libreChatRuntime.enabled,\n libreChatAgentProvider: libreChatRuntime.provider,\n libreChatAgentReason: libreChatRuntime.enabled ? undefined : libreChatRuntime.reason,\n codexSdkAvailable: true,\n codexSdkChatEnabled: codexRuntime.enabled,\n codexSdkProvider: codexRuntime.provider,\n openClawAgentChatEnabled: openClawRuntime.enabled,\n openClawAgentProvider: openClawRuntime.provider,\n openClawAgentReason: openClawRuntime.enabled ? undefined : openClawRuntime.reason,\n requireMemberForAi,\n memberMonthlyLimit,\n freeMonthlyLimit,\n deepseekMaxCompletionTokens,\n openClawRuntimeUpdatesDir,\n openClawRuntimeManifestConfigured: fs.existsSync(openClawRuntimeManifestPath),\n cbetaApiRoot,\n timestamp: nowIso(),\n });\n});\n\napp.get(\n '/api/miniapps/registry',\n asyncHandler(async (req, res) => {\n const user = await resolveUser(req, { username: req.query.username });\n const official = officialMiniAppRegistry(req);\n const store = await readMiniAppStore();\n const visibleSandbox = store.apps.filter((appRecord) => {\n if (appRecord.reviewStatus === 'approved' && appRecord.source === 'marketplace') {\n return true;\n }\n return appRecord.ownerId === user.userId;\n });\n const bots = [\n ...official.bots,\n ...visibleSandbox.map((record) => miniAppRecordToBot(record)),\n ];\n const miniApps = [\n ...official.miniApps,\n ...visibleSandbox.map((record) => miniAppRecordToManifest(req, record)),\n ];\n const registry = {\n schemaVersion: 1,\n hostApiVersion: miniAppHostApiVersion,\n bots,\n miniApps,\n updatedAt: nowIso(),\n };\n registry.signature = sha256(JSON.stringify({ bots, miniApps }));\n jsonResponse(res, 200, { success: true, registry });\n }),\n);\n\napp.get(\n '/api/miniapps/dev/:miniAppId/index.html',\n asyncHandler(async (req, res) => {\n const miniAppId = safeMiniAppId(req.params.miniAppId);\n const store = await readMiniAppStore();\n const record = store.apps.find((appRecord) => appRecord.miniAppId === miniAppId);\n if (!record) {\n res.status(404).type('text/html').send('<h1>Mini app not found</h1>');\n return;\n }\n res.setHeader('Content-Type', 'text/html; charset=utf-8');\n res.setHeader('Cache-Control', 'no-store');\n res.send(record.sourceHtml || generatedMiniAppHtml(record));\n }),\n);\n\napp.post(\n '/api/miniapps/dev/create',\n asyncHandler(async (req, res) => {\n const user = await resolveUser(req, req.body || {});\n const title = safeUserText(req.body?.title, '个人小程序').slice(0, 80);\n const subtitle = safeUserText(req.body?.subtitle, '个人沙箱小程序').slice(0, 120);\n const permissions = sanitizeMiniAppPermissions(req.body?.permissions);\n const prompt = safeUserText(req.body?.prompt || title);\n const idSeed = crypto.randomUUID().replaceAll('-', '').slice(0, 12);\n const miniAppId = `sandbox.${idSeed}`;\n const botId = `bot.${miniAppId}`;\n const sourceHtml = generatedMiniAppHtml({ title, prompt });\n const scan = miniAppScan(permissions, sourceHtml);\n const createdAt = nowIso();\n const record = {\n ownerId: user.userId,\n miniAppId,\n botId,\n title,\n subtitle,\n initials: title.slice(0, 1) || '小',\n icon: 'code',\n avatarColor: '#3D8BFF',\n greeting: '你好,我是这个个人沙箱小程序的机器人。',\n inputHint: '输入消息,或打开小程序面板',\n version: '0.0.1',\n permissions,\n surfaces: ['chatPanel'],\n theme: 'telegramDark',\n source: 'sandbox',\n reviewStatus: 'sandbox',\n sourceHtml,\n scan,\n createdAt,\n updatedAt: createdAt,\n };\n const store = await readMiniAppStore();\n store.apps.push(record);\n await writeMiniAppStore(store);\n jsonResponse(res, 200, {\n success: true,\n miniApp: miniAppRecordToManifest(req, record),\n bot: miniAppRecordToBot(record),\n scan,\n });\n }),\n);\n\napp.post(\n '/api/miniapps/dev/:miniAppId/version',\n asyncHandler(async (req, res) => {\n const user = await resolveUser(req, req.body || {});\n const miniAppId = safeMiniAppId(req.params.miniAppId);\n const store = await readMiniAppStore();\n const index = store.apps.findIndex((appRecord) => appRecord.miniAppId === miniAppId);\n if (index < 0) {\n return jsonResponse(res, 404, { success: false, message: 'mini app not found' });\n }\n const record = store.apps[index];\n if (record.ownerId !== user.userId) {\n return jsonResponse(res, 403, { success: false, message: 'not your mini app' });\n }\n const title = safeUserText(req.body?.title, record.title).slice(0, 80);\n const subtitle = safeUserText(req.body?.subtitle, record.subtitle).slice(0, 120);\n const sourceHtml =\n safeUserText(req.body?.sourceHtml) ||\n generatedMiniAppHtml({ title, prompt: safeUserText(req.body?.prompt || title) });\n const permissions = sanitizeMiniAppPermissions(req.body?.permissions || record.permissions);\n const scan = miniAppScan(permissions, sourceHtml);\n store.apps[index] = {\n ...record,\n title,\n subtitle,\n sourceHtml,\n permissions,\n scan,\n reviewStatus: 'sandbox',\n version: safeUserText(req.body?.version, record.version || '0.0.1'),\n updatedAt: nowIso(),\n };\n await writeMiniAppStore(store);\n jsonResponse(res, 200, {\n success: true,\n miniApp: miniAppRecordToManifest(req, store.apps[index]),\n bot: miniAppRecordToBot(store.apps[index]),\n scan,\n });\n }),\n);\n\napp.post(\n '/api/miniapps/:miniAppId/submit-review',\n asyncHandler(async (req, res) => {\n const user = await resolveUser(req, req.body || {});\n const miniAppId = safeMiniAppId(req.params.miniAppId);\n const store = await readMiniAppStore();\n const index = store.apps.findIndex((appRecord) => appRecord.miniAppId === miniAppId);\n if (index < 0) {\n return jsonResponse(res, 404, { success: false, message: 'mini app not found' });\n }\n const record = store.apps[index];\n if (record.ownerId !== user.userId) {\n return jsonResponse(res, 403, { success: false, message: 'not your mini app' });\n }\n const scan = miniAppScan(record.permissions || [], record.sourceHtml || '');\n if (!scan.passed) {\n return jsonResponse(res, 400, {\n success: false,\n message: 'security scan failed',\n scan,\n });\n }\n store.apps[index] = {\n ...record,\n scan,\n reviewStatus: 'pending_review',\n updatedAt: nowIso(),\n };\n await writeMiniAppStore(store);\n jsonResponse(res, 200, {\n success: true,\n miniApp: miniAppRecordToManifest(req, store.apps[index]),\n scan,\n });\n }),\n);\n\napp.post(\n '/api/botfather/generate-miniapp',\n asyncHandler(async (req, res) => {\n const user = await resolveUser(req, req.body || {});\n const prompt = safeUserText(req.body?.prompt);\n if (!prompt) {\n return jsonResponse(res, 400, { success: false, message: 'prompt is required' });\n }\n const title = titleFromPromptForMiniApp(prompt);\n const subtitle = '机器人之父生成的个人沙箱小程序';\n const requestedPermissions = Array.isArray(req.body?.permissions) ? req.body.permissions : [];\n const generated = await generateBotFatherMiniAppHtml({ title, prompt, user });\n let sourceHtml = generated.sourceHtml;\n let generation = generated.generation;\n const permissions = sanitizeMiniAppPermissions([\n ...requestedPermissions,\n ...inferMiniAppPermissionsFromText(prompt, sourceHtml),\n ]);\n let scan = miniAppScan(permissions, sourceHtml);\n if (!scan.passed) {\n sourceHtml = generatedMiniAppHtml({ title, prompt });\n generation = {\n provider: 'template',\n fallbackReason: 'ai_output_failed_security_scan',\n };\n scan = miniAppScan(permissions, sourceHtml);\n }\n const idSeed = crypto.randomUUID().replaceAll('-', '').slice(0, 12);\n const miniAppId = `sandbox.${idSeed}`;\n const createdAt = nowIso();\n const record = {\n ownerId: user.userId,\n miniAppId,\n botId: `bot.${miniAppId}`,\n title,\n subtitle,\n initials: title.slice(0, 1) || '小',\n icon: 'code',\n avatarColor: '#3D8BFF',\n greeting: `我是「${title}」的机器人。`,\n inputHint: '继续描述修改需求,或打开小程序面板',\n version: '0.0.1',\n permissions,\n surfaces: ['chatPanel'],\n theme: 'telegramDark',\n source: 'sandbox',\n reviewStatus: 'sandbox',\n sourceHtml,\n scan,\n generation,\n prompt,\n createdAt,\n updatedAt: createdAt,\n };\n const store = await readMiniAppStore();\n store.apps.push(record);\n await writeMiniAppStore(store);\n jsonResponse(res, 200, {\n success: true,\n miniApp: miniAppRecordToManifest(req, record),\n bot: miniAppRecordToBot(record),\n scan,\n generation,\n actions: ['openMiniApp', 'continueEditing', 'submitReview'],\n });\n }),\n);\n\napp.get('/api/ai/models', (_req, res) => {\n jsonResponse(res, 200, {\n success: true,\n provider: 'deepseek',\n defaultModel: deepseekModel,\n models: deepseekAvailableModels.map((model) => ({\n id: model,\n label: model === 'deepseek-reasoner' ? 'DeepSeek Reasoner' : 'DeepSeek Chat',\n })),\n });\n});\napp.get(\n '/api/ai/quota',\n asyncHandler(async (req, res) => {\n const user = await resolveUser(req, { username: req.query.username });\n const budget = enforceTokenBudget(user, 0);\n const usage = usageFor(user.userId);\n jsonResponse(res, 200, {\n success: true,\n isMember: user.isMember,\n monthlyLimit: budget.limit,\n usedTokens: usage.used,\n remainingTokens: Math.max(0, budget.limit - usage.used),\n month: usage.month,\n });\n }),\n);\n\napp.post(\n '/api/agent/chat',\n asyncHandler(async (req, res) => {\n const message = safeUserText(req.body?.message);\n if (!message) {\n return jsonResponse(res, 400, { success: false, message: 'message is required' });\n }\n\n const user = await resolveUser(req, req.body || {});\n const promptEstimate = estimateTokens(message);\n const estimated = promptEstimate + 600;\n const budget = enforceTokenBudget(user, estimated);\n const maxCompletionTokens = completionBudgetFor(budget, promptEstimate);\n const createdAt = nowIso();\n const conversationId = safeUserText(req.body?.conversationId) || crypto.randomUUID();\n const mode = normalizeAgentMode(req.body?.mode);\n const stream = req.body?.stream === true;\n let conversation = statements.getConversation.get(conversationId, user.userId);\n const isNewConversation = !conversation;\n\n if (isNewConversation) {\n conversation = { id: conversationId, user_id: user.userId, title: titleFromMessage(message) };\n statements.insertConversation.run({\n id: conversationId,\n userId: user.userId,\n title: conversation.title,\n provider: 'agent',\n model: openClawAgentId,\n createdAt,\n updatedAt: createdAt,\n });\n }\n\n const messageId = safeUserText(req.body?.messageId) || crypto.randomUUID();\n statements.insertMessage.run({\n id: messageId,\n conversationId,\n role: 'user',\n content: message,\n promptTokens: 0,\n completionTokens: 0,\n totalTokens: 0,\n createdAt,\n });\n\n const runId = `run_${crypto.randomUUID().replaceAll('-', '')}`;\n const sessionKey = openClawSessionKey(user, conversationId);\n const openClawRuntime = createOpenClawRuntime();\n statements.insertAgentRun.run({\n id: runId,\n userId: user.userId,\n conversationId,\n messageId,\n openClawRunId: null,\n openClawSessionKey: sessionKey,\n status: stream ? 'queued' : 'running',\n mode,\n provider: openClawRuntime.enabled ? openClawRuntime.provider : 'dacheng-ai-fallback',\n model: openClawRuntime.enabled ? openClawAgentId : deepseekModel,\n startedAt: createdAt,\n });\n\n if (stream) {\n return jsonResponse(res, 200, {\n success: true,\n runId,\n conversationId,\n streamUrl: `/api/agent/runs/${runId}/events`,\n });\n }\n\n const controller = new AbortController();\n activeAgentRuns.set(runId, controller);\n try {\n const skillResult = await runResourceFinderSkill({ message, user });\n const skillContext = resourceContextMessage(skillResult);\n const historyRows = statements.listMessages.all(conversationId).slice(-14);\n const modelMessages = [\n { role: 'system', content: systemPrompt },\n ...(skillContext ? [{ role: 'system', content: skillContext }] : []),\n ...normalizeMessages(historyRows),\n ];\n\n const aiResult = await runAgentModel({\n messages: modelMessages,\n user,\n conversationId,\n mode,\n signal: controller.signal,\n maxCompletionTokens,\n });\n const usage = {\n promptTokens: aiResult.usage?.promptTokens || estimateTokens(JSON.stringify(modelMessages)),\n completionTokens: aiResult.usage?.completionTokens || estimateTokens(aiResult.message),\n totalTokens:\n aiResult.usage?.totalTokens ||\n estimateTokens(JSON.stringify(modelMessages)) + estimateTokens(aiResult.message),\n };\n recordUsage(user.userId, usage.totalTokens);\n const answeredAt = nowIso();\n const assistantMessageId = crypto.randomUUID();\n statements.insertMessage.run({\n id: assistantMessageId,\n conversationId,\n role: 'assistant',\n content: aiResult.message,\n promptTokens: usage.promptTokens,\n completionTokens: usage.completionTokens,\n totalTokens: usage.totalTokens,\n createdAt: answeredAt,\n });\n statements.updateConversation.run({\n id: conversationId,\n userId: user.userId,\n title: isNewConversation ? titleFromMessage(message) : conversation.title,\n updatedAt: answeredAt,\n });\n updateAgentRunStatus({ id: runId, user_id: user.userId }, 'completed', {\n openClawRunId: aiResult.runId,\n inputTokens: usage.promptTokens,\n outputTokens: usage.completionTokens,\n });\n const action = determineClientAction(skillResult, message);\n return jsonResponse(res, 200, {\n success: true,\n runId,\n conversationId,\n messageId: assistantMessageId,\n provider: aiResult.provider,\n model: aiResult.model,\n message: aiResult.message,\n usage: agentUsagePayload(usage, user, budget),\n ...(action ? { clientAction: action } : {}),\n });\n } catch (error) {\n const status = error.name === 'AbortError' ? 'cancelled' : 'failed';\n updateAgentRunStatus({ id: runId, user_id: user.userId }, status, {\n errorCode: error.name || 'AGENT_RUN_FAILED',\n errorMessage: publicAiErrorMessage(error),\n });\n if (status === 'cancelled') {\n return jsonResponse(res, 499, { success: false, runId, status: 'cancelled' });\n }\n throw error;\n } finally {\n activeAgentRuns.delete(runId);\n }\n }),\n);\n\napp.get(\n '/api/agent/runs/:runId/events',\n asyncHandler(async (req, res) => {\n const user = await resolveUser(req, {});\n const run = statements.getAgentRun.get(req.params.runId, user.userId);\n if (!run) {\n return jsonResponse(res, 404, { success: false, message: 'run not found' });\n }\n if (run.status === 'completed' || run.status === 'failed' || run.status === 'cancelled') {\n return jsonResponse(res, 409, { success: false, message: `run is already ${run.status}` });\n }\n\n sseHeaders(res);\n const controller = new AbortController();\n activeAgentRuns.set(run.id, controller);\n updateAgentRunStatus(run, 'running');\n writeSse(res, 'run.started', { runId: run.id, conversationId: run.conversation_id });\n\n try {\n let streamedText = '';\n const modelMessages = buildAgentMessages(run.conversation_id);\n const lastUserMsg = [...modelMessages].reverse().find(m => m.role === 'user');\n const userMsgText = lastUserMsg ? lastUserMsg.content : '';\n const skillResult = userMsgText ? await runResourceFinderSkill({\n message: userMsgText,\n user,\n onStep: (step) => writeSse(res, 'tool.call.completed', {\n tool: 'agent.step',\n displayName: step.title || 'Agent step',\n summary: step.message || '',\n }),\n }) : null;\n const skillContext = resourceContextMessage(skillResult);\n const finalMessages = skillContext ? [\n modelMessages[0],\n { role: 'system', content: skillContext },\n ...modelMessages.slice(1)\n ] : modelMessages;\n\n const promptEstimate = estimateTokens(JSON.stringify(finalMessages));\n const estimated = promptEstimate + 600;\n const budget = enforceTokenBudget(user, estimated);\n const maxCompletionTokens = completionBudgetFor(budget, promptEstimate);\n const aiResult = await runAgentModel({\n messages: finalMessages,\n user,\n conversationId: run.conversation_id,\n mode: run.mode,\n signal: controller.signal,\n maxCompletionTokens,\n callbacks: {\n onStep: (step) => writeSse(res, 'tool.call.completed', {\n tool: 'agent.step',\n displayName: step.title || 'Agent step',\n summary: step.message || '',\n }),\n onToken: (delta) => {\n streamedText += delta;\n writeSse(res, 'assistant.delta', { text: delta });\n },\n },\n });\n const message = aiResult.message || streamedText.trim();\n const usage = {\n promptTokens: aiResult.usage?.promptTokens || estimateTokens(JSON.stringify(finalMessages)),\n completionTokens: aiResult.usage?.completionTokens || estimateTokens(message),\n totalTokens:\n aiResult.usage?.totalTokens || estimateTokens(JSON.stringify(finalMessages)) + estimateTokens(message),\n };\n recordUsage(user.userId, usage.totalTokens);\n const assistantMessageId = crypto.randomUUID();\n const answeredAt = nowIso();\n statements.insertMessage.run({\n id: assistantMessageId,\n conversationId: run.conversation_id,\n role: 'assistant',\n content: message,\n promptTokens: usage.promptTokens,\n completionTokens: usage.completionTokens,\n totalTokens: usage.totalTokens,\n createdAt: answeredAt,\n });\n statements.updateConversation.run({\n id: run.conversation_id,\n userId: user.userId,\n title: titleFromMessage(message),\n updatedAt: answeredAt,\n });\n updateAgentRunStatus(run, 'completed', {\n openClawRunId: aiResult.runId,\n inputTokens: usage.promptTokens,\n outputTokens: usage.completionTokens,\n });\n const action = determineClientAction(skillResult, userMsgText);\n writeSse(res, 'assistant.message', { messageId: assistantMessageId, content: message });\n writeSse(res, 'run.completed', {\n usage: agentUsagePayload(usage, user, budget),\n ...(action ? { clientAction: action } : {}),\n });\n res.end();\n } catch (error) {\n const status = error.name === 'AbortError' ? 'cancelled' : 'failed';\n updateAgentRunStatus(run, status, {\n errorCode: error.name || 'AGENT_RUN_FAILED',\n errorMessage: publicAiErrorMessage(error),\n });\n writeSse(res, status === 'cancelled' ? 'run.cancelled' : 'run.failed', {\n runId: run.id,\n message: publicAiErrorMessage(error),\n });\n res.end();\n } finally {\n activeAgentRuns.delete(run.id);\n }\n }),\n);\n\napp.post(\n '/api/agent/runs/:runId/cancel',\n asyncHandler(async (req, res) => {\n const user = await resolveUser(req, req.body || {});\n const run = statements.getAgentRun.get(req.params.runId, user.userId);\n if (!run) {\n return jsonResponse(res, 404, { success: false, message: 'run not found' });\n }\n const controller = activeAgentRuns.get(run.id);\n if (controller) controller.abort();\n updateAgentRunStatus(run, 'cancelled');\n return jsonResponse(res, 200, { success: true, runId: run.id, status: 'cancelled' });\n }),\n);\n\napp.post(\n '/api/agent/messages/:messageId/feedback',\n asyncHandler(async (req, res) => {\n const user = await resolveUser(req, req.body || {});\n const rating = safeUserText(req.body?.rating);\n if (!['up', 'down'].includes(rating)) {\n return jsonResponse(res, 400, { success: false, message: 'rating must be up or down' });\n }\n statements.insertAgentFeedback.run({\n id: crypto.randomUUID(),\n messageId: req.params.messageId,\n userId: user.userId,\n rating,\n reason: safeUserText(req.body?.reason),\n comment: safeUserText(req.body?.comment),\n createdAt: nowIso(),\n });\n return jsonResponse(res, 200, { success: true });\n }),\n);\n\napp.post(\n '/api/ai/chat',\n asyncHandler(async (req, res) => {\n const message = safeUserText(req.body?.message);\n if (!message) {\n return jsonResponse(res, 400, { success: false, message: 'message is required' });\n }\n\n const user = await resolveUser(req, req.body || {});\n const promptEstimate = estimateTokens(message);\n const estimated = promptEstimate + 600;\n const budget = enforceTokenBudget(user, estimated);\n const maxCompletionTokens = completionBudgetFor(budget, promptEstimate);\n const createdAt = nowIso();\n const conversationId = safeUserText(req.body?.conversationId) || crypto.randomUUID();\n const requestedModel = normalizeDeepSeekModelName(req.body?.model);\n const clientContext = clientContextMessage(req.body?.client);\n let conversation = statements.getConversation.get(conversationId, user.userId);\n const isNewConversation = !conversation;\n\n if (isNewConversation) {\n conversation = {\n id: conversationId,\n user_id: user.userId,\n title: titleFromMessage(message),\n };\n statements.insertConversation.run({\n id: conversationId,\n userId: user.userId,\n title: conversation.title,\n provider: 'deepseek',\n model: requestedModel,\n createdAt,\n updatedAt: createdAt,\n });\n }\n\n statements.insertMessage.run({\n id: crypto.randomUUID(),\n conversationId,\n role: 'user',\n content: message,\n promptTokens: 0,\n completionTokens: 0,\n totalTokens: 0,\n createdAt,\n });\n\n const libreChatRuntime = createLibreChatAgentRuntime();\n const skillResult = await runResourceFinderSkill({ message, user });\n const skillContext = resourceContextMessage(skillResult);\n const historyRows = statements.listMessages.all(conversationId).slice(-14);\n const modelMessages = [\n { role: 'system', content: systemPrompt },\n ...(clientContext ? [{ role: 'system', content: clientContext }] : []),\n ...(skillContext ? [{ role: 'system', content: skillContext }] : []),\n ...normalizeMessages(historyRows),\n ];\n\n const openClawRuntime = createOpenClawRuntime();\n const codexRuntime = createCodexDeepSeekRuntime();\n let provider = openClawRuntime.enabled\n ? openClawRuntime.provider\n : libreChatRuntime.enabled\n ? libreChatRuntime.provider\n : 'deepseek';\n let responseModel = openClawRuntime.enabled\n ? openClawRuntime.model\n : libreChatRuntime.enabled\n ? libreChatRuntime.model\n : requestedModel;\n let aiResult;\n try {\n if (openClawRuntime.enabled) {\n aiResult = await callOpenClawAgent({\n messages: modelMessages,\n user,\n conversationId,\n mode: normalizeAgentMode(req.body?.mode),\n });\n provider = aiResult.provider || provider;\n responseModel = aiResult.model || responseModel;\n } else if (libreChatRuntime.enabled) {\n aiResult = await callLibreChatAgent(modelMessages);\n } else if (codexRuntime.enabled) {\n provider = codexRuntime.provider;\n aiResult = await callCodexSdkDeepSeek(modelMessages);\n } else {\n aiResult = await callDeepSeek(modelMessages, { model: requestedModel, maxCompletionTokens });\n }\n } catch (error) {\n if (libreChatRuntime.enabled) {\n logger.warn({ error: String(error) }, 'LibreChat Agent chat failed, falling back to Codex SDK or DeepSeek direct');\n provider = codexRuntime.enabled ? codexRuntime.provider : 'deepseek';\n responseModel = requestedModel;\n if (codexRuntime.enabled) {\n try {\n aiResult = await callCodexSdkDeepSeek(modelMessages);\n } catch (codexError) {\n if (isDeepSeekQuotaError(codexError)) throw codexError;\n logger.warn({ error: String(codexError) }, 'Codex SDK chat failed, falling back to DeepSeek direct');\n provider = 'deepseek';\n aiResult = await callDeepSeek(modelMessages, { model: requestedModel, maxCompletionTokens });\n }\n } else {\n aiResult = await callDeepSeek(modelMessages, { model: requestedModel, maxCompletionTokens });\n }\n } else {\n if (!codexRuntime.enabled || isDeepSeekQuotaError(error)) throw error;\n logger.warn({ error: String(error) }, 'Codex SDK chat failed, falling back to DeepSeek direct');\n provider = 'deepseek';\n responseModel = requestedModel;\n aiResult = await callDeepSeek(modelMessages, { model: requestedModel, maxCompletionTokens });\n }\n }\n const usage = {\n promptTokens: aiResult.usage.promptTokens || estimateTokens(JSON.stringify(modelMessages)),\n completionTokens: aiResult.usage.completionTokens || estimateTokens(aiResult.message),\n totalTokens:\n aiResult.usage.totalTokens ||\n estimateTokens(JSON.stringify(modelMessages)) + estimateTokens(aiResult.message),\n };\n recordUsage(user.userId, usage.totalTokens);\n\n const answeredAt = nowIso();\n statements.insertMessage.run({\n id: crypto.randomUUID(),\n conversationId,\n role: 'assistant',\n content: aiResult.message,\n promptTokens: usage.promptTokens,\n completionTokens: usage.completionTokens,\n totalTokens: usage.totalTokens,\n createdAt: answeredAt,\n });\n statements.updateConversation.run({\n id: conversationId,\n userId: user.userId,\n title: isNewConversation ? titleFromMessage(message) : conversation.title,\n updatedAt: answeredAt,\n });\n\n const latestUsage = usageFor(user.userId);\n const monthlyLimit = budget.limit;\n const action = determineClientAction(skillResult, message);\n jsonResponse(res, 200, {\n success: true,\n conversationId,\n provider,\n model: responseModel,\n message: aiResult.message,\n usage: {\n ...usage,\n monthlyLimit,\n remainingTokens: Math.max(0, monthlyLimit - latestUsage.used),\n },\n ...(action ? { clientAction: action } : {}),\n });\n }),\n);\n\napp.post('/api/ai/chat/stream', async (req, res) => {\n sseHeaders(res);\n\n try {\n const message = safeUserText(req.body?.message);\n if (!message) {\n writeSse(res, 'error', { message: 'message is required' });\n res.end();\n return;\n }\n\n const user = await resolveUser(req, req.body || {});\n const promptEstimate = estimateTokens(message);\n const estimated = promptEstimate + 600;\n const budget = enforceTokenBudget(user, estimated);\n const maxCompletionTokens = completionBudgetFor(budget, promptEstimate);\n const createdAt = nowIso();\n const conversationId = safeUserText(req.body?.conversationId) || crypto.randomUUID();\n const requestedModel = normalizeDeepSeekModelName(req.body?.model);\n const clientContext = clientContextMessage(req.body?.client);\n let conversation = statements.getConversation.get(conversationId, user.userId);\n const isNewConversation = !conversation;\n\n if (isNewConversation) {\n conversation = {\n id: conversationId,\n user_id: user.userId,\n title: titleFromMessage(message),\n };\n statements.insertConversation.run({\n id: conversationId,\n userId: user.userId,\n title: conversation.title,\n provider: 'deepseek',\n model: requestedModel,\n createdAt,\n updatedAt: createdAt,\n });\n }\n\n statements.insertMessage.run({\n id: crypto.randomUUID(),\n conversationId,\n role: 'user',\n content: message,\n promptTokens: 0,\n completionTokens: 0,\n totalTokens: 0,\n createdAt,\n });\n\n const codexRuntime = createCodexDeepSeekRuntime();\n const libreChatRuntime = createLibreChatAgentRuntime();\n const initialOpenClawRuntime = createOpenClawRuntime();\n let provider = initialOpenClawRuntime.enabled\n ? initialOpenClawRuntime.provider\n : libreChatRuntime.enabled\n ? libreChatRuntime.provider\n : codexRuntime.enabled\n ? codexRuntime.provider\n : 'deepseek';\n let responseModel = initialOpenClawRuntime.enabled\n ? initialOpenClawRuntime.model\n : libreChatRuntime.enabled\n ? libreChatRuntime.model\n : requestedModel;\n writeSse(res, 'meta', {\n conversationId,\n provider,\n model: responseModel,\n });\n const skillResult = await runResourceFinderSkill({\n message,\n user,\n onStep: (step) => writeSse(res, 'step', step),\n });\n const skillContext = resourceContextMessage(skillResult);\n const historyRows = statements.listMessages.all(conversationId).slice(-14);\n const modelMessages = [\n { role: 'system', content: systemPrompt },\n ...(clientContext ? [{ role: 'system', content: clientContext }] : []),\n ...(skillContext ? [{ role: 'system', content: skillContext }] : []),\n ...normalizeMessages(historyRows),\n ];\n\n let aiResult;\n const openClawRuntime = initialOpenClawRuntime;\n try {\n if (openClawRuntime.enabled) {\n aiResult = await callOpenClawAgent({\n messages: modelMessages,\n user,\n conversationId,\n mode: normalizeAgentMode(req.body?.mode),\n });\n provider = aiResult.provider || provider;\n responseModel = aiResult.model || responseModel;\n writeSse(res, 'delta', { text: aiResult.message });\n } else if (libreChatRuntime.enabled) {\n aiResult = await callLibreChatAgentStream(modelMessages, {\n onToken: (token) => writeSse(res, 'delta', { text: token }),\n });\n } else if (codexRuntime.enabled) {\n aiResult = await callCodexSdkDeepSeek(modelMessages, {\n onToken: (token) => writeSse(res, 'delta', { text: token }),\n onStep: (step) => writeSse(res, 'step', step),\n });\n } else {\n aiResult = await callDeepSeekStream(modelMessages, {\n model: requestedModel,\n maxCompletionTokens,\n onToken: (token) => writeSse(res, 'delta', { text: token }),\n });\n }\n } catch (error) {\n if (libreChatRuntime.enabled) {\n logger.warn({ error: String(error) }, 'LibreChat Agent streaming chat failed, falling back to Codex SDK or DeepSeek direct');\n provider = codexRuntime.enabled ? codexRuntime.provider : 'deepseek';\n responseModel = requestedModel;\n writeSse(res, 'meta', {\n conversationId,\n provider,\n model: responseModel,\n });\n if (codexRuntime.enabled) {\n try {\n aiResult = await callCodexSdkDeepSeek(modelMessages, {\n onToken: (token) => writeSse(res, 'delta', { text: token }),\n onStep: (step) => writeSse(res, 'step', step),\n });\n } catch (codexError) {\n if (isDeepSeekQuotaError(codexError)) throw codexError;\n logger.warn({ error: String(codexError) }, 'Codex SDK streaming chat failed, falling back to DeepSeek direct');\n provider = 'deepseek';\n responseModel = requestedModel;\n writeSse(res, 'meta', {\n conversationId,\n provider,\n model: responseModel,\n });\n aiResult = await callDeepSeekStream(modelMessages, {\n model: requestedModel,\n maxCompletionTokens,\n onToken: (token) => writeSse(res, 'delta', { text: token }),\n });\n }\n } else {\n aiResult = await callDeepSeekStream(modelMessages, {\n model: requestedModel,\n maxCompletionTokens,\n onToken: (token) => writeSse(res, 'delta', { text: token }),\n });\n }\n } else {\n if (!codexRuntime.enabled || isDeepSeekQuotaError(error)) throw error;\n logger.warn({ error: String(error) }, 'Codex SDK streaming chat failed, falling back to DeepSeek direct');\n provider = 'deepseek';\n responseModel = requestedModel;\n writeSse(res, 'meta', {\n conversationId,\n provider,\n model: responseModel,\n });\n aiResult = await callDeepSeekStream(modelMessages, {\n model: requestedModel,\n maxCompletionTokens,\n onToken: (token) => writeSse(res, 'delta', { text: token }),\n });\n }\n }\n\n const usage = {\n promptTokens: aiResult.usage.promptTokens || estimateTokens(JSON.stringify(modelMessages)),\n completionTokens: aiResult.usage.completionTokens || estimateTokens(aiResult.message),\n totalTokens:\n aiResult.usage.totalTokens ||\n estimateTokens(JSON.stringify(modelMessages)) + estimateTokens(aiResult.message),\n };\n recordUsage(user.userId, usage.totalTokens);\n\n const answeredAt = nowIso();\n statements.insertMessage.run({\n id: crypto.randomUUID(),\n conversationId,\n role: 'assistant',\n content: aiResult.message,\n promptTokens: usage.promptTokens,\n completionTokens: usage.completionTokens,\n totalTokens: usage.totalTokens,\n createdAt: answeredAt,\n });\n statements.updateConversation.run({\n id: conversationId,\n userId: user.userId,\n title: isNewConversation ? titleFromMessage(message) : conversation.title,\n updatedAt: answeredAt,\n });\n\n const latestUsage = usageFor(user.userId);\n const monthlyLimit = budget.limit;\n const action = determineClientAction(skillResult, message);\n writeSse(res, 'done', {\n conversationId,\n provider,\n model: responseModel,\n message: aiResult.message,\n usage: {\n ...usage,\n monthlyLimit,\n remainingTokens: Math.max(0, monthlyLimit - latestUsage.used),\n },\n ...(action ? { clientAction: action } : {}),\n });\n res.end();\n } catch (error) {\n logger.error({ error: error.stack || String(error) }, 'streaming chat failed');\n writeSse(res, 'error', {\n message: publicAiErrorMessage(error),\n details: error.details,\n });\n res.end();\n }\n});\n\napp.get(\n '/api/ai/conversations',\n asyncHandler(async (req, res) => {\n const user = await resolveUser(req, { username: req.query.username });\n const items = statements.listConversations.all(user.userId).map((item) => ({\n id: item.id,\n title: item.title,\n updatedAt: item.updatedAt,\n }));\n jsonResponse(res, 200, { success: true, items });\n }),\n);\n\napp.get(\n '/api/ai/conversations/:id',\n asyncHandler(async (req, res) => {\n const user = await resolveUser(req);\n const conversation = statements.getConversation.get(req.params.id, user.userId);\n if (!conversation) {\n return jsonResponse(res, 404, { success: false, message: 'conversation not found' });\n }\n const messages = statements.listMessages.all(req.params.id).map((item) => ({\n role: item.role,\n content: item.content,\n createdAt: item.createdAt,\n }));\n jsonResponse(res, 200, {\n success: true,\n conversation: {\n id: conversation.id,\n title: conversation.title,\n updatedAt: conversation.updated_at,\n },\n messages,\n });\n }),\n);\n\nfunction buildUrl(base, endpoint, params = {}) {\n const url = new URL(endpoint, `${base}/`);\n for (const [key, value] of Object.entries(params)) {\n if (Array.isArray(value)) {\n for (const item of value) {\n if (item !== undefined && item !== null && item !== '') {\n url.searchParams.append(key, String(item));\n }\n }\n } else if (value !== undefined && value !== null && value !== '') {\n url.searchParams.set(key, String(value));\n }\n }\n return url;\n}\n\nasync function fetchJson(url) {\n const response = await fetch(url, {\n headers: { Accept: 'application/json' },\n signal: AbortSignal.timeout(15_000),\n });\n if (!response.ok) {\n throw new Error(`Request failed: ${response.status}`);\n }\n return response.json();\n}\n\nfunction stripHtml(value) {\n return String(value || '')\n .replace(/<script[\\s\\S]*?<\\/script>/gi, ' ')\n .replace(/<style[\\s\\S]*?<\\/style>/gi, ' ')\n .replace(/<[^>]+>/g, ' ')\n .replace(/ /g, ' ')\n .replace(/</g, '<')\n .replace(/>/g, '>')\n .replace(/&/g, '&')\n .replace(/&#(\\d+);/g, (_match, code) => String.fromCharCode(Number(code)))\n .replace(/\\s+/g, ' ')\n .trim();\n}\n\nfunction cleanExtractedText(value) {\n let text = String(value || '')\n .replace(/\\b[A-Z]{1,3}\\d{2}n\\d{4,5}[a-z]?_p?\\d{4}[a-z]\\d{2}\\b/g, ' ')\n .replace(/\\uFFFD/g, '')\n .replace(/\\s+([,。;:!?、」』)》】])/g, '$1')\n .replace(/([「『《(【])\\s+/g, '$1');\n for (let i = 0; i < 3; i += 1) {\n text = text.replace(/([\\u3400-\\u9fff])\\s+([\\u3400-\\u9fff])/g, '$1$2');\n }\n return text.replace(/[ \\t]{2,}/g, ' ').trim();\n}\n\nfunction compactText(value, limit = 140) {\n const text = stripHtml(value);\n return text.length > limit ? `${text.slice(0, limit)}...` : text;\n}\n\nfunction collectSearchResults(data) {\n if (!data || typeof data !== 'object') return [];\n if (Array.isArray(data.results)) return data.results;\n if (Array.isArray(data?.results?.docs)) return data.results.docs;\n if (Array.isArray(data?.response?.docs)) return data.response.docs;\n if (Array.isArray(data.docs)) return data.docs;\n return [];\n}\n\nfunction firstText(item, keys) {\n for (const key of keys) {\n const value = item?.[key];\n if (Array.isArray(value)) {\n const first = value.map((entry) => safeUserText(String(entry))).find(Boolean);\n if (first) return first;\n }\n if (typeof value === 'string' && value.trim()) return value.trim();\n if (typeof value === 'number') return String(value);\n }\n return '';\n}\n\nfunction firstNumber(item, keys, fallback = undefined) {\n for (const key of keys) {\n const value = item?.[key];\n if (typeof value === 'number') return value;\n if (typeof value === 'string' && value.trim() && Number.isFinite(Number(value))) {\n return Number(value);\n }\n }\n return fallback;\n}\n\nasync function searchCbetaTitle(query, limit) {\n const url = buildUrl(cbetaApiRoot, 'search/title', { q: query, start: 0, rows: limit });\n const data = await fetchJson(url);\n return collectSearchResults(data).flatMap((item) => {\n const work = firstText(item, ['work', 'work_id', 'workId']);\n const title = firstText(item, ['content', 'title', 'work_title', 'workTitle']);\n if (!work || !title) return [];\n const juan = firstNumber(item, ['juan', 'juan_num', 'juanNum'], 1);\n return [\n {\n id: `cbeta-title-${work}-${juan}`,\n title,\n sourceName: 'CBETA 佛典',\n url: `cbeta:${work}:${juan}`,\n snippet: firstText(item, ['byline', 'creators', 'time_dynasty']) || '可下载经文正文并用于全球法布施。',\n resourceType: 'scripture',\n work,\n juan,\n },\n ];\n });\n}\n\nasync function searchCbetaContent(query, limit) {\n const endpoints = ['search/fulltext', 'search/content', 'search/all_in_one'];\n const settled = await Promise.allSettled(\n endpoints.map((endpoint) =>\n fetchJson(buildUrl(cbetaApiRoot, endpoint, { q: query, start: 0, rows: limit, field: 'content' })),\n ),\n );\n const results = [];\n for (const response of settled) {\n if (response.status !== 'fulfilled') continue;\n for (const item of collectSearchResults(response.value)) {\n const work = firstText(item, ['work', 'work_id', 'workId', 'sutra', 'id']);\n const snippet = compactText(firstText(item, ['kwic', 'highlight', 'snippet', 'content', 'text', 'body', 'p']));\n if (!work || !snippet) continue;\n const juan = firstNumber(item, ['juan', 'juan_num', 'juanNum', '卷'], 1);\n const title = firstText(item, ['title', 'work_title', 'workTitle', 'sutra_name', 'book', 'name']) || work;\n results.push({\n id: `cbeta-content-${work}-${juan}-${sha256(snippet).slice(0, 8)}`,\n title,\n sourceName: 'CBETA 佛典',\n url: `cbeta:${work}:${juan}`,\n snippet,\n resourceType: 'scripture',\n work,\n juan,\n });\n if (results.length >= limit) return results;\n }\n }\n return results;\n}\n\nfunction directUrlResource(query) {\n const url = safeUserText(query);\n let parsed;\n try {\n parsed = new URL(url);\n } catch {\n return null;\n }\n if (!['http:', 'https:'].includes(parsed.protocol)) return null;\n return {\n id: `url-${sha256(url).slice(0, 12)}`,\n title: parsed.hostname,\n sourceName: '网页资源',\n url,\n snippet: '直接下载并提取这个链接的可读正文。',\n resourceType: 'web',\n };\n}\n\nfunction looksLikeAudioQuery(query) {\n return /音乐|音樂|音频|音訊|mp3|audio|念诵|念誦|诵经|誦經|唱诵|唱誦|读诵|讀誦/.test(\n safeUserText(query).toLowerCase(),\n );\n}\n\nfunction archiveSearchTerm(query) {\n return (\n safeUserText(query)\n .replace(/下载|查找|寻找|找一?个|资源|音频|音訊|音乐|音樂|mp3|audio|念诵|念誦|诵经|誦經|唱诵|唱誦|读诵|讀誦/gi, ' ')\n .replace(/\\s+/g, ' ')\n .trim() || safeUserText(query)\n );\n}\n\nfunction solrPhrase(value) {\n return String(value).replace(/\\\\/g, '\\\\\\\\').replace(/\"/g, '\\\\\"');\n}\n\nasync function searchInternetArchiveAudio(query, limit) {\n if (!looksLikeAudioQuery(query)) return [];\n const term = archiveSearchTerm(query);\n if (!term) return [];\n const variants = Array.from(\n new Set([\n term,\n term.replaceAll('金刚经', '金剛經'),\n term.replaceAll('金剛經', '金刚经'),\n ].filter(Boolean)),\n );\n const phraseQuery = variants\n .flatMap((item) => [`title:\"${solrPhrase(item)}\"`, `description:\"${solrPhrase(item)}\"`])\n .join(' OR ');\n const url = buildUrl('https://archive.org', 'advancedsearch.php', {\n q: `(${phraseQuery}) AND mediatype:audio`,\n rows: Math.min(Math.max(limit, 1), 10),\n output: 'json',\n 'fl[]': ['identifier', 'title', 'description'],\n });\n const data = await fetchJson(url);\n const docs = Array.isArray(data?.response?.docs) ? data.response.docs : [];\n return docs\n .map((item) => {\n const identifier = firstText(item, ['identifier']);\n const title = firstText(item, ['title']) || identifier;\n if (!identifier || !title) return null;\n return {\n id: `ia-audio-${identifier}`,\n title,\n sourceName: 'Internet Archive 音频',\n url: `ia:${identifier}`,\n snippet: compactText(firstText(item, ['description'])) || '公开可访问的音频资源,将下载第一个可用 MP3 文件。',\n resourceType: 'audio',\n identifier,\n };\n })\n .filter(Boolean);\n}\n\nconst curatedCbetaWorks = [\n {\n work: 'T0235',\n juan: 1,\n title: '金剛般若波羅蜜經',\n keywords: ['金剛般若波羅蜜經', '金刚般若波罗蜜经', '金剛經', '金刚经'],\n snippet: '姚秦 鳩摩羅什譯。常用于读诵、共修与法布施的般若经典。',\n },\n {\n work: 'T0251',\n juan: 1,\n title: '般若波羅蜜多心經',\n keywords: ['般若波羅蜜多心經', '般若波罗蜜多心经', '心經', '心经'],\n snippet: '唐 玄奘譯。篇幅短,适合加入禅室功课本与日常持诵。',\n },\n {\n work: 'T0262',\n juan: 1,\n title: '妙法蓮華經',\n keywords: ['妙法蓮華經', '妙法莲华经', '法華經', '法华经'],\n snippet: '姚秦 鳩摩羅什譯。可按卷下载正文,用于长期功课与分享。',\n },\n {\n work: 'T0279',\n juan: 1,\n title: '大方廣佛華嚴經',\n keywords: ['大方廣佛華嚴經', '大方广佛华严经', '華嚴經', '华严经'],\n snippet: '唐 實叉難陀譯。可按卷下载正文,用于系统研读与法布施。',\n },\n {\n work: 'T0360',\n juan: 1,\n title: '佛說無量壽經',\n keywords: ['佛說無量壽經', '佛说无量寿经', '無量壽經', '无量寿经'],\n snippet: '净土经典。可下载正文,用于净土功课本与全球法布施。',\n },\n {\n work: 'T0365',\n juan: 1,\n title: '佛說觀無量壽佛經',\n keywords: ['佛說觀無量壽佛經', '佛说观无量寿佛经', '觀無量壽經', '观无量寿经'],\n snippet: '净土三经之一。适合查找下载后加入净土功课。',\n },\n {\n work: 'T0366',\n juan: 1,\n title: '佛說阿彌陀經',\n keywords: ['佛說阿彌陀經', '佛说阿弥陀经', '阿彌陀經', '阿弥陀经'],\n snippet: '姚秦 鳩摩羅什譯。篇幅适中,适合共修、持诵与分享。',\n },\n {\n work: 'T0475',\n juan: 1,\n title: '維摩詰所說經',\n keywords: ['維摩詰所說經', '维摩诘所说经', '維摩詰經', '维摩诘经'],\n snippet: '姚秦 鳩摩羅什譯。大乘经典,可按卷下载用于研读。',\n },\n {\n work: 'T0676',\n juan: 1,\n title: '解深密經',\n keywords: ['解深密經', '解深密经'],\n snippet: '唯识相关大乘经典。适合研读、整理笔记与法布施。',\n },\n {\n work: 'T1666',\n juan: 1,\n title: '大乘起信論',\n keywords: ['大乘起信論', '大乘起信论', '起信論', '起信论'],\n snippet: '大乘论典。适合下载后作为研习功课资料。',\n },\n];\n\nfunction localCbetaResults(query, limit) {\n const normalizedQuery = safeUserText(query).toLowerCase();\n if (!normalizedQuery) return [];\n return curatedCbetaWorks\n .filter((item) =>\n item.keywords.some((keyword) => {\n const normalizedKeyword = keyword.toLowerCase();\n return normalizedKeyword.includes(normalizedQuery) || normalizedQuery.includes(normalizedKeyword);\n }),\n )\n .slice(0, limit)\n .map((item) => ({\n id: `cbeta-curated-${item.work}-${item.juan}`,\n title: item.title,\n sourceName: 'CBETA 佛典',\n url: `cbeta:${item.work}:${item.juan}`,\n snippet: item.snippet,\n resourceType: 'scripture',\n work: item.work,\n juan: item.juan,\n }));\n}\n\nfunction uniqueResources(items, limit) {\n const seen = new Set();\n const results = [];\n for (const item of items) {\n const key = `${item.url}:${item.title}`;\n if (seen.has(key)) continue;\n seen.add(key);\n results.push(item);\n if (results.length >= limit) break;\n }\n return results;\n}\n\nasync function findResourceCandidates(query, limit) {\n const direct = directUrlResource(query);\n const results = [];\n if (direct) results.push(direct);\n try {\n results.push(...(await searchInternetArchiveAudio(query, limit)));\n } catch (error) {\n logger.warn({ error: String(error), query }, 'Internet Archive audio search skipped');\n }\n results.push(...localCbetaResults(query, limit));\n\n const [titleResults, contentResults] = await Promise.allSettled([\n searchCbetaTitle(query, limit),\n searchCbetaContent(query, limit),\n ]);\n if (titleResults.status === 'fulfilled') results.push(...titleResults.value);\n if (contentResults.status === 'fulfilled') results.push(...contentResults.value);\n\n return uniqueResources(results, limit);\n}\n\nfunction looksLikeResourceTask(message) {\n const text = safeUserText(message).toLowerCase();\n if (!text) return false;\n return /下载|查找|寻找|找一?个|资源|經文|经文|佛经|佛經|仪轨|儀軌|音乐|音樂|mp3|audio|念诵|念誦|诵经|誦經/.test(text);\n}\n\nfunction extractResourceQuery(message) {\n const text = safeUserText(message)\n .replace(/请|請|帮我|幫我|麻烦|麻煩|可以|能不能|需要|我要|想要/g, ' ')\n .replace(/下载|下載|查找|寻找|尋找|找一个|找一個|找|资源|資源|并|並|然后|然後|用于|用於|全球法布施|加入功课本|加入功課本/g, ' ')\n .replace(/\\s+/g, ' ')\n .trim();\n return text || safeUserText(message);\n}\n\nfunction resourceContextMessage(skillResult) {\n if (!skillResult) return '';\n const lines = [\n '本轮资源检索/下载已经由大乘 App 后端完成。',\n '不要再次调用 search_dharma_resources、download_dharma_resource 或其他资源 MCP 工具;请直接基于以下结果回答用户。',\n `后端已调用 skill: ${skillResult.skillName}`,\n `执行目标: ${skillResult.query}`,\n ];\n if (skillResult.selected) {\n lines.push(`候选资源: ${skillResult.selected.title}`);\n lines.push(`来源: ${skillResult.selected.sourceName}`);\n lines.push(`资源地址: ${skillResult.selected.url}`);\n }\n if (skillResult.downloaded) {\n lines.push(`下载标题: ${skillResult.downloaded.title}`);\n lines.push(`下载文件: ${skillResult.downloaded.fileName}`);\n const text = safeUserText(skillResult.downloaded.contentText);\n if (text) {\n lines.push('下载正文摘录:');\n lines.push(text.slice(0, 8000));\n }\n }\n return lines.join('\\n');\n}\n\nfunction determineClientAction(skillResult, message) {\n if (skillResult && skillResult.downloaded) {\n const text = safeUserText(message);\n if (/法布施|全球法布施|分享/.test(text)) {\n return {\n type: 'prepare_dharma_share_text',\n title: skillResult.downloaded.title,\n text: skillResult.downloaded.contentText || '',\n };\n }\n }\n return null;\n}\n\nasync function runResourceFinderSkill({ message, user, onStep }) {\n if (!looksLikeResourceTask(message)) return null;\n\n const skillName = 'resource-finder-downloader';\n const query = extractResourceQuery(message);\n const audioTask = looksLikeAudioQuery(message) || looksLikeAudioQuery(query);\n onStep?.({\n type: 'skill.step',\n skillName,\n stage: 'start',\n title: '调用资源查找下载 skill',\n message: `识别到资源任务: ${query}`,\n });\n\n onStep?.({\n type: 'skill.step',\n skillName,\n stage: 'search',\n title: '搜索可分享资源',\n message: audioTask\n ? '优先检索公开可访问音频资源,失败时再回落到佛典正文资源。'\n : '优先检索 CBETA 佛典正文接口,搜索索引不可用时使用内置经典映射。',\n });\n const candidates = await findResourceCandidates(query, 8);\n if (candidates.length === 0) {\n onStep?.({\n type: 'skill.step',\n skillName,\n stage: 'empty',\n title: '没有找到可下载资源',\n message: '后端没有找到明确可下载且可分享的资源。',\n });\n return {\n skillName,\n query,\n candidates: [],\n selected: null,\n downloaded: null,\n };\n }\n\n const selected = candidates[0];\n onStep?.({\n type: 'skill.step',\n skillName,\n stage: 'select',\n title: '选择可信候选资源',\n message: `${selected.title} / ${selected.sourceName}`,\n });\n\n onStep?.({\n type: 'skill.step',\n skillName,\n stage: 'download',\n title: '下载并提取资源',\n message: selected.url,\n });\n const cbeta = parseCbetaResource(selected);\n const internetArchive = parseInternetArchiveResource(selected);\n const downloaded = cbeta\n ? await downloadCbetaResource({ ...selected, ...cbeta })\n : internetArchive\n ? await downloadInternetArchiveAudio({ ...selected, ...internetArchive })\n : await downloadWebResource(selected);\n const persisted = persistDownloadedResource(user, downloaded);\n\n onStep?.({\n type: 'skill.step',\n skillName,\n stage: 'done',\n title: '资源已准备完成',\n message: downloaded.fileName,\n });\n\n return {\n skillName,\n query,\n candidates,\n selected,\n downloaded: {\n ...downloaded,\n downloadId: persisted.id,\n },\n };\n}\n\napp.post(\n '/api/resources/search',\n asyncHandler(async (req, res) => {\n const query = safeUserText(req.body?.query);\n const limit = Math.min(Math.max(Number(req.body?.limit || 12), 1), 20);\n if (query.length < 2) {\n return jsonResponse(res, 400, { success: false, message: 'query is required' });\n }\n\n const items = await findResourceCandidates(query, limit);\n\n jsonResponse(res, 200, {\n success: true,\n source: 'resource-index',\n items,\n });\n }),\n);\n\nfunction parseCbetaResource(input) {\n const work = safeUserText(input.work);\n const juan = Number(input.juan || 1);\n if (work) return { work, juan: Number.isFinite(juan) ? juan : 1 };\n\n const url = safeUserText(input.url);\n const match = /^cbeta:([^:]+):(\\d+)$/i.exec(url);\n if (!match) return null;\n return {\n work: match[1].toUpperCase(),\n juan: Number(match[2] || 1),\n };\n}\n\nfunction parseInternetArchiveResource(input) {\n const url = safeUserText(input.url);\n const match = /^ia:([^:]+)$/i.exec(url);\n const identifier = match?.[1] || safeUserText(input.identifier);\n if (!identifier) return null;\n return { identifier };\n}\n\nfunction safeFileName(value) {\n const normalized = String(value || 'dharma-resource')\n .replace(/[\\\\/:*?\"<>|]+/g, '-')\n .replace(/\\s+/g, '-')\n .replace(/-+/g, '-')\n .replace(/^-|-$/g, '')\n .slice(0, 80);\n return normalized || 'dharma-resource';\n}\n\nfunction extractCbetaHtml(data) {\n const first = Array.isArray(data?.results) ? data.results[0] : null;\n if (typeof first === 'string') return first;\n if (first && typeof first === 'object' && typeof first.html === 'string') return first.html;\n if (typeof data?.html === 'string') return data.html;\n return '';\n}\n\nasync function downloadCbetaResource(resource) {\n const url = buildUrl(cbetaApiRoot, 'juans', {\n work: resource.work,\n juan: resource.juan,\n work_info: 1,\n toc: 1,\n });\n const data = await fetchJson(url);\n const workInfo = data.work_info || {};\n const title = safeUserText(workInfo.title) || safeUserText(resource.title) || resource.work;\n const html = extractCbetaHtml(data);\n const text = cleanExtractedText(stripHtml(html));\n if (!text) throw new Error('CBETA resource returned empty content');\n return {\n title,\n sourceName: 'CBETA 佛典',\n url: `cbeta:${resource.work}:${resource.juan}`,\n contentText: text.slice(0, maxResourceTextChars),\n fileName: `${resource.work}_${resource.juan}_${safeFileName(title)}.txt`,\n };\n}\n\nasync function downloadInternetArchiveAudio(resource) {\n const identifier = safeUserText(resource.identifier);\n if (!identifier) throw new Error('Internet Archive identifier is required');\n const metadata = await fetchJson(`https://archive.org/metadata/${encodeURIComponent(identifier)}`);\n const title = safeUserText(metadata?.metadata?.title) || safeUserText(resource.title) || identifier;\n const files = Array.isArray(metadata?.files) ? metadata.files : [];\n const audioFile = files.find((file) => {\n const name = safeUserText(file?.name);\n const format = safeUserText(file?.format).toLowerCase();\n return /\\.(mp3|m4a|ogg)$/i.test(name) || format.includes('mp3');\n });\n if (!audioFile?.name) {\n throw new Error('Internet Archive audio item has no downloadable audio file');\n }\n const sourceFileName = audioFile.name;\n const fileUrl = `https://archive.org/download/${encodeURIComponent(identifier)}/${sourceFileName\n .split('/')\n .map((part) => encodeURIComponent(part))\n .join('/')}`;\n const response = await fetch(fileUrl, {\n headers: {\n Accept: 'audio/mpeg, audio/*;q=0.9, */*;q=0.5',\n 'User-Agent': 'DachengResourceDownloader/1.0 (+https://ombhrum.com)',\n },\n signal: AbortSignal.timeout(60_000),\n });\n if (!response.ok) {\n throw new Error(`Internet Archive audio download failed: ${response.status}`);\n }\n const binaryContent = Buffer.from(await response.arrayBuffer());\n if (!binaryContent.length) throw new Error('Internet Archive audio returned empty content');\n const sizeMb = (binaryContent.length / 1024 / 1024).toFixed(1);\n return {\n title,\n sourceName: 'Internet Archive 音频',\n url: fileUrl,\n contentText: `已下载音频资源: ${title}\\n文件名: ${sourceFileName}\\n大小: ${sizeMb} MB\\n来源: ${fileUrl}`,\n binaryContent,\n fileName: safeFileName(sourceFileName),\n };\n}\n\nasync function downloadWebResource(input) {\n const url = safeUserText(input.url);\n const parsed = new URL(url);\n if (!['http:', 'https:'].includes(parsed.protocol)) {\n throw new Error('Only http/https resources can be downloaded');\n }\n\n const response = await fetch(parsed, {\n headers: {\n Accept: 'text/html, text/plain, application/xhtml+xml, application/xml;q=0.8, */*;q=0.5',\n 'User-Agent': 'DachengResourceDownloader/1.0 (+https://ombhrum.com)',\n },\n signal: AbortSignal.timeout(25_000),\n });\n if (!response.ok) throw new Error(`Resource download failed: ${response.status}`);\n const contentType = response.headers.get('content-type') || '';\n const body = await response.text();\n const title = safeUserText(input.title) || parsed.hostname;\n const contentText = contentType.includes('html') ? cleanExtractedText(stripHtml(body)) : body.trim();\n if (!contentText) throw new Error('Resource returned empty content');\n return {\n title,\n sourceName: safeUserText(input.sourceName) || parsed.hostname,\n url,\n contentText: contentText.slice(0, maxResourceTextChars),\n fileName: `${safeFileName(title)}.txt`,\n };\n}\n\nfunction persistDownloadedResource(user, content) {\n const id = crypto.randomUUID();\n const fileName = `${id}-${safeFileName(content.fileName)}`;\n const filePath = path.join(resourcesDir, fileName);\n if (content.binaryContent) {\n fs.writeFileSync(filePath, content.binaryContent);\n } else {\n fs.writeFileSync(filePath, content.contentText, 'utf8');\n }\n statements.insertResourceDownload.run({\n id,\n userId: user.userId,\n title: content.title,\n sourceName: content.sourceName,\n sourceUrl: content.url,\n fileName: content.fileName,\n filePath,\n createdAt: nowIso(),\n });\n return { id, fileName, filePath };\n}\n\nfunction createLibreChatMcpServer() {\n const server = new McpServer({\n name: 'dacheng-ai-tools',\n version: '0.1.0',\n });\n\n server.registerTool(\n 'search_dharma_resources',\n {\n title: 'Search dharma resources',\n description:\n 'Search for legally shareable Buddhist scripture, text, web, or audio resources that can be prepared for Fabushi sharing.',\n inputSchema: {\n query: z.string().min(2).max(200),\n limit: z.number().int().min(1).max(12).optional(),\n },\n outputSchema: {\n query: z.string(),\n items: z.array(\n z.object({\n id: z.string(),\n title: z.string(),\n sourceName: z.string(),\n url: z.string(),\n snippet: z.string(),\n resourceType: z.string(),\n work: z.string().optional(),\n juan: z.number().optional(),\n }),\n ),\n },\n annotations: {\n readOnlyHint: true,\n destructiveHint: false,\n openWorldHint: true,\n },\n },\n async ({ query, limit }) => {\n const items = await findResourceCandidates(query, limit ?? 8);\n return textToolResult(`Found ${items.length} candidate resources for \"${query}\".`, {\n query,\n items,\n });\n },\n );\n\n server.registerTool(\n 'download_dharma_resource',\n {\n title: 'Download dharma resource',\n description:\n 'Download and extract a selected resource. Use the url/work/juan returned by search_dharma_resources whenever possible.',\n inputSchema: {\n url: z.string().min(1).max(1000),\n title: z.string().max(300).optional(),\n sourceName: z.string().max(120).optional(),\n work: z.string().max(40).optional(),\n juan: z.number().int().min(1).optional(),\n identifier: z.string().max(300).optional(),\n },\n outputSchema: {\n title: z.string(),\n sourceName: z.string(),\n url: z.string(),\n fileName: z.string(),\n downloadId: z.string(),\n contentText: z.string(),\n },\n annotations: {\n readOnlyHint: false,\n destructiveHint: false,\n openWorldHint: true,\n },\n },\n async (input) => {\n const user = { userId: 'librechat:mcp' };\n const cbeta = parseCbetaResource(input);\n const internetArchive = parseInternetArchiveResource(input);\n const content = cbeta\n ? await downloadCbetaResource({ ...input, ...cbeta })\n : internetArchive\n ? await downloadInternetArchiveAudio({ ...input, ...internetArchive })\n : await downloadWebResource(input);\n const persisted = persistDownloadedResource(user, content);\n const { binaryContent: _binaryContent, ...publicContent } = content;\n return textToolResult(`Downloaded ${publicContent.title} as ${publicContent.fileName}.`, {\n ...publicContent,\n downloadId: persisted.id,\n });\n },\n );\n\n server.registerTool(\n 'prepare_dharma_share_text',\n {\n title: 'Prepare dharma sharing text',\n description:\n 'Prepare a text payload for the Fabushi app to confirm and add to global dharma sharing. This returns a clientAction; it does not start sending.',\n inputSchema: {\n title: z.string().min(1).max(120),\n text: z.string().min(1).max(20000),\n },\n outputSchema: {\n clientAction: z.object({\n type: z.literal('prepare_dharma_share_text'),\n title: z.string(),\n text: z.string(),\n }),\n },\n annotations: {\n readOnlyHint: false,\n destructiveHint: false,\n openWorldHint: false,\n },\n },\n async ({ title, text }) =>\n textToolResult(`Prepared \"${title}\" for user confirmation in the Fabushi app.`, {\n clientAction: {\n type: 'prepare_dharma_share_text',\n title: safeUserText(title).slice(0, 120),\n text: String(text || '').trim().slice(0, 20000),\n },\n }),\n );\n\n server.registerTool(\n 'prepare_practice_book_item',\n {\n title: 'Prepare practice book item',\n description:\n 'Prepare a scripture, chant, or practice item for the Fabushi app practice book. This returns a clientAction; it does not mutate the app by itself.',\n inputSchema: {\n title: z.string().min(1).max(120),\n sourceName: z.string().max(120).optional(),\n text: z.string().min(1).max(20000),\n },\n outputSchema: {\n clientAction: z.object({\n type: z.literal('prepare_practice_book_item'),\n title: z.string(),\n sourceName: z.string(),\n text: z.string(),\n }),\n },\n annotations: {\n readOnlyHint: false,\n destructiveHint: false,\n openWorldHint: false,\n },\n },\n async ({ title, sourceName, text }) =>\n textToolResult(`Prepared \"${title}\" for the Fabushi practice book confirmation flow.`, {\n clientAction: {\n type: 'prepare_practice_book_item',\n title: safeUserText(title).slice(0, 120),\n sourceName: safeUserText(sourceName || '大乘 AI').slice(0, 120),\n text: String(text || '').trim().slice(0, 20000),\n },\n }),\n );\n\n return server;\n}\n\nasync function handleMcpRequest(req, res) {\n const server = createLibreChatMcpServer();\n const transport = new StreamableHTTPServerTransport({\n sessionIdGenerator: undefined,\n enableJsonResponse: true,\n });\n\n res.on('close', () => {\n transport.close();\n server.close();\n });\n\n await server.connect(transport);\n await transport.handleRequest(req, res, req.body);\n}\n\napp.all('/mcp', async (req, res) => {\n try {\n await handleMcpRequest(req, res);\n } catch (error) {\n logger.error({ error: error.stack || String(error) }, 'MCP request failed');\n if (!res.headersSent) {\n res.status(500).json({\n error: 'MCP request failed',\n message: publicAiErrorMessage(error),\n });\n }\n }\n});\n\napp.post(\n '/api/resources/download',\n asyncHandler(async (req, res) => {\n const user = await resolveUser(req, req.body || {});\n const cbeta = parseCbetaResource(req.body || {});\n const internetArchive = parseInternetArchiveResource(req.body || {});\n const content = cbeta\n ? await downloadCbetaResource({ ...req.body, ...cbeta })\n : internetArchive\n ? await downloadInternetArchiveAudio({ ...req.body, ...internetArchive })\n : await downloadWebResource(req.body || {});\n const persisted = persistDownloadedResource(user, content);\n const { binaryContent: _binaryContent, ...publicContent } = content;\n jsonResponse(res, 200, {\n success: true,\n downloadId: persisted.id,\n ...publicContent,\n });\n }),\n);\n\napp.post(\n '/api/codex/resource-task',\n asyncHandler(async (req, res) => {\n if (process.env.ENABLE_CODEX_SDK !== 'true') {\n return jsonResponse(res, 403, {\n success: false,\n message: 'Codex SDK task runner is disabled. Set ENABLE_CODEX_SDK=true on the server to enable it.',\n });\n }\n const prompt = safeUserText(req.body?.prompt);\n if (!prompt) {\n return jsonResponse(res, 400, { success: false, message: 'prompt is required' });\n }\n const { Codex } = await import('@openai/codex-sdk');\n const codexRuntime = createCodexDeepSeekRuntime();\n const codex = new Codex(codexRuntime.enabled ? codexRuntime.options : {});\n const thread = codex.startThread(\n codexRuntime.enabled\n ? codexRuntime.threadOptions\n : { skipGitRepoCheck: true, networkAccessEnabled: true },\n );\n const result = await thread.run(prompt);\n jsonResponse(res, 200, {\n success: true,\n threadId: thread.id,\n provider: codexRuntime.provider,\n model: deepseekModel,\n result,\n });\n }),\n);\n\napp.use((error, _req, res, _next) => {\n const status = Number(error.statusCode || error.status || 500);\n const safeStatus = status >= 400 && status < 600 ? status : 500;\n logger.error({ error: error.stack || String(error) }, 'request failed');\n jsonResponse(res, safeStatus, {\n success: false,\n message: error.message || 'Internal Server Error',\n details: error.details,\n });\n});\n\nconst port = Number(process.env.PORT || 8788);\napp.listen(port, '0.0.0.0', () => {\n logger.info(\n {\n port,\n dbPath,\n dataDir,\n deepseekModel,\n cbetaApiRoot,\n },\n 'Dacheng AI backend started',\n );\n});\n"}, "35d781fc2a76059ef89f3d205fe659af08756ac7": {"t": 1785757976.4322593, "n": 140492, "h": "efb9cb810e3d4f5c", "c": "import crypto from 'node:crypto';\nimport fs from 'node:fs';\nimport path from 'node:path';\nimport process from 'node:process';\nimport { fileURLToPath } from 'node:url';\n\nimport { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';\nimport { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';\nimport Database from 'better-sqlite3';\nimport cors from 'cors';\nimport dotenv from 'dotenv';\nimport express from 'express';\nimport rateLimit from 'express-rate-limit';\nimport helmet from 'helmet';\nimport pino from 'pino';\nimport { z } from 'zod';\n\ndotenv.config();\n\nconst __dirname = path.dirname(fileURLToPath(import.meta.url));\nconst rootDir = path.resolve(__dirname, '..');\nconst dataDir = process.env.DATA_DIR || path.join(rootDir, 'data');\nconst resourcesDir = path.join(dataDir, 'resources');\nconst miniAppsDir = path.join(dataDir, 'miniapps');\nconst miniAppsStorePath = path.join(miniAppsDir, 'sandbox-miniapps.json');\nconst openClawRuntimeUpdatesDir =\n process.env.OPENCLAW_RUNTIME_UPDATES_DIR || path.join(dataDir, 'openclaw-runtime');\nconst openClawRuntimeEnableMacosUpdates =\n process.env.OPENCLAW_RUNTIME_ENABLE_MACOS_UPDATES === 'true';\nconst codexHomeDir = process.env.CODEX_HOME || path.join(dataDir, 'codex-home');\nconst codexTempDir = process.env.CODEX_TMPDIR || path.join(dataDir, 'codex-tmp');\nconst codexRuntimeDir = process.env.XDG_RUNTIME_DIR || path.join(dataDir, 'codex-runtime');\nconst dbPath = process.env.SQLITE_PATH || path.join(dataDir, 'dacheng-ai.sqlite');\n\nfs.mkdirSync(resourcesDir, { recursive: true });\nfs.mkdirSync(miniAppsDir, { recursive: true });\nfs.mkdirSync(openClawRuntimeUpdatesDir, { recursive: true });\nfs.mkdirSync(codexHomeDir, { recursive: true });\nfs.mkdirSync(codexTempDir, { recursive: true });\nfs.mkdirSync(codexRuntimeDir, { recursive: true });\n\nconst logger = pino({\n level: process.env.LOG_LEVEL || 'info',\n});\n\nconst app = express();\napp.disable('x-powered-by');\napp.set('trust proxy', Number(process.env.TRUST_PROXY_HOPS || 1));\napp.use(helmet());\nconst corsOrigin =\n !process.env.CORS_ORIGIN || process.env.CORS_ORIGIN.trim() === '*'\n ? true\n : process.env.CORS_ORIGIN.split(',').map((item) => item.trim()).filter(Boolean);\napp.use(cors({ origin: corsOrigin }));\napp.use(express.json({ limit: '1mb' }));\napp.use(\n rateLimit({\n windowMs: 60_000,\n limit: Number(process.env.RATE_LIMIT_PER_MINUTE || 90),\n standardHeaders: true,\n legacyHeaders: false,\n }),\n);\n\nconst db = new Database(dbPath);\ndb.pragma('journal_mode = WAL');\ndb.pragma('foreign_keys = ON');\n\ndb.exec(`\n CREATE TABLE IF NOT EXISTS conversations (\n id TEXT PRIMARY KEY,\n user_id TEXT NOT NULL,\n title TEXT NOT NULL,\n provider TEXT NOT NULL,\n model TEXT NOT NULL,\n created_at TEXT NOT NULL,\n updated_at TEXT NOT NULL\n );\n\n CREATE INDEX IF NOT EXISTS idx_conversations_user_updated\n ON conversations (user_id, updated_at DESC);\n\n CREATE TABLE IF NOT EXISTS messages (\n id TEXT PRIMARY KEY,\n conversation_id TEXT NOT NULL,\n role TEXT NOT NULL,\n content TEXT NOT NULL,\n prompt_tokens INTEGER NOT NULL DEFAULT 0,\n completion_tokens INTEGER NOT NULL DEFAULT 0,\n total_tokens INTEGER NOT NULL DEFAULT 0,\n created_at TEXT NOT NULL,\n FOREIGN KEY (conversation_id) REFERENCES conversations(id) ON DELETE CASCADE\n );\n\n CREATE INDEX IF NOT EXISTS idx_messages_conversation_created\n ON messages (conversation_id, created_at ASC);\n\n CREATE TABLE IF NOT EXISTS usage_months (\n user_id TEXT NOT NULL,\n month TEXT NOT NULL,\n total_tokens INTEGER NOT NULL DEFAULT 0,\n updated_at TEXT NOT NULL,\n PRIMARY KEY (user_id, month)\n );\n\n CREATE TABLE IF NOT EXISTS resource_downloads (\n id TEXT PRIMARY KEY,\n user_id TEXT NOT NULL,\n title TEXT NOT NULL,\n source_name TEXT NOT NULL,\n source_url TEXT NOT NULL,\n file_name TEXT NOT NULL,\n file_path TEXT NOT NULL,\n created_at TEXT NOT NULL\n );\n\n CREATE TABLE IF NOT EXISTS agent_runs (\n id TEXT PRIMARY KEY,\n user_id TEXT NOT NULL,\n conversation_id TEXT NOT NULL,\n message_id TEXT NOT NULL,\n openclaw_run_id TEXT,\n openclaw_session_key TEXT NOT NULL,\n status TEXT NOT NULL,\n mode TEXT NOT NULL,\n provider TEXT NOT NULL,\n model TEXT,\n input_tokens INTEGER NOT NULL DEFAULT 0,\n output_tokens INTEGER NOT NULL DEFAULT 0,\n tool_call_count INTEGER NOT NULL DEFAULT 0,\n cost_microusd INTEGER NOT NULL DEFAULT 0,\n started_at TEXT NOT NULL,\n completed_at TEXT,\n failed_at TEXT,\n error_code TEXT,\n error_message TEXT\n );\n\n CREATE INDEX IF NOT EXISTS idx_agent_runs_user_started\n ON agent_runs (user_id, started_at DESC);\n\n CREATE INDEX IF NOT EXISTS idx_agent_runs_status_started\n ON agent_runs (status, started_at DESC);\n\n CREATE TABLE IF NOT EXISTS agent_message_feedback (\n id TEXT PRIMARY KEY,\n message_id TEXT NOT NULL,\n user_id TEXT NOT NULL,\n rating TEXT NOT NULL,\n reason TEXT,\n comment TEXT,\n created_at TEXT NOT NULL\n );\n`);\n\nconst statements = {\n insertConversation: db.prepare(`\n INSERT INTO conversations (id, user_id, title, provider, model, created_at, updated_at)\n VALUES (@id, @userId, @title, @provider, @model, @createdAt, @updatedAt)\n `),\n updateConversation: db.prepare(`\n UPDATE conversations\n SET title = @title, updated_at = @updatedAt\n WHERE id = @id AND user_id = @userId\n `),\n getConversation: db.prepare(`\n SELECT * FROM conversations WHERE id = ? AND user_id = ? LIMIT 1\n `),\n listConversations: db.prepare(`\n SELECT id, title, updated_at AS updatedAt\n FROM conversations\n WHERE user_id = ?\n ORDER BY updated_at DESC\n LIMIT 30\n `),\n insertMessage: db.prepare(`\n INSERT INTO messages (\n id, conversation_id, role, content,\n prompt_tokens, completion_tokens, total_tokens, created_at\n )\n VALUES (\n @id, @conversationId, @role, @content,\n @promptTokens, @completionTokens, @totalTokens, @createdAt\n )\n `),\n listMessages: db.prepare(`\n SELECT role, content, prompt_tokens AS promptTokens,\n completion_tokens AS completionTokens, total_tokens AS totalTokens,\n created_at AS createdAt\n FROM messages\n WHERE conversation_id = ?\n ORDER BY created_at ASC\n `),\n getUsage: db.prepare(`\n SELECT total_tokens AS totalTokens FROM usage_months WHERE user_id = ? AND month = ?\n `),\n upsertUsage: db.prepare(`\n INSERT INTO usage_months (user_id, month, total_tokens, updated_at)\n VALUES (@userId, @month, @totalTokens, @updatedAt)\n ON CONFLICT(user_id, month)\n DO UPDATE SET total_tokens = usage_months.total_tokens + excluded.total_tokens,\n updated_at = excluded.updated_at\n `),\n insertResourceDownload: db.prepare(`\n INSERT INTO resource_downloads (\n id, user_id, title, source_name, source_url, file_name, file_path, created_at\n )\n VALUES (\n @id, @userId, @title, @sourceName, @sourceUrl, @fileName, @filePath, @createdAt\n )\n `),\n insertAgentRun: db.prepare(`\n INSERT INTO agent_runs (\n id, user_id, conversation_id, message_id, openclaw_run_id, openclaw_session_key,\n status, mode, provider, model, started_at\n )\n VALUES (\n @id, @userId, @conversationId, @messageId, @openClawRunId, @openClawSessionKey,\n @status, @mode, @provider, @model, @startedAt\n )\n `),\n getAgentRun: db.prepare(`\n SELECT * FROM agent_runs WHERE id = ? AND user_id = ? LIMIT 1\n `),\n updateAgentRunStatus: db.prepare(`\n UPDATE agent_runs\n SET status = @status,\n openclaw_run_id = COALESCE(@openClawRunId, openclaw_run_id),\n input_tokens = COALESCE(@inputTokens, input_tokens),\n output_tokens = COALESCE(@outputTokens, output_tokens),\n tool_call_count = COALESCE(@toolCallCount, tool_call_count),\n completed_at = @completedAt,\n failed_at = @failedAt,\n error_code = @errorCode,\n error_message = @errorMessage\n WHERE id = @id AND user_id = @userId\n `),\n insertAgentFeedback: db.prepare(`\n INSERT INTO agent_message_feedback (id, message_id, user_id, rating, reason, comment, created_at)\n VALUES (@id, @messageId, @userId, @rating, @reason, @comment, @createdAt)\n `),\n};\n\nconst deepseekApiKey = process.env.DEEPSEEK_API_KEY || '';\nconst deepseekBaseUrl = (process.env.DEEPSEEK_BASE_URL || 'https://api.deepseek.com').replace(/\\/+$/, '');\nconst deepseekModel = process.env.DEEPSEEK_MODEL || 'deepseek-chat';\nconst deepseekAvailableModels = Array.from(\n new Set(\n (process.env.DEEPSEEK_MODELS || `${deepseekModel},deepseek-chat,deepseek-reasoner`)\n .split(',')\n .map((item) => normalizeDeepSeekModelName(item))\n .filter(Boolean),\n ),\n);\nconst cbetaApiRoot = (process.env.CBETA_API_ROOT || 'https://api.ombhrum.com/api/cbeta').replace(/\\/+$/, '');\nconst fabushiApiBaseUrl = (process.env.FABUSHI_API_BASE_URL || 'https://api.ombhrum.com').replace(/\\/+$/, '');\nconst memberMonthlyLimit = Number(process.env.MEMBER_MONTHLY_TOKEN_LIMIT || 1_000_000);\nconst freeMonthlyLimit = Number(process.env.FREE_MONTHLY_TOKEN_LIMIT || 1_000);\nconst requireMemberForAi = process.env.REQUIRE_MEMBER_FOR_AI === 'true';\nconst deepseekMaxCompletionTokens = Math.max(1, Number(process.env.DEEPSEEK_MAX_COMPLETION_TOKENS || 1400));\nconst maxResourceTextChars = Number(process.env.MAX_RESOURCE_TEXT_CHARS || 80_000);\nconst openClawRuntimeManifestPath =\n process.env.OPENCLAW_RUNTIME_MANIFEST_PATH || path.join(openClawRuntimeUpdatesDir, 'manifest.json');\nconst openClawRuntimePublicBaseUrl = (process.env.OPENCLAW_RUNTIME_PUBLIC_BASE_URL || '').replace(/\\/+$/, '');\nconst enableLibreChatAgentChat = process.env.ENABLE_LIBRECHAT_AGENT_CHAT === 'true';\nconst libreChatAgentsBaseUrl = (\n process.env.LIBRECHAT_AGENTS_BASE_URL || 'http://127.0.0.1:3080/api/agents/v1'\n).replace(/\\/+$/, '');\nconst libreChatAgentApiKey = process.env.LIBRECHAT_AGENT_API_KEY || '';\nconst libreChatAgentId = process.env.LIBRECHAT_AGENT_ID || '';\nconst enableCodexSdkChat = process.env.ENABLE_CODEX_SDK_CHAT === 'true';\nconst enableOpenClawAgentChat = process.env.ENABLE_OPENCLAW_AGENT_CHAT === 'true';\nconst openClawGatewayUrl = (process.env.OPENCLAW_GATEWAY_URL || 'http://127.0.0.1:18789').replace(/\\/+$/, '');\nconst openClawGatewayToken = process.env.OPENCLAW_GATEWAY_TOKEN || '';\nconst openClawAgentId = process.env.OPENCLAW_AGENT_ID || 'fabushi-public-agent';\nconst openClawRunsEndpoint = process.env.OPENCLAW_RUNS_ENDPOINT || '';\nconst codexDeepSeekProviderId = 'deepseek-chat-completions';\nconst codexResponsesBaseUrl = (\n process.env.CODEX_DEEPSEEK_RESPONSES_BASE_URL ||\n `http://127.0.0.1:${process.env.PORT || 8788}/codex-deepseek/v1`\n).replace(/\\/+$/, '');\n\nconst systemPrompt = [\n '你是“大乘”App 的 AI 助手。',\n '你的核心任务是帮助用户查找、整理、理解并全球法布施合法可分享的佛法资源。',\n '回答要庄重、简洁、可执行;涉及经典、仪轨或资源时,提醒用户尊重版权、来源和当地法规。',\n '如果用户想找资源、下载经文、寻找音频或准备可分享资料,你必须先在后端自动执行搜索、验证和下载步骤;不要要求用户去前端 + 菜单手动查找。',\n '你可以把执行进度总结给用户,但不要输出隐藏推理原文。',\n].join('\\n');\n\nfunction nowIso() {\n return new Date().toISOString();\n}\n\nfunction monthKey(date = new Date()) {\n return `${date.getUTCFullYear()}-${String(date.getUTCMonth() + 1).padStart(2, '0')}`;\n}\n\nfunction jsonResponse(res, status, payload) {\n res.status(status).json(payload);\n}\n\nasync function readJsonFile(filePath) {\n const raw = await fs.promises.readFile(filePath, 'utf8');\n return JSON.parse(raw);\n}\n\nfunction requestOrigin(req) {\n if (openClawRuntimePublicBaseUrl) return openClawRuntimePublicBaseUrl;\n const proto = req.get('X-Forwarded-Proto') || req.protocol || 'https';\n const host = req.get('X-Forwarded-Host') || req.get('Host');\n return host ? `${proto}://${host}` : '';\n}\n\nfunction absolutizeOpenClawRuntimeUrl(req, value) {\n const text = readText(value);\n if (!text) return '';\n if (/^https?:\\/\\//i.test(text)) return text;\n const normalized = text.startsWith('/')\n ? text\n : `/api/openclaw/runtime/files/${text.replace(/^\\/+/, '')}`;\n const origin = requestOrigin(req);\n return origin ? new URL(normalized, `${origin}/`).toString() : normalized;\n}\n\nfunction textToolResult(text, structuredContent = {}) {\n return {\n structuredContent,\n content: [{ type: 'text', text }],\n };\n}\n\nfunction sseHeaders(res) {\n res.status(200);\n res.setHeader('Content-Type', 'text/event-stream; charset=utf-8');\n res.setHeader('Cache-Control', 'no-cache, no-transform');\n res.setHeader('Connection', 'keep-alive');\n res.setHeader('X-Accel-Buffering', 'no');\n if (typeof res.flushHeaders === 'function') res.flushHeaders();\n}\n\nfunction writeSse(res, event, payload) {\n res.write(`event: ${event}\\n`);\n res.write(`data: ${JSON.stringify(payload)}\\n\\n`);\n}\n\nfunction asyncHandler(fn) {\n return (req, res, next) => {\n Promise.resolve(fn(req, res, next)).catch(next);\n };\n}\n\nfunction bearerToken(req) {\n const delegated = readText(req.get('x-dacheng-auth-token'));\n if (\n delegated &&\n delegated !== 'DACHENG_AUTH_TOKEN' &&\n !delegated.startsWith('secretref-env:') &&\n delegated !== 'dacheng-openclaw-proxy'\n ) {\n return delegated;\n }\n const header = req.get('Authorization') || '';\n if (!header.startsWith('Bearer ')) return '';\n const token = header.slice('Bearer '.length).trim();\n return token === 'dacheng-openclaw-proxy' ? '' : token;\n}\n\nfunction sha256(value) {\n return crypto.createHash('sha256').update(value).digest('hex');\n}\n\nfunction safeUserText(value, fallback = '') {\n if (typeof value !== 'string') return fallback;\n return value.replace(/\\u0000/g, '').trim();\n}\n\nfunction estimateTokens(text) {\n const normalized = String(text || '').trim();\n if (!normalized) return 0;\n return Math.max(1, Math.ceil(normalized.length / 3));\n}\n\nfunction readText(value) {\n if (value === undefined || value === null) return '';\n return String(value).replace(/\\u0000/g, '').trim();\n}\n\nconst miniAppHostApiVersion = '1.0';\nconst miniAppHighRiskPermissions = new Set([\n 'desktop.control',\n 'local.loopback',\n 'files.write',\n 'projects.write',\n 'openclaw.restart',\n]);\n\nfunction officialMiniAppRegistry(req) {\n const webBase = process.env.MINIAPP_WEB_BASE_URL || 'https://fabushi.ombhrum.com';\n const official = [\n {\n botId: 'bot.global-dharma',\n miniAppId: 'official.global-dharma',\n title: '全球法布施',\n subtitle: '地区、循环、场能都在对话框上方设置',\n initials: '法',\n icon: 'public',\n avatarColor: '#4CAF7A',\n kind: 'global_dharma',\n greeting: '把要分享的文字、链接或素材发给我,我会按上方选择的地区与模式启动全球法布施。',\n inputHint: '输入要法布施的文字/链接,或点 + 添加素材',\n permissions: [\n 'app.context',\n 'bot.chat',\n 'dharma.share',\n 'files.pick',\n 'openclaw.status',\n 'openclaw.chat',\n 'local.loopback',\n 'desktop.control',\n ],\n },\n {\n botId: 'bot.flashcards',\n miniAppId: 'official.flashcards',\n title: '背诵闪卡制作',\n subtitle: '随机挖空 / AI 制卡从顶部模式按钮选择',\n initials: '卡',\n icon: 'flashcards',\n avatarColor: '#7E57C2',\n kind: 'flashcards',\n greeting: '粘贴经文、文章正文或链接即可制作背诵闪卡。制卡模式请在上方按钮切换。',\n inputHint: '粘贴链接或正文,发送后制作闪卡',\n permissions: ['app.context', 'bot.chat', 'flashcards.create', 'openclaw.status', 'openclaw.chat'],\n },\n {\n botId: 'bot.platform-publish',\n miniAppId: 'official.platform-publish',\n title: '法布施到平台',\n subtitle: '选择平台后生成发布草稿并打开入口',\n initials: '发',\n icon: 'publish',\n avatarColor: '#FF9F43',\n kind: 'platform_publish',\n greeting: '把要发布的正文或链接发给我,上方选择平台后,我会生成发布草稿并打开对应平台入口。',\n inputHint: '输入要发布到平台的正文/链接',\n permissions: ['app.context', 'bot.chat', 'platform.publish', 'files.pick'],\n },\n {\n botId: 'bot.father',\n miniAppId: 'official.bot-father',\n title: '机器人之父',\n subtitle: '用对话生成个人沙箱小程序',\n initials: '父',\n icon: 'bot_father',\n avatarColor: '#3D8BFF',\n kind: 'bot_father',\n greeting: '告诉我你想要的小程序,我会生成 manifest、界面和权限声明,并放进你的个人沙箱。',\n inputHint: '描述你想创建的小程序',\n permissions: ['app.context', 'bot.chat', 'miniapps.dev'],\n },\n ];\n return {\n bots: official.map((bot, index) => ({\n ...bot,\n destinationIndex: index,\n source: 'official',\n })),\n miniApps: official.map((bot) => ({\n miniAppId: bot.miniAppId,\n botId: bot.botId,\n title: bot.title,\n subtitle: bot.subtitle,\n entryUrl: `${webBase.replace(/\\/+$/, '')}/miniapps/${encodeURIComponent(bot.miniAppId)}`,\n version: '1.0.0',\n permissions: bot.permissions,\n surfaces: ['homePinned', 'chatPanel'],\n theme: 'telegramDark',\n signature: 'official',\n reviewStatus: 'trusted',\n source: 'official',\n })),\n };\n}\n\nasync function readMiniAppStore() {\n try {\n const raw = await fs.promises.readFile(miniAppsStorePath, 'utf8');\n const decoded = JSON.parse(raw);\n return {\n apps: Array.isArray(decoded.apps) ? decoded.apps : [],\n };\n } catch (error) {\n if (error?.code === 'ENOENT') return { apps: [] };\n throw error;\n }\n}\n\nasync function writeMiniAppStore(store) {\n await fs.promises.mkdir(miniAppsDir, { recursive: true });\n await fs.promises.writeFile(\n miniAppsStorePath,\n JSON.stringify({ apps: store.apps || [] }, null, 2),\n );\n}\n\nfunction miniAppEntryUrl(req, miniAppId) {\n const origin = requestOrigin(req) || '';\n const pathName = `/api/miniapps/dev/${encodeURIComponent(miniAppId)}/index.html`;\n return origin ? new URL(pathName, `${origin}/`).toString() : pathName;\n}\n\nfunction sanitizeMiniAppPermissions(value, { allowHighRisk = false } = {}) {\n const raw = Array.isArray(value) ? value : [];\n const normalized = raw\n .map((item) => readText(item).toLowerCase())\n .filter((item) => /^[a-z][a-z0-9_.:-]{0,64}$/.test(item));\n const deduped = [...new Set(['app.context', 'bot.chat', ...normalized])];\n return deduped.filter((permission) => allowHighRisk || !miniAppHighRiskPermissions.has(permission));\n}\n\nfunction miniAppScan(permissions, sourceHtml = '') {\n const highRisk = permissions.filter((permission) => miniAppHighRiskPermissions.has(permission));\n const blockedPatterns = [\n /authorization\\s*:\\s*['\"`]bearer/i,\n /desktop_control_bridge_token/i,\n /openclaw_gateway_token/i,\n /eval\\s*\\(/i,\n ];\n const blocked = blockedPatterns\n .filter((pattern) => pattern.test(sourceHtml))\n .map((pattern) => pattern.toString());\n return {\n passed: blocked.length === 0,\n highRiskPermissions: highRisk,\n blockedPatterns: blocked,\n };\n}\n\nfunction miniAppRecordToBot(record) {\n return {\n botId: record.botId,\n miniAppId: record.miniAppId,\n title: record.title,\n subtitle: record.subtitle,\n initials: record.initials || '小',\n icon: record.icon || 'code',\n avatarColor: record.avatarColor || '#3D8BFF',\n kind: 'third_party',\n greeting: record.greeting || '你好,我是这个小程序的机器人。',\n inputHint: record.inputHint || '输入消息,或打开小程序面板',\n permissions: record.permissions || ['app.context', 'bot.chat'],\n source: record.source || 'sandbox',\n };\n}\n\nfunction miniAppRecordToManifest(req, record) {\n return {\n miniAppId: record.miniAppId,\n botId: record.botId,\n title: record.title,\n subtitle: record.subtitle,\n entryUrl: miniAppEntryUrl(req, record.miniAppId),\n version: record.version || '0.0.1',\n permissions: record.permissions || ['app.context', 'bot.chat'],\n surfaces: record.surfaces || ['chatPanel'],\n theme: record.theme || 'telegramDark',\n signature: record.signature || `sandbox:${record.ownerId}`,\n reviewStatus: record.reviewStatus || 'sandbox',\n source: record.source || 'sandbox',\n };\n}\n\nfunction safeMiniAppId(value) {\n return readText(value).replace(/[^a-zA-Z0-9_.-]/g, '-').slice(0, 80);\n}\n\nfunction titleFromPromptForMiniApp(prompt) {\n const text = readText(prompt).replace(/\\s+/g, ' ');\n if (!text) return '个人小程序';\n return text.length <= 18 ? text : text.slice(0, 18);\n}\n\nfunction escapeHtml(value) {\n return String(value || '')\n .replace(/&/g, '&')\n .replace(/</g, '<')\n .replace(/>/g, '>')\n .replace(/\"/g, '"')\n .replace(/'/g, ''');\n}\n\nfunction generatedMiniAppHtml({ title, prompt }) {\n const safeTitle = escapeHtml(title);\n const safePrompt = escapeHtml(prompt);\n const promptJson = JSON.stringify(readText(prompt));\n return `<!doctype html>\n<html lang=\"zh-Hans\">\n<head>\n <meta charset=\"utf-8\" />\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n <title>${safeTitle}</title>\n <style>\n body { margin:0; font-family:-apple-system,BlinkMacSystemFont,\"Segoe UI\",sans-serif; background:#0f1722; color:#fff; }\n main { padding:20px; display:grid; gap:14px; }\n .card { border:1px solid #263445; background:#17212b; border-radius:16px; padding:16px; }\n button { border:0; border-radius:999px; padding:10px 14px; background:#3390ec; color:white; font-weight:800; }\n pre { white-space:pre-wrap; color:#9ec7ff; }\n </style>\n</head>\n<body>\n <main>\n <section class=\"card\">\n <h1>${safeTitle}</h1>\n <p>这是机器人之父根据你的描述生成的个人沙箱小程序。</p>\n <pre>${safePrompt}</pre>\n </section>\n <section class=\"card\">\n <button id=\"context\">读取宿主上下文</button>\n <button id=\"chat\">让机器人解释这个小程序</button>\n <pre id=\"output\">等待操作...</pre>\n </section>\n </main>\n <script>\n const out = document.getElementById('output');\n async function invoke(method, params) {\n if (!window.FabushiMiniApp) throw new Error('FabushiMiniApp host SDK not ready');\n const result = await window.FabushiMiniApp.invoke(method, params || {});\n out.textContent = JSON.stringify(result, null, 2);\n }\n document.getElementById('context').onclick = () => invoke('app.getContext');\n const prompt = ${promptJson};\n document.getElementById('chat').onclick = () => invoke('bot.sendMessage', {\n message: '请简要说明这个小程序能做什么:' + prompt\n });\n </script>\n</body>\n</html>`;\n}\n\nfunction extractMiniAppHtml(value) {\n const text = readText(value);\n if (!text) return '';\n const fenced = text.match(/```(?:html)?\\s*([\\s\\S]*?)```/i);\n const candidate = (fenced ? fenced[1] : text).trim();\n if (!/<html[\\s>]/i.test(candidate) || !/<script[\\s>]/i.test(candidate)) return '';\n return candidate.startsWith('<!doctype') || candidate.startsWith('<html')\n ? candidate\n : `<!doctype html>\\n${candidate}`;\n}\n\nasync function generateBotFatherMiniAppHtml({ title, prompt, user }) {\n const fallback = generatedMiniAppHtml({ title, prompt });\n try {\n const result = await runAgentModel({\n user,\n mode: 'miniapp_generation',\n maxCompletionTokens: deepseekMaxCompletionTokens,\n messages: [\n {\n role: 'system',\n content: [\n '你是 Fabushi App 的小程序生成器。',\n '只输出一个完整静态 HTML 文件,不要 Markdown,不要解释。',\n '小程序只能通过 window.FabushiMiniApp.invoke(method, params) 调用宿主能力。',\n '禁止外链脚本、eval、Authorization token、桌面桥 token、OpenClaw token。',\n '优先实现一个可点击、可立即使用的小面板,包含 app.getContext 或 bot.sendMessage 示例。',\n ].join('\\n'),\n },\n {\n role: 'user',\n content: [\n `小程序标题:${title}`,\n `用户需求:${prompt}`,\n '请生成 HTML/CSS/JS 单文件。',\n ].join('\\n'),\n },\n ],\n });\n const sourceHtml = extractMiniAppHtml(result.message);\n if (!sourceHtml) {\n return {\n sourceHtml: fallback,\n generation: { provider: 'template', fallbackReason: 'ai_output_not_html' },\n };\n }\n return {\n sourceHtml,\n generation: {\n provider: result.provider || 'ai',\n model: result.model || '',\n },\n };\n } catch (error) {\n return {\n sourceHtml: fallback,\n generation: {\n provider: 'template',\n fallbackReason: readText(error?.message || error),\n },\n };\n }\n}\n\nfunction clientContextMessage(client) {\n if (!client || typeof client !== 'object') return '';\n const selectedModel = readText(client.selectedModel || client.model);\n const project = client.project && typeof client.project === 'object' ? client.project : {};\n const projectName = readText(project.name);\n const projectPath = readText(project.path);\n const lines = ['桌面端上下文:'];\n if (selectedModel) lines.push(`用户选择的 DeepSeek 模型:${normalizeDeepSeekModelName(selectedModel)}`);\n if (projectPath) {\n lines.push(`当前项目名称:${projectName || projectPath}`);\n lines.push(`当前项目路径:${projectPath}`);\n lines.push('如果本轮任务涉及读取、修改、运行、分析或整理项目,请优先围绕当前项目路径工作;不要擅自操作其他目录。');\n }\n return lines.length > 1 ? lines.join('\\n') : '';\n}\nfunction unwrapPayload(payload) {\n if (!payload || typeof payload !== 'object') return {};\n return payload.data && typeof payload.data === 'object' ? payload.data : payload;\n}\n\nfunction readFirstText(values) {\n for (const value of values) {\n const text = readText(value);\n if (text) return text;\n }\n return '';\n}\n\nfunction activeFlag(value) {\n if (value === true) return true;\n if (value === 1) return true;\n if (typeof value !== 'string') return false;\n return ['true', '1', 'yes', 'active'].includes(value.trim().toLowerCase());\n}\n\nfunction futureDate(value) {\n const text = readText(value);\n if (!text) return false;\n const timestamp = Date.parse(text);\n return Number.isFinite(timestamp) && timestamp > Date.now();\n}\n\nfunction normalizeMembershipPayload(payload) {\n if (!payload || typeof payload !== 'object') return false;\n const data = unwrapPayload(payload);\n const membership = data.membership && typeof data.membership === 'object' ? data.membership : data;\n const type = readText(membership.type || membership.membershipType || data.membershipType).toLowerCase();\n const status = readText(membership.status || data.status).toLowerCase();\n const expiry = membership.expiresAt ||\n membership.expires_at ||\n membership.membershipExpiry ||\n membership.membership_expires_at ||\n data.membershipExpiry ||\n data.membership_expires_at;\n const hasExplicitActiveFlag =\n membership.isActive !== undefined ||\n membership.active !== undefined ||\n membership.hasPremiumAccess !== undefined ||\n membership.hasPremiumMembership !== undefined ||\n membership.isPremium !== undefined;\n\n if (hasExplicitActiveFlag) {\n return Boolean(\n activeFlag(membership.isActive) ||\n activeFlag(membership.active) ||\n activeFlag(membership.hasPremiumAccess) ||\n activeFlag(membership.hasPremiumMembership) ||\n activeFlag(membership.isPremium),\n );\n }\n\n return Boolean(\n status === 'active' ||\n type === 'paid' ||\n type === 'premium' ||\n type === 'trial' ||\n futureDate(expiry),\n );\n}\n\nfunction extractRemoteAccount(payload) {\n const data = unwrapPayload(payload);\n const nestedUser = data.user && typeof data.user === 'object' ? data.user : {};\n const id = readFirstText([\n data.userId,\n data.user_id,\n data.userNo,\n data.user_no,\n data.id,\n nestedUser.userId,\n nestedUser.user_id,\n nestedUser.userNo,\n nestedUser.user_no,\n nestedUser.id,\n ]);\n const username = readFirstText([data.username, data.name, nestedUser.username, nestedUser.name]);\n const email = readFirstText([data.email, nestedUser.email]);\n const membership = data.membership && typeof data.membership === 'object' ? data.membership : null;\n return {\n authenticated: Boolean(id || username || email || membership || data.success === true),\n id,\n username,\n email,\n isMember: normalizeMembershipPayload(payload),\n membership,\n };\n}\n\nasync function resolveRemoteAccount(token) {\n const fallback = {\n authenticated: false,\n id: '',\n username: '',\n email: '',\n isMember: false,\n membership: null,\n };\n if (!token || !fabushiApiBaseUrl) return fallback;\n\n const endpoints = [\n '/api/stripe/membership-status',\n '/api/alipay/check-membership',\n '/api/auth/user-info',\n ];\n\n for (const endpoint of endpoints) {\n try {\n const response = await fetch(`${fabushiApiBaseUrl}${endpoint}`, {\n headers: {\n Accept: 'application/json',\n Authorization: `Bearer ${token}`,\n },\n signal: AbortSignal.timeout(8_000),\n });\n if (response.status === 401 || response.status === 403) return fallback;\n if (!response.ok) continue;\n const payload = await response.json();\n const account = extractRemoteAccount(payload);\n if (account.authenticated || account.isMember) {\n return {\n ...fallback,\n ...account,\n authenticated: true,\n };\n }\n } catch (error) {\n logger.debug({ endpoint, error: String(error) }, 'membership lookup skipped');\n }\n }\n\n return fallback;\n}\n\nasync function resolveUser(req, body = {}) {\n const token = bearerToken(req);\n const usernameHint = safeUserText(body.username || req.query.username);\n\n const testToken = process.env.TEST_ACCOUNT_TOKEN || 'dacheng-test-token-bypass-limits';\n if (token && token === testToken) {\n return {\n userId: 'user:test_account',\n username: 'TestAccount',\n tokenHash: sha256(token).slice(0, 24),\n isAuthenticated: true,\n isMember: true,\n isTestAccount: true,\n membership: { type: 'lifetime', active: true },\n };\n }\n\n const tokenHash = token ? sha256(token).slice(0, 24) : '';\n const anonymousHash = sha256(`${req.ip}|${req.get('User-Agent') || ''}`).slice(0, 24);\n const remoteAccount = await resolveRemoteAccount(token);\n const remoteIdentity = remoteAccount.id ||\n remoteAccount.username ||\n remoteAccount.email ||\n (remoteAccount.authenticated ? tokenHash : '');\n const userId = remoteAccount.authenticated\n ? `user:${remoteIdentity}`\n : tokenHash\n ? `token:${tokenHash}`\n : `anon:${anonymousHash}`;\n return {\n userId,\n username: remoteAccount.username || usernameHint,\n tokenHash,\n isAuthenticated: remoteAccount.authenticated,\n isMember: remoteAccount.isMember,\n membership: remoteAccount.membership,\n };\n}\n\nfunction usageFor(userId) {\n const month = monthKey();\n const row = statements.getUsage.get(userId, month);\n return {\n month,\n used: Number(row?.totalTokens || 0),\n };\n}\n\nfunction enforceTokenBudget(user, estimatedTokensForRequest) {\n if (user && user.isTestAccount) {\n return { limit: 999999999, usage: { month: monthKey(), used: 0 } };\n }\n if (requireMemberForAi && !user.isMember) {\n const error = new Error('大乘 AI 需要开通会员后使用');\n error.statusCode = 403;\n error.code = 'AI_MEMBERSHIP_REQUIRED';\n error.details = {\n requireMemberForAi,\n monthlyLimit: memberMonthlyLimit,\n remainingTokens: 0,\n };\n throw error;\n }\n\n const limit = user.isMember ? memberMonthlyLimit : freeMonthlyLimit;\n const usage = usageFor(user.userId);\n if (usage.used + estimatedTokensForRequest > limit) {\n const error = new Error('本月 AI token 额度已不足');\n error.statusCode = 429;\n error.code = 'USER_TOKEN_QUOTA_EXCEEDED';\n error.details = {\n monthlyLimit: limit,\n usedTokens: usage.used,\n remainingTokens: Math.max(0, limit - usage.used),\n };\n throw error;\n }\n return { limit, usage };\n}\n\nfunction completionBudgetFor(budget, estimatedPromptTokens) {\n const remainingAfterPrompt = budget.limit - budget.usage.used - estimatedPromptTokens;\n return Math.max(1, Math.min(deepseekMaxCompletionTokens, Math.floor(remainingAfterPrompt)));\n}\n\nfunction recordUsage(userId, totalTokens) {\n if (!totalTokens || totalTokens < 1) return;\n statements.upsertUsage.run({\n userId,\n month: monthKey(),\n totalTokens,\n updatedAt: nowIso(),\n });\n}\n\nfunction titleFromMessage(message) {\n const compact = message.replace(/\\s+/g, ' ').trim();\n if (!compact) return '新对话';\n return compact.length > 22 ? `${compact.slice(0, 22)}...` : compact;\n}\n\nfunction normalizeMessages(rows) {\n return rows.map((row) => ({\n role: row.role,\n content: row.content,\n }));\n}\n\nfunction normalizeMaxCompletionTokens(value) {\n const numeric = Number(value || deepseekMaxCompletionTokens);\n if (!Number.isFinite(numeric) || numeric < 1) return deepseekMaxCompletionTokens;\n return Math.max(1, Math.min(deepseekMaxCompletionTokens, Math.floor(numeric)));\n}\n\nfunction normalizeDeepSeekModelName(value) {\n const raw = readText(value || deepseekModel);\n if (!raw) return deepseekModel;\n const modelId = raw.includes('/') ? raw.split('/').pop() : raw;\n return modelId || deepseekModel;\n}\n\nasync function callDeepSeek(messages, options = {}) {\n if (!deepseekApiKey) {\n const error = new Error('DeepSeek API key is not configured');\n error.statusCode = 503;\n throw error;\n }\n\n const model = normalizeDeepSeekModelName(options.model);\n const response = await fetch(`${deepseekBaseUrl}/chat/completions`, {\n method: 'POST',\n headers: {\n Accept: 'application/json',\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${deepseekApiKey}`,\n },\n body: JSON.stringify({\n model,\n messages,\n temperature: 0.4,\n max_tokens: normalizeMaxCompletionTokens(options.maxCompletionTokens),\n stream: false,\n }),\n signal: AbortSignal.timeout(60_000),\n });\n\n const bodyText = await response.text();\n let payload;\n try {\n payload = bodyText ? JSON.parse(bodyText) : {};\n } catch {\n payload = { error: bodyText };\n }\n\n if (!response.ok) {\n const upstreamMessage = payload?.error?.message || payload?.message || '';\n const friendlyMessage =\n response.status === 402 || /insufficient\\s+balance/i.test(upstreamMessage)\n ? '大乘 AI 服务额度暂不可用,请联系管理员充值 DeepSeek 账户后重试。'\n : upstreamMessage || `DeepSeek request failed: ${response.status}`;\n const error = new Error(friendlyMessage);\n error.statusCode = response.status;\n if (response.status === 402 || /insufficient\\s+balance/i.test(upstreamMessage)) {\n error.code = 'DEEPSEEK_PROVIDER_QUOTA';\n }\n throw error;\n }\n\n const message = payload?.choices?.[0]?.message?.content?.trim();\n if (!message) {\n const error = new Error('DeepSeek returned an empty response');\n error.statusCode = 502;\n throw error;\n }\n\n return {\n message,\n model,\n usage: {\n promptTokens: Number(payload?.usage?.prompt_tokens || 0),\n completionTokens: Number(payload?.usage?.completion_tokens || 0),\n totalTokens: Number(payload?.usage?.total_tokens || 0),\n },\n };\n}\n\nasync function callDeepSeekStream(messages, callbacks = {}) {\n if (!deepseekApiKey) {\n const error = new Error('DeepSeek API key is not configured');\n error.statusCode = 503;\n throw error;\n }\n\n const model = normalizeDeepSeekModelName(callbacks.model);\n const response = await fetch(`${deepseekBaseUrl}/chat/completions`, {\n method: 'POST',\n headers: {\n Accept: 'text/event-stream',\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${deepseekApiKey}`,\n },\n body: JSON.stringify({\n model,\n messages,\n temperature: 0.4,\n max_tokens: normalizeMaxCompletionTokens(callbacks.maxCompletionTokens),\n stream: true,\n stream_options: { include_usage: true },\n }),\n signal: callbacks.signal || AbortSignal.timeout(90_000),\n });\n\n if (!response.ok) {\n const bodyText = await response.text();\n let payload;\n try {\n payload = bodyText ? JSON.parse(bodyText) : {};\n } catch {\n payload = { error: bodyText };\n }\n const upstreamMessage = payload?.error?.message || payload?.message || '';\n const friendlyMessage =\n response.status === 402 || /insufficient\\s+balance/i.test(upstreamMessage)\n ? '大乘 AI 服务额度暂不可用,请联系管理员充值 DeepSeek 账户后重试。'\n : upstreamMessage || `DeepSeek request failed: ${response.status}`;\n const error = new Error(friendlyMessage);\n error.statusCode = response.status;\n if (response.status === 402 || /insufficient\\s+balance/i.test(upstreamMessage)) {\n error.code = 'DEEPSEEK_PROVIDER_QUOTA';\n }\n throw error;\n }\n\n let buffer = '';\n let message = '';\n let usage = {\n promptTokens: 0,\n completionTokens: 0,\n totalTokens: 0,\n };\n const decoder = new TextDecoder();\n\n for await (const chunk of response.body) {\n buffer += decoder.decode(chunk, { stream: true });\n const lines = buffer.split(/\\r?\\n/);\n buffer = lines.pop() || '';\n\n for (const rawLine of lines) {\n const line = rawLine.trim();\n if (!line.startsWith('data:')) continue;\n const data = line.slice('data:'.length).trim();\n if (!data || data === '[DONE]') continue;\n\n let payload;\n try {\n payload = JSON.parse(data);\n } catch {\n continue;\n }\n\n const delta = payload?.choices?.[0]?.delta?.content || '';\n if (delta) {\n message += delta;\n callbacks.onToken?.(delta);\n }\n\n if (payload?.usage) {\n usage = {\n promptTokens: Number(payload.usage.prompt_tokens || usage.promptTokens || 0),\n completionTokens: Number(payload.usage.completion_tokens || usage.completionTokens || 0),\n totalTokens: Number(payload.usage.total_tokens || usage.totalTokens || 0),\n };\n }\n }\n }\n\n message = message.trim();\n if (!message) {\n const error = new Error('DeepSeek returned an empty response');\n error.statusCode = 502;\n throw error;\n }\n\n return { message, model, usage };\n}\n\nfunction createCodexDeepSeekRuntime() {\n if (!enableCodexSdkChat || !deepseekApiKey) {\n return {\n enabled: false,\n provider: 'deepseek-direct',\n reason: enableCodexSdkChat ? 'DeepSeek API key is not configured' : 'ENABLE_CODEX_SDK_CHAT is not true',\n };\n }\n\n return {\n enabled: true,\n provider: 'codex-sdk-deepseek',\n options: {\n apiKey: deepseekApiKey,\n baseUrl: deepseekBaseUrl,\n env: {\n ...process.env,\n DEEPSEEK_API_KEY: deepseekApiKey,\n HOME: codexHomeDir,\n CODEX_HOME: codexHomeDir,\n TMPDIR: codexTempDir,\n XDG_RUNTIME_DIR: codexRuntimeDir,\n XDG_CACHE_HOME: path.join(codexHomeDir, '.cache'),\n XDG_CONFIG_HOME: path.join(codexHomeDir, '.config'),\n XDG_DATA_HOME: path.join(codexHomeDir, '.local', 'share'),\n PATH: process.env.PATH || '/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin',\n },\n config: {\n model_provider: codexDeepSeekProviderId,\n model_providers: {\n [codexDeepSeekProviderId]: {\n name: 'DeepSeek Chat Completions',\n base_url: codexResponsesBaseUrl,\n env_key: 'DEEPSEEK_API_KEY',\n wire_api: 'responses',\n query_params: {},\n },\n },\n },\n },\n threadOptions: {\n model: deepseekModel,\n approvalPolicy: 'never',\n sandboxMode: 'read-only',\n skipGitRepoCheck: true,\n networkAccessEnabled: true,\n },\n };\n}\n\nfunction codexPromptFromMessages(messages) {\n return messages\n .map((message) => {\n const role = String(message.role || 'user').toUpperCase();\n return `${role}:\\n${message.content}`;\n })\n .join('\\n\\n');\n}\n\nfunction responseContentText(content) {\n if (typeof content === 'string') return content;\n if (!Array.isArray(content)) return '';\n return content\n .map((part) => {\n if (typeof part === 'string') return part;\n if (!part || typeof part !== 'object') return '';\n return String(part.text || part.input_text || part.output_text || '');\n })\n .filter(Boolean)\n .join('\\n');\n}\n\nfunction codexResponsesPrompt(body) {\n const input = Array.isArray(body?.input) ? body.input : [];\n const userItems = input.filter((item) => item?.role === 'user');\n const source = userItems[userItems.length - 1] || input[input.length - 1];\n const prompt = responseContentText(source?.content).trim();\n if (prompt) return prompt;\n if (typeof body?.input === 'string') return body.input.trim();\n return '';\n}\n\nfunction writeResponsesEvent(res, event, data) {\n res.write(`event: ${event}\\n`);\n res.write(`data: ${JSON.stringify(data)}\\n\\n`);\n}\n\nfunction responseUsage(usage = {}) {\n return {\n input_tokens: Number(usage.promptTokens || usage.input_tokens || 0),\n cached_input_tokens: 0,\n output_tokens: Number(usage.completionTokens || usage.output_tokens || 0),\n reasoning_output_tokens: 0,\n total_tokens: Number(usage.totalTokens || usage.total_tokens || 0),\n };\n}\n\nfunction createLibreChatAgentRuntime() {\n if (!enableLibreChatAgentChat || !libreChatAgentApiKey || !libreChatAgentId) {\n return {\n enabled: false,\n provider: 'librechat-agent',\n reason: !enableLibreChatAgentChat\n ? 'ENABLE_LIBRECHAT_AGENT_CHAT is not true'\n : !libreChatAgentApiKey\n ? 'LIBRECHAT_AGENT_API_KEY is not configured'\n : 'LIBRECHAT_AGENT_ID is not configured',\n };\n }\n\n return {\n enabled: true,\n provider: 'librechat-agent',\n model: libreChatAgentId,\n chatCompletionsUrl: `${libreChatAgentsBaseUrl}/chat/completions`,\n };\n}\n\nfunction libreChatUsage(usage = {}) {\n return {\n promptTokens: Number(usage.prompt_tokens || usage.input_tokens || usage.promptTokens || 0),\n completionTokens: Number(usage.completion_tokens || usage.output_tokens || usage.completionTokens || 0),\n totalTokens: Number(usage.total_tokens || usage.totalTokens || 0),\n };\n}\n\nasync function callLibreChatAgent(messages) {\n const runtime = createLibreChatAgentRuntime();\n if (!runtime.enabled) {\n const error = new Error(runtime.reason || 'LibreChat Agent chat is not enabled');\n error.statusCode = 503;\n throw error;\n }\n\n const response = await fetch(runtime.chatCompletionsUrl, {\n method: 'POST',\n headers: {\n Accept: 'application/json',\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${libreChatAgentApiKey}`,\n },\n body: JSON.stringify({\n model: runtime.model,\n messages,\n temperature: 0.4,\n stream: false,\n }),\n signal: AbortSignal.timeout(120_000),\n });\n\n const bodyText = await response.text();\n let payload;\n try {\n payload = bodyText ? JSON.parse(bodyText) : {};\n } catch {\n payload = { error: bodyText };\n }\n\n if (!response.ok) {\n const upstreamMessage = payload?.error?.message || payload?.message || bodyText || '';\n const error = new Error(upstreamMessage || `LibreChat Agent request failed: ${response.status}`);\n error.statusCode = response.status;\n throw error;\n }\n\n const message = payload?.choices?.[0]?.message?.content?.trim();\n if (!message) {\n const error = new Error('LibreChat Agent returned an empty response');\n error.statusCode = 502;\n throw error;\n }\n\n return {\n message,\n usage: libreChatUsage(payload?.usage),\n };\n}\n\nasync function callLibreChatAgentStream(messages, callbacks = {}) {\n const runtime = createLibreChatAgentRuntime();\n if (!runtime.enabled) {\n const error = new Error(runtime.reason || 'LibreChat Agent chat is not enabled');\n error.statusCode = 503;\n throw error;\n }\n\n const response = await fetch(runtime.chatCompletionsUrl, {\n method: 'POST',\n headers: {\n Accept: 'text/event-stream',\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${libreChatAgentApiKey}`,\n },\n body: JSON.stringify({\n model: runtime.model,\n messages,\n temperature: 0.4,\n stream: true,\n }),\n signal: callbacks.signal || AbortSignal.timeout(180_000),\n });\n\n if (!response.ok) {\n const bodyText = await response.text();\n let payload;\n try {\n payload = bodyText ? JSON.parse(bodyText) : {};\n } catch {\n payload = { error: bodyText };\n }\n const upstreamMessage = payload?.error?.message || payload?.message || bodyText || '';\n const error = new Error(upstreamMessage || `LibreChat Agent request failed: ${response.status}`);\n error.statusCode = response.status;\n throw error;\n }\n\n let buffer = '';\n let message = '';\n let usage = { promptTokens: 0, completionTokens: 0, totalTokens: 0 };\n const decoder = new TextDecoder();\n\n for await (const chunk of response.body) {\n buffer += decoder.decode(chunk, { stream: true });\n const lines = buffer.split(/\\r?\\n/);\n buffer = lines.pop() || '';\n\n for (const rawLine of lines) {\n const line = rawLine.trim();\n if (!line.startsWith('data:')) continue;\n const data = line.slice('data:'.length).trim();\n if (!data || data === '[DONE]') continue;\n\n let payload;\n try {\n payload = JSON.parse(data);\n } catch {\n continue;\n }\n\n const delta = payload?.choices?.[0]?.delta?.content || '';\n if (delta) {\n message += delta;\n callbacks.onToken?.(delta);\n }\n\n if (payload?.usage) {\n usage = libreChatUsage(payload.usage);\n }\n }\n }\n\n message = message.trim();\n if (!message) {\n const error = new Error('LibreChat Agent returned an empty response');\n error.statusCode = 502;\n throw error;\n }\n\n return { message, usage };\n}\n\nfunction isDeepSeekQuotaError(error) {\n if (error?.code === 'DEEPSEEK_PROVIDER_QUOTA') return true;\n if (error?.code === 'USER_TOKEN_QUOTA_EXCEEDED' || error?.code === 'AI_MEMBERSHIP_REQUIRED') return false;\n const message = String(error?.message || error || '');\n return /insufficient\\s+balance|balance/i.test(message);\n}\n\nfunction publicAiErrorMessage(error) {\n if (isDeepSeekQuotaError(error)) {\n return '大乘 AI 服务额度暂不可用,请联系管理员充值 DeepSeek 账户后重试。';\n }\n return error?.message || 'Internal Server Error';\n}\n\nfunction responsesPayload({\n responseId,\n itemId,\n status,\n model,\n text = '',\n usage = null,\n error = null,\n}) {\n return {\n id: responseId,\n object: 'response',\n status,\n model,\n output:\n status === 'completed'\n ? [\n {\n id: itemId,\n type: 'message',\n role: 'assistant',\n status: 'completed',\n content: [{ type: 'output_text', text }],\n },\n ]\n : [],\n ...(usage ? { usage: responseUsage(usage) } : {}),\n ...(error\n ? {\n error: {\n message: error.message || String(error),\n type: 'server_error',\n },\n }\n : {}),\n };\n}\n\nasync function callCodexSdkDeepSeek(messages, callbacks = {}) {\n const codexRuntime = createCodexDeepSeekRuntime();\n if (!codexRuntime.enabled) {\n const error = new Error(codexRuntime.reason || 'Codex SDK chat is not enabled');\n error.statusCode = 503;\n throw error;\n }\n\n const { Codex } = await import('@openai/codex-sdk');\n const codex = new Codex(codexRuntime.options);\n const thread = codex.startThread(codexRuntime.threadOptions);\n const prompt = codexPromptFromMessages(messages);\n\n if (callbacks.onToken || callbacks.onStep) {\n const { events } = await thread.runStreamed(prompt, { signal: callbacks.signal });\n let finalResponse = '';\n let usage = null;\n\n for await (const event of events) {\n if (event.type === 'item.completed') {\n const item = event.item;\n if (item.type === 'agent_message') {\n finalResponse = item.text || finalResponse;\n callbacks.onToken?.(item.text || '');\n } else if (item.type === 'todo_list') {\n callbacks.onStep?.({\n title: 'Codex SDK 执行计划',\n message: item.items.map((todo) => `${todo.completed ? '✓' : '•'} ${todo.text}`).join('\\n'),\n });\n } else if (item.type === 'command_execution') {\n callbacks.onStep?.({\n title: 'Codex SDK 执行命令',\n message: item.command,\n });\n } else if (item.type === 'mcp_tool_call') {\n callbacks.onStep?.({\n title: 'Codex SDK 调用工具',\n message: `${item.server}.${item.tool}`,\n });\n }\n } else if (event.type === 'turn.completed') {\n usage = event.usage;\n } else if (event.type === 'turn.failed') {\n throw new Error(event.error?.message || 'Codex SDK turn failed');\n } else if (event.type === 'error') {\n throw new Error(event.message || 'Codex SDK stream failed');\n }\n }\n\n if (!finalResponse.trim()) {\n const error = new Error('Codex SDK returned an empty response');\n error.statusCode = 502;\n throw error;\n }\n\n return {\n message: finalResponse.trim(),\n usage: {\n promptTokens: Number(usage?.input_tokens || 0),\n completionTokens: Number(usage?.output_tokens || 0),\n totalTokens: Number((usage?.input_tokens || 0) + (usage?.output_tokens || 0)),\n },\n };\n }\n\n const turn = await thread.run(prompt);\n if (!turn.finalResponse?.trim()) {\n const error = new Error('Codex SDK returned an empty response');\n error.statusCode = 502;\n throw error;\n }\n\n return {\n message: turn.finalResponse.trim(),\n usage: {\n promptTokens: Number(turn.usage?.input_tokens || 0),\n completionTokens: Number(turn.usage?.output_tokens || 0),\n totalTokens: Number((turn.usage?.input_tokens || 0) + (turn.usage?.output_tokens || 0)),\n },\n };\n}\n\nconst activeAgentRuns = new Map();\n\nfunction normalizeAgentMode(value) {\n const mode = safeUserText(value || 'dharma_guide');\n return /^[a-z][a-z0-9_:-]{0,48}$/i.test(mode) ? mode : 'dharma_guide';\n}\n\nfunction openClawSessionKey(user, conversationId) {\n const env = process.env.NODE_ENV === 'production' ? 'prod' : 'dev';\n return `fabushi:env:${env}:user:${user.userId}:conversation:${conversationId}`;\n}\n\nfunction createOpenClawRuntime() {\n if (!enableOpenClawAgentChat || !openClawGatewayToken || !openClawGatewayUrl) {\n return {\n enabled: false,\n provider: 'openclaw-gateway',\n reason: !enableOpenClawAgentChat\n ? 'ENABLE_OPENCLAW_AGENT_CHAT is not true'\n : !openClawGatewayToken\n ? 'OPENCLAW_GATEWAY_TOKEN is not configured'\n : 'OPENCLAW_GATEWAY_URL is not configured',\n };\n }\n\n return {\n enabled: true,\n provider: 'openclaw-gateway',\n model: openClawAgentId,\n runsUrl: openClawRunsEndpoint || `${openClawGatewayUrl}/v1/agents/${encodeURIComponent(openClawAgentId)}/runs`,\n };\n}\n\nfunction extractOpenClawText(payload) {\n if (!payload || typeof payload !== 'object') return '';\n const candidates = [\n payload.message,\n payload.content,\n payload.output_text,\n payload.finalResponse,\n payload.final_response,\n payload.result?.message,\n payload.result?.content,\n payload.output?.message,\n payload.output?.content,\n payload.choices?.[0]?.message?.content,\n ];\n for (const candidate of candidates) {\n if (typeof candidate === 'string' && candidate.trim()) return candidate.trim();\n }\n return '';\n}\n\nfunction extractOpenClawUsage(payload) {\n const usage = payload?.usage || payload?.result?.usage || {};\n return {\n promptTokens: Number(usage.input_tokens || usage.prompt_tokens || usage.promptTokens || 0),\n completionTokens: Number(usage.output_tokens || usage.completion_tokens || usage.completionTokens || 0),\n totalTokens: Number(usage.total_tokens || usage.totalTokens || 0),\n };\n}\n\nasync function callOpenClawAgent({ messages, user, conversationId, mode, signal }) {\n const runtime = createOpenClawRuntime();\n if (!runtime.enabled) {\n const error = new Error(runtime.reason || 'OpenClaw Agent chat is not enabled');\n error.statusCode = 503;\n throw error;\n }\n\n const sessionKey = openClawSessionKey(user, conversationId);\n const response = await fetch(runtime.runsUrl, {\n method: 'POST',\n headers: {\n Accept: 'application/json',\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${openClawGatewayToken}`,\n },\n body: JSON.stringify({\n agentId: openClawAgentId,\n sessionKey,\n mode,\n stream: false,\n input: { messages },\n messages,\n metadata: {\n app: 'fabushi',\n userId: user.userId,\n conversationId,\n toolPolicy: 'public-user-read-mostly',\n },\n }),\n signal: signal || AbortSignal.timeout(180_000),\n });\n\n const bodyText = await response.text();\n let payload;\n try {\n payload = bodyText ? JSON.parse(bodyText) : {};\n } catch {\n payload = { message: bodyText };\n }\n\n if (!response.ok) {\n const error = new Error(payload?.error?.message || payload?.message || `OpenClaw request failed: ${response.status}`);\n error.statusCode = response.status;\n throw error;\n }\n\n const message = extractOpenClawText(payload);\n if (!message) {\n const error = new Error('OpenClaw returned an empty response');\n error.statusCode = 502;\n throw error;\n }\n\n return {\n message,\n runId: payload.runId || payload.id || payload.result?.runId || null,\n provider: runtime.provider,\n model: runtime.model,\n usage: extractOpenClawUsage(payload),\n };\n}\n\nasync function runAgentModel({ messages, user, conversationId, mode, callbacks = {}, signal, maxCompletionTokens }) {\n const openClawRuntime = createOpenClawRuntime();\n if (openClawRuntime.enabled && !callbacks.onToken) {\n return await callOpenClawAgent({ messages, user, conversationId, mode, signal });\n }\n\n const codexRuntime = createCodexDeepSeekRuntime();\n const libreChatRuntime = createLibreChatAgentRuntime();\n let provider = libreChatRuntime.enabled\n ? libreChatRuntime.provider\n : codexRuntime.enabled\n ? codexRuntime.provider\n : 'deepseek';\n let model = libreChatRuntime.enabled ? libreChatRuntime.model : deepseekModel;\n\n try {\n if (libreChatRuntime.enabled) {\n const result = callbacks.onToken\n ? await callLibreChatAgentStream(messages, { ...callbacks, signal })\n : await callLibreChatAgent(messages);\n return { ...result, provider, model };\n }\n if (codexRuntime.enabled) {\n const result = await callCodexSdkDeepSeek(messages, { ...callbacks, signal });\n return { ...result, provider, model };\n }\n const result = callbacks.onToken\n ? await callDeepSeekStream(messages, { ...callbacks, signal, maxCompletionTokens })\n : await callDeepSeek(messages, { maxCompletionTokens });\n return { ...result, provider, model };\n } catch (error) {\n if (!codexRuntime.enabled || isDeepSeekQuotaError(error)) throw error;\n logger.warn({ error: String(error) }, 'Agent model failed, falling back to DeepSeek direct');\n const result = callbacks.onToken\n ? await callDeepSeekStream(messages, { ...callbacks, signal, maxCompletionTokens })\n : await callDeepSeek(messages, { maxCompletionTokens });\n return { ...result, provider: 'deepseek', model: deepseekModel };\n }\n}\n\nfunction updateAgentRunStatus(run, status, fields = {}) {\n statements.updateAgentRunStatus.run({\n id: run.id,\n userId: run.user_id || run.userId,\n status,\n openClawRunId: fields.openClawRunId ?? null,\n inputTokens: fields.inputTokens ?? null,\n outputTokens: fields.outputTokens ?? null,\n toolCallCount: fields.toolCallCount ?? null,\n completedAt: status === 'completed' || status === 'cancelled' ? nowIso() : null,\n failedAt: status === 'failed' ? nowIso() : null,\n errorCode: fields.errorCode ?? null,\n errorMessage: fields.errorMessage ?? null,\n });\n}\n\nfunction buildAgentMessages(conversationId) {\n const historyRows = statements.listMessages.all(conversationId).slice(-14);\n return [{ role: 'system', content: systemPrompt }, ...normalizeMessages(historyRows)];\n}\n\nfunction agentUsagePayload(usage, user, budget) {\n const promptTokens = usage.promptTokens || usage.inputTokens || 0;\n const completionTokens = usage.completionTokens || usage.outputTokens || 0;\n const totalTokens = usage.totalTokens || promptTokens + completionTokens;\n const latestUsage = usageFor(user.userId);\n return {\n inputTokens: promptTokens,\n outputTokens: completionTokens,\n totalTokens,\n monthlyLimit: budget.limit,\n remainingTokens: Math.max(0, budget.limit - latestUsage.used),\n };\n}\n\nfunction openAiContentToText(content) {\n if (typeof content === 'string') return content;\n if (Array.isArray(content)) {\n return content\n .map((part) => {\n if (typeof part === 'string') return part;\n if (!part || typeof part !== 'object') return '';\n return readText(part.text || part.content || part.value);\n })\n .filter(Boolean)\n .join('\\n');\n }\n return readText(content);\n}\n\nfunction normalizeOpenAiMessages(value) {\n if (!Array.isArray(value)) return [];\n const allowedRoles = new Set(['system', 'user', 'assistant', 'tool']);\n return value\n .map((item) => {\n if (!item || typeof item !== 'object') return null;\n const role = allowedRoles.has(item.role) ? item.role : 'user';\n const content = openAiContentToText(item.content);\n if (!content) return null;\n return { role, content };\n })\n .filter(Boolean);\n}\n\nfunction truncateForDeepSeekMessage(text, maxChars = 12_000) {\n const value = readText(text);\n if (value.length <= maxChars) return value;\n return `${value.slice(0, maxChars)}\\n\\n[内容已截断]`;\n}\n\nfunction truncateTailForDeepSeekMessage(text, maxChars = 2400) {\n const value = readText(text);\n if (value.length <= maxChars) return value;\n return `[前文已省略]\\n\\n${value.slice(-maxChars)}`;\n}\n\nfunction stripOpenClawPromptNoise(text) {\n const value = readText(text);\n if (!value) return '';\n\n const finalPromptMatch = value.match(/finalPromptText[\"'\\s::]+([\\s\\S]{1,4000})$/i);\n if (finalPromptMatch?.[1]) {\n return readText(finalPromptMatch[1].replace(/^[\"'`]+|[\"'`,}\\]]+$/g, ''));\n }\n\n const markers = [\n 'final prompt:',\n 'final user prompt:',\n 'user prompt:',\n '用户问题:',\n '用户消息:',\n '用户输入:',\n ];\n const lower = value.toLowerCase();\n for (const marker of markers) {\n const index = lower.lastIndexOf(marker.toLowerCase());\n if (index >= 0) {\n const tail = value.slice(index + marker.length).trim();\n if (tail) return tail;\n }\n }\n\n return value;\n}\n\nfunction compactOpenClawMessagesForDeepSeek(messages) {\n const conversationalMessages = messages\n .filter((message) => message.role === 'user' || message.role === 'assistant')\n .map((message) => ({\n role: message.role,\n content: truncateTailForDeepSeekMessage(stripOpenClawPromptNoise(message.content)),\n }))\n .filter((message) => message.content);\n\n const recent = conversationalMessages.slice(-6);\n while (recent.length > 0 && recent[0].role !== 'user') {\n recent.shift();\n }\n\n if (recent.length === 0) {\n const fallback = messages\n .slice()\n .reverse()\n .map((message) => truncateTailForDeepSeekMessage(stripOpenClawPromptNoise(message.content), 2400))\n .find(Boolean);\n if (!fallback) return messages;\n recent.push({ role: 'user', content: fallback });\n }\n\n return [\n {\n role: 'system',\n content: '你是大乘 App 的桌面 AI 助手。请直接回答用户问题,默认使用简洁、准确的中文。',\n },\n ...recent,\n ];\n}\n\nfunction isDeepSeekBlockedError(error) {\n return (\n Number(error?.statusCode) === 403 &&\n /blocked|被拦截|forbidden/i.test(String(error?.message || error || ''))\n );\n}\n\nfunction shouldRetryWithCompactedOpenClawMessages(error, messages, compactedMessages) {\n if (!isDeepSeekBlockedError(error)) return false;\n return JSON.stringify(messages) !== JSON.stringify(compactedMessages);\n}\n\nfunction openClawBlockedFallbackText(messages) {\n const lastUser = messages\n .slice()\n .reverse()\n .find((message) => message.role === 'user')?.content || '';\n const localActionIntent =\n /(file|folder|directory|project|workspace|path|文件|文件夹|目录|项目|工程|路径|本地)/i.test(lastUser) &&\n /(create|make|mkdir|write|edit|modify|rename|delete|move|copy|save|download|read|list|生成|创建|新建|写入|编辑|修改|重命名|删除|移动|复制|保存|下载|读取|列出)/i.test(lastUser);\n\n if (localActionIntent) {\n return [\n '本机工具规划被模型服务拦截了,所以这次没有实际读取、创建、修改或下载本机文件。',\n '如果只是新建文件夹,请先在底部选择项目后重新发送“新建文件夹”;桌面端会直接在已选项目里创建。',\n '如果是更复杂的本地文件操作,请在“连应用”里重启本机 AI 后重试,或把要操作的文件路径和目标说得更具体。',\n ].join('\\n');\n }\n\n return '模型服务拦截了这次回复,未能生成结果。请换一种更具体、更短的表述后重试。';\n}\nfunction usageWithFallback(usage, messages, text) {\n const promptTokens = Number(usage?.promptTokens || usage?.inputTokens || 0);\n const completionTokens = Number(usage?.completionTokens || usage?.outputTokens || 0);\n const totalTokens = Number(usage?.totalTokens || 0);\n if (totalTokens > 0) {\n return {\n promptTokens,\n completionTokens,\n totalTokens,\n };\n }\n const estimatedPrompt = promptTokens || estimateTokens(JSON.stringify(messages));\n const estimatedCompletion = completionTokens || estimateTokens(text);\n return {\n promptTokens: estimatedPrompt,\n completionTokens: estimatedCompletion,\n totalTokens: estimatedPrompt + estimatedCompletion,\n };\n}\n\nfunction openAiUsagePayload(usage, user, budget) {\n const promptTokens = Number(usage.promptTokens || 0);\n const completionTokens = Number(usage.completionTokens || 0);\n const totalTokens = Number(usage.totalTokens || promptTokens + completionTokens);\n const latestUsage = usageFor(user.userId);\n const remainingTokens = Math.max(0, budget.limit - latestUsage.used);\n return {\n prompt_tokens: promptTokens,\n completion_tokens: completionTokens,\n total_tokens: totalTokens,\n promptTokens,\n completionTokens,\n totalTokens,\n monthlyLimit: budget.limit,\n remainingTokens,\n };\n}\n\nfunction writeOpenAiSse(res, payload) {\n res.write(`data: ${JSON.stringify(payload)}\\n\\n`);\n}\n\napp.post(\n '/api/openclaw/deepseek/v1/chat/completions',\n asyncHandler(async (req, res) => {\n const user = await resolveUser(req, req.body || {});\n const incomingMessages = normalizeOpenAiMessages(req.body?.messages);\n if (incomingMessages.length === 0) {\n res.status(400).json({ error: { message: 'messages is required' } });\n return;\n }\n\n const messages = compactOpenClawMessagesForDeepSeek(incomingMessages);\n const promptEstimate = estimateTokens(JSON.stringify(messages));\n const estimated = promptEstimate + 600;\n const budget = enforceTokenBudget(user, estimated);\n const maxCompletionTokens = Math.min(\n completionBudgetFor(budget, promptEstimate),\n normalizeMaxCompletionTokens(req.body?.max_tokens),\n );\n const model = normalizeDeepSeekModelName(req.body?.model);\n const wantsStream =\n req.body?.stream === true ||\n String(req.get('accept') || '').includes('text/event-stream');\n const id = `chatcmpl_${crypto.randomUUID().replaceAll('-', '')}`;\n const created = Math.floor(Date.now() / 1000);\n const compactedMessages = messages;\n\n if (!wantsStream) {\n let result;\n let usageMessages = messages;\n try {\n result = await callDeepSeek(messages, { model, maxCompletionTokens });\n } catch (error) {\n if (!shouldRetryWithCompactedOpenClawMessages(error, messages, compactedMessages)) {\n throw error;\n }\n logger.warn(\n {\n statusCode: error?.statusCode,\n messageCount: messages.length,\n compactedMessageCount: compactedMessages.length,\n },\n 'OpenClaw DeepSeek proxy retrying with compacted messages',\n );\n usageMessages = compactedMessages;\n result = await callDeepSeek(compactedMessages, { model, maxCompletionTokens });\n }\n const usage = usageWithFallback(result.usage, usageMessages, result.message);\n recordUsage(user.userId, usage.totalTokens);\n res.json({\n id,\n object: 'chat.completion',\n created,\n model: result.model || model,\n choices: [\n {\n index: 0,\n message: { role: 'assistant', content: result.message },\n finish_reason: 'stop',\n },\n ],\n usage: openAiUsagePayload(usage, user, budget),\n });\n return;\n }\n\n const abortController = new AbortController();\n req.on('close', () => abortController.abort());\n\n res.status(200);\n res.set({\n 'Content-Type': 'text/event-stream; charset=utf-8',\n 'Cache-Control': 'no-cache, no-transform',\n Connection: 'keep-alive',\n 'X-Accel-Buffering': 'no',\n });\n res.flushHeaders?.();\n\n const baseChunk = {\n id,\n object: 'chat.completion.chunk',\n created,\n model,\n system_fingerprint: 'fabushi-openclaw-deepseek-proxy',\n };\n writeOpenAiSse(res, {\n ...baseChunk,\n choices: [{ index: 0, delta: { role: 'assistant' }, finish_reason: null }],\n });\n\n let text = '';\n let usageMessages = messages;\n try {\n let result;\n try {\n result = await callDeepSeekStream(messages, {\n model,\n maxCompletionTokens,\n signal: abortController.signal,\n onToken: (delta) => {\n text += delta;\n writeOpenAiSse(res, {\n ...baseChunk,\n choices: [{ index: 0, delta: { content: delta }, finish_reason: null }],\n });\n },\n });\n } catch (error) {\n if (!shouldRetryWithCompactedOpenClawMessages(error, messages, compactedMessages)) {\n throw error;\n }\n logger.warn(\n {\n statusCode: error?.statusCode,\n messageCount: messages.length,\n compactedMessageCount: compactedMessages.length,\n },\n 'OpenClaw DeepSeek proxy stream retrying with compacted messages',\n );\n text = '';\n usageMessages = compactedMessages;\n result = await callDeepSeekStream(compactedMessages, {\n model,\n maxCompletionTokens,\n signal: abortController.signal,\n onToken: (delta) => {\n text += delta;\n writeOpenAiSse(res, {\n ...baseChunk,\n choices: [{ index: 0, delta: { content: delta }, finish_reason: null }],\n });\n },\n });\n }\n text = result.message || text;\n const usage = usageWithFallback(result.usage, usageMessages, text);\n recordUsage(user.userId, usage.totalTokens);\n writeOpenAiSse(res, {\n ...baseChunk,\n model: result.model || model,\n choices: [{ index: 0, delta: {}, finish_reason: 'stop' }],\n });\n writeOpenAiSse(res, {\n ...baseChunk,\n model: result.model || model,\n choices: [],\n usage: openAiUsagePayload(usage, user, budget),\n });\n res.write('data: [DONE]\\n\\n');\n } catch (error) {\n if (isDeepSeekBlockedError(error)) {\n const fallbackMessage = openClawBlockedFallbackText(usageMessages);\n const usage = usageWithFallback(null, usageMessages, fallbackMessage);\n recordUsage(user.userId, usage.totalTokens);\n logger.warn(\n { statusCode: error?.statusCode, code: error?.code },\n 'OpenClaw DeepSeek proxy stream blocked; returning local fallback message',\n );\n writeOpenAiSse(res, {\n ...baseChunk,\n choices: [{ index: 0, delta: { content: fallbackMessage }, finish_reason: null }],\n });\n writeOpenAiSse(res, {\n ...baseChunk,\n choices: [{ index: 0, delta: {}, finish_reason: 'stop' }],\n });\n writeOpenAiSse(res, {\n ...baseChunk,\n choices: [],\n usage: openAiUsagePayload(usage, user, budget),\n });\n res.write('data: [DONE]\\n\\n');\n return;\n }\n logger.error(\n { error: String(error), code: error?.code, statusCode: error?.statusCode },\n 'OpenClaw DeepSeek proxy stream failed',\n );\n writeOpenAiSse(res, {\n ...baseChunk,\n choices: [],\n error: {\n message: publicAiErrorMessage(error),\n code: error?.code || 'OPENCLAW_DEEPSEEK_PROXY_ERROR',\n },\n });\n res.write('data: [DONE]\\n\\n');\n } finally {\n res.end();\n }\n }),\n);\n\napp.post(\n '/codex-deepseek/v1/responses',\n asyncHandler(async (req, res) => {\n const bearer = bearerToken(req);\n if (!deepseekApiKey || bearer !== deepseekApiKey) {\n res.status(401).json({\n error: { message: 'Unauthorized Codex DeepSeek adapter request' },\n });\n return;\n }\n\n const prompt = codexResponsesPrompt(req.body);\n if (!prompt) {\n res.status(400).json({ error: { message: 'Responses input is required' } });\n return;\n }\n\n const model = String(req.body?.model || deepseekModel);\n const responseId = `resp_${crypto.randomUUID().replaceAll('-', '')}`;\n const itemId = `msg_${crypto.randomUUID().replaceAll('-', '')}`;\n const wantsStream =\n req.body?.stream !== false ||\n String(req.get('accept') || '').includes('text/event-stream');\n const messages = [{ role: 'user', content: prompt }];\n\n if (!wantsStream) {\n const result = await callDeepSeek(messages);\n res.json(\n responsesPayload({\n responseId,\n itemId,\n status: 'completed',\n model,\n text: result.message,\n usage: result.usage,\n }),\n );\n return;\n }\n\n res.status(200);\n res.set({\n 'Content-Type': 'text/event-stream; charset=utf-8',\n 'Cache-Control': 'no-cache, no-transform',\n Connection: 'keep-alive',\n 'X-Accel-Buffering': 'no',\n });\n res.flushHeaders?.();\n\n writeResponsesEvent(res, 'response.created', {\n type: 'response.created',\n response: responsesPayload({\n responseId,\n itemId,\n status: 'in_progress',\n model,\n }),\n });\n writeResponsesEvent(res, 'response.output_item.added', {\n type: 'response.output_item.added',\n output_index: 0,\n item: {\n id: itemId,\n type: 'message',\n role: 'assistant',\n status: 'in_progress',\n content: [],\n },\n });\n writeResponsesEvent(res, 'response.content_part.added', {\n type: 'response.content_part.added',\n item_id: itemId,\n output_index: 0,\n content_index: 0,\n part: { type: 'output_text', text: '' },\n });\n\n let text = '';\n try {\n const result = await callDeepSeekStream(messages, {\n onToken: (delta) => {\n text += delta;\n writeResponsesEvent(res, 'response.output_text.delta', {\n type: 'response.output_text.delta',\n item_id: itemId,\n output_index: 0,\n content_index: 0,\n delta,\n });\n },\n });\n text = result.message || text;\n writeResponsesEvent(res, 'response.output_text.done', {\n type: 'response.output_text.done',\n item_id: itemId,\n output_index: 0,\n content_index: 0,\n text,\n });\n writeResponsesEvent(res, 'response.content_part.done', {\n type: 'response.content_part.done',\n item_id: itemId,\n output_index: 0,\n content_index: 0,\n part: { type: 'output_text', text },\n });\n writeResponsesEvent(res, 'response.output_item.done', {\n type: 'response.output_item.done',\n output_index: 0,\n item: {\n id: itemId,\n type: 'message',\n role: 'assistant',\n status: 'completed',\n content: [{ type: 'output_text', text }],\n },\n });\n writeResponsesEvent(res, 'response.completed', {\n type: 'response.completed',\n response: responsesPayload({\n responseId,\n itemId,\n status: 'completed',\n model,\n text,\n usage: result.usage,\n }),\n });\n res.end();\n } catch (error) {\n writeResponsesEvent(res, 'response.failed', {\n type: 'response.failed',\n response: responsesPayload({\n responseId,\n itemId,\n status: 'failed',\n model,\n text,\n error,\n }),\n });\n res.end();\n }\n }),\n);\n\napp.use(\n '/api/openclaw/runtime/files',\n express.static(openClawRuntimeUpdatesDir, {\n fallthrough: false,\n immutable: true,\n maxAge: '7d',\n setHeaders(res) {\n res.setHeader('X-Content-Type-Options', 'nosniff');\n },\n }),\n);\n\napp.get(\n '/api/openclaw/runtime/manifest',\n asyncHandler(async (req, res) => {\n const platform = readText(req.query.platform);\n const currentVersion = readText(req.query.currentVersion);\n const appVersion = readText(req.query.appVersion);\n\n let manifest;\n try {\n manifest = await readJsonFile(openClawRuntimeManifestPath);\n } catch (error) {\n logger.info(\n { manifestPath: openClawRuntimeManifestPath, error: String(error) },\n 'OpenClaw runtime manifest unavailable',\n );\n return jsonResponse(res, 200, {\n success: true,\n schema: 1,\n updateAvailable: false,\n reason: 'manifest-unavailable',\n platform,\n currentVersion,\n appVersion,\n });\n }\n\n const latest = manifest.latest && typeof manifest.latest === 'object'\n ? manifest.latest\n : manifest;\n const platforms = latest.platforms && typeof latest.platforms === 'object'\n ? latest.platforms\n : {};\n const platformRelease = platform ? platforms[platform] : null;\n const version = readText(latest.version || manifest.version);\n\n if (!platform || !platformRelease || typeof platformRelease !== 'object') {\n return jsonResponse(res, 200, {\n success: true,\n schema: Number(manifest.schema || 1),\n channel: readText(manifest.channel || latest.channel || 'stable'),\n updateAvailable: false,\n reason: platform ? 'platform-unavailable' : 'platform-required',\n platform,\n currentVersion,\n appVersion,\n latestVersion: version,\n });\n }\n\n if (platform.startsWith('macos-') && !openClawRuntimeEnableMacosUpdates) {\n return jsonResponse(res, 200, {\n success: true,\n schema: Number(manifest.schema || 1),\n channel: readText(manifest.channel || latest.channel || 'stable'),\n updateAvailable: false,\n reason: 'macos-runtime-updates-disabled',\n platform,\n currentVersion,\n appVersion,\n latestVersion: version,\n });\n }\n\n const downloadUrl = absolutizeOpenClawRuntimeUrl(\n req,\n platformRelease.archiveUrl || platformRelease.url || platformRelease.fileName,\n );\n const release = {\n version,\n channel: readText(manifest.channel || latest.channel || 'stable'),\n minAppVersion: readText(latest.minAppVersion || platformRelease.minAppVersion),\n notes: readText(latest.notes || platformRelease.notes),\n nodeExecutable: readText(\n platformRelease.nodeExecutable || latest.nodeExecutable || 'node/bin/node',\n ),\n cliEntrypoint: readText(\n platformRelease.cliEntrypoint || latest.cliEntrypoint || 'openclaw/openclaw.mjs',\n ),\n defaultPort: Number(latest.defaultPort || manifest.defaultPort || 18789),\n defaultModel: readText(latest.defaultModel || manifest.defaultModel || 'openclaw/default'),\n defaultModelOverride: readText(latest.defaultModelOverride || manifest.defaultModelOverride),\n gatewayArgs: Array.isArray(latest.gatewayArgs)\n ? latest.gatewayArgs.map((item) => String(item))\n : ['gateway', '--port', '{port}', '--force'],\n archiveUrl: downloadUrl,\n sha256: readText(platformRelease.sha256),\n size: Number(platformRelease.size || 0),\n };\n\n jsonResponse(res, 200, {\n success: true,\n schema: Number(manifest.schema || 1),\n updateAvailable: Boolean(version && downloadUrl && version !== currentVersion),\n platform,\n currentVersion,\n appVersion,\n latest: release,\n generatedAt: readText(manifest.generatedAt || latest.generatedAt),\n });\n }),\n);\n\napp.get('/health', (_req, res) => {\n const codexRuntime = createCodexDeepSeekRuntime();\n const libreChatRuntime = createLibreChatAgentRuntime();\n const openClawRuntime = createOpenClawRuntime();\n jsonResponse(res, 200, {\n status: 'ok',\n service: 'dacheng-ai-backend',\n provider: libreChatRuntime.enabled ? libreChatRuntime.provider : 'deepseek',\n model: libreChatRuntime.enabled ? libreChatRuntime.model : deepseekModel,\n deepseekModel,\n deepseekAvailableModels,\n libreChatAgentChatEnabled: libreChatRuntime.enabled,\n libreChatAgentProvider: libreChatRuntime.provider,\n libreChatAgentReason: libreChatRuntime.enabled ? undefined : libreChatRuntime.reason,\n codexSdkAvailable: true,\n codexSdkChatEnabled: codexRuntime.enabled,\n codexSdkProvider: codexRuntime.provider,\n openClawAgentChatEnabled: openClawRuntime.enabled,\n openClawAgentProvider: openClawRuntime.provider,\n openClawAgentReason: openClawRuntime.enabled ? undefined : openClawRuntime.reason,\n requireMemberForAi,\n memberMonthlyLimit,\n freeMonthlyLimit,\n deepseekMaxCompletionTokens,\n openClawRuntimeUpdatesDir,\n openClawRuntimeManifestConfigured: fs.existsSync(openClawRuntimeManifestPath),\n cbetaApiRoot,\n timestamp: nowIso(),\n });\n});\n\napp.get(\n '/api/miniapps/registry',\n asyncHandler(async (req, res) => {\n const user = await resolveUser(req, { username: req.query.username });\n const official = officialMiniAppRegistry(req);\n const store = await readMiniAppStore();\n const visibleSandbox = store.apps.filter((appRecord) => {\n if (appRecord.reviewStatus === 'approved' && appRecord.source === 'marketplace') {\n return true;\n }\n return appRecord.ownerId === user.userId;\n });\n const bots = [\n ...official.bots,\n ...visibleSandbox.map((record) => miniAppRecordToBot(record)),\n ];\n const miniApps = [\n ...official.miniApps,\n ...visibleSandbox.map((record) => miniAppRecordToManifest(req, record)),\n ];\n const registry = {\n schemaVersion: 1,\n hostApiVersion: miniAppHostApiVersion,\n bots,\n miniApps,\n updatedAt: nowIso(),\n };\n registry.signature = sha256(JSON.stringify({ bots, miniApps }));\n jsonResponse(res, 200, { success: true, registry });\n }),\n);\n\napp.get(\n '/api/miniapps/dev/:miniAppId/index.html',\n asyncHandler(async (req, res) => {\n const miniAppId = safeMiniAppId(req.params.miniAppId);\n const store = await readMiniAppStore();\n const record = store.apps.find((appRecord) => appRecord.miniAppId === miniAppId);\n if (!record) {\n res.status(404).type('text/html').send('<h1>Mini app not found</h1>');\n return;\n }\n res.setHeader('Content-Type', 'text/html; charset=utf-8');\n res.setHeader('Cache-Control', 'no-store');\n res.send(record.sourceHtml || generatedMiniAppHtml(record));\n }),\n);\n\napp.post(\n '/api/miniapps/dev/create',\n asyncHandler(async (req, res) => {\n const user = await resolveUser(req, req.body || {});\n const title = safeUserText(req.body?.title, '个人小程序').slice(0, 80);\n const subtitle = safeUserText(req.body?.subtitle, '个人沙箱小程序').slice(0, 120);\n const permissions = sanitizeMiniAppPermissions(req.body?.permissions);\n const prompt = safeUserText(req.body?.prompt || title);\n const idSeed = crypto.randomUUID().replaceAll('-', '').slice(0, 12);\n const miniAppId = `sandbox.${idSeed}`;\n const botId = `bot.${miniAppId}`;\n const sourceHtml = generatedMiniAppHtml({ title, prompt });\n const scan = miniAppScan(permissions, sourceHtml);\n const createdAt = nowIso();\n const record = {\n ownerId: user.userId,\n miniAppId,\n botId,\n title,\n subtitle,\n initials: title.slice(0, 1) || '小',\n icon: 'code',\n avatarColor: '#3D8BFF',\n greeting: '你好,我是这个个人沙箱小程序的机器人。',\n inputHint: '输入消息,或打开小程序面板',\n version: '0.0.1',\n permissions,\n surfaces: ['chatPanel'],\n theme: 'telegramDark',\n source: 'sandbox',\n reviewStatus: 'sandbox',\n sourceHtml,\n scan,\n createdAt,\n updatedAt: createdAt,\n };\n const store = await readMiniAppStore();\n store.apps.push(record);\n await writeMiniAppStore(store);\n jsonResponse(res, 200, {\n success: true,\n miniApp: miniAppRecordToManifest(req, record),\n bot: miniAppRecordToBot(record),\n scan,\n });\n }),\n);\n\napp.post(\n '/api/miniapps/dev/:miniAppId/version',\n asyncHandler(async (req, res) => {\n const user = await resolveUser(req, req.body || {});\n const miniAppId = safeMiniAppId(req.params.miniAppId);\n const store = await readMiniAppStore();\n const index = store.apps.findIndex((appRecord) => appRecord.miniAppId === miniAppId);\n if (index < 0) {\n return jsonResponse(res, 404, { success: false, message: 'mini app not found' });\n }\n const record = store.apps[index];\n if (record.ownerId !== user.userId) {\n return jsonResponse(res, 403, { success: false, message: 'not your mini app' });\n }\n const title = safeUserText(req.body?.title, record.title).slice(0, 80);\n const subtitle = safeUserText(req.body?.subtitle, record.subtitle).slice(0, 120);\n const sourceHtml =\n safeUserText(req.body?.sourceHtml) ||\n generatedMiniAppHtml({ title, prompt: safeUserText(req.body?.prompt || title) });\n const permissions = sanitizeMiniAppPermissions(req.body?.permissions || record.permissions);\n const scan = miniAppScan(permissions, sourceHtml);\n store.apps[index] = {\n ...record,\n title,\n subtitle,\n sourceHtml,\n permissions,\n scan,\n reviewStatus: 'sandbox',\n version: safeUserText(req.body?.version, record.version || '0.0.1'),\n updatedAt: nowIso(),\n };\n await writeMiniAppStore(store);\n jsonResponse(res, 200, {\n success: true,\n miniApp: miniAppRecordToManifest(req, store.apps[index]),\n bot: miniAppRecordToBot(store.apps[index]),\n scan,\n });\n }),\n);\n\napp.post(\n '/api/miniapps/:miniAppId/submit-review',\n asyncHandler(async (req, res) => {\n const user = await resolveUser(req, req.body || {});\n const miniAppId = safeMiniAppId(req.params.miniAppId);\n const store = await readMiniAppStore();\n const index = store.apps.findIndex((appRecord) => appRecord.miniAppId === miniAppId);\n if (index < 0) {\n return jsonResponse(res, 404, { success: false, message: 'mini app not found' });\n }\n const record = store.apps[index];\n if (record.ownerId !== user.userId) {\n return jsonResponse(res, 403, { success: false, message: 'not your mini app' });\n }\n const scan = miniAppScan(record.permissions || [], record.sourceHtml || '');\n if (!scan.passed) {\n return jsonResponse(res, 400, {\n success: false,\n message: 'security scan failed',\n scan,\n });\n }\n store.apps[index] = {\n ...record,\n scan,\n reviewStatus: 'pending_review',\n updatedAt: nowIso(),\n };\n await writeMiniAppStore(store);\n jsonResponse(res, 200, {\n success: true,\n miniApp: miniAppRecordToManifest(req, store.apps[index]),\n scan,\n });\n }),\n);\n\napp.post(\n '/api/botfather/generate-miniapp',\n asyncHandler(async (req, res) => {\n const user = await resolveUser(req, req.body || {});\n const prompt = safeUserText(req.body?.prompt);\n if (!prompt) {\n return jsonResponse(res, 400, { success: false, message: 'prompt is required' });\n }\n const title = titleFromPromptForMiniApp(prompt);\n const subtitle = '机器人之父生成的个人沙箱小程序';\n const permissions = sanitizeMiniAppPermissions(req.body?.permissions);\n const generated = await generateBotFatherMiniAppHtml({ title, prompt, user });\n let sourceHtml = generated.sourceHtml;\n let generation = generated.generation;\n let scan = miniAppScan(permissions, sourceHtml);\n if (!scan.passed) {\n sourceHtml = generatedMiniAppHtml({ title, prompt });\n generation = {\n provider: 'template',\n fallbackReason: 'ai_output_failed_security_scan',\n };\n scan = miniAppScan(permissions, sourceHtml);\n }\n const idSeed = crypto.randomUUID().replaceAll('-', '').slice(0, 12);\n const miniAppId = `sandbox.${idSeed}`;\n const createdAt = nowIso();\n const record = {\n ownerId: user.userId,\n miniAppId,\n botId: `bot.${miniAppId}`,\n title,\n subtitle,\n initials: title.slice(0, 1) || '小',\n icon: 'code',\n avatarColor: '#3D8BFF',\n greeting: `我是「${title}」的机器人。`,\n inputHint: '继续描述修改需求,或打开小程序面板',\n version: '0.0.1',\n permissions,\n surfaces: ['chatPanel'],\n theme: 'telegramDark',\n source: 'sandbox',\n reviewStatus: 'sandbox',\n sourceHtml,\n scan,\n generation,\n prompt,\n createdAt,\n updatedAt: createdAt,\n };\n const store = await readMiniAppStore();\n store.apps.push(record);\n await writeMiniAppStore(store);\n jsonResponse(res, 200, {\n success: true,\n miniApp: miniAppRecordToManifest(req, record),\n bot: miniAppRecordToBot(record),\n scan,\n generation,\n actions: ['openMiniApp', 'continueEditing', 'submitReview'],\n });\n }),\n);\n\napp.get('/api/ai/models', (_req, res) => {\n jsonResponse(res, 200, {\n success: true,\n provider: 'deepseek',\n defaultModel: deepseekModel,\n models: deepseekAvailableModels.map((model) => ({\n id: model,\n label: model === 'deepseek-reasoner' ? 'DeepSeek Reasoner' : 'DeepSeek Chat',\n })),\n });\n});\napp.get(\n '/api/ai/quota',\n asyncHandler(async (req, res) => {\n const user = await resolveUser(req, { username: req.query.username });\n const budget = enforceTokenBudget(user, 0);\n const usage = usageFor(user.userId);\n jsonResponse(res, 200, {\n success: true,\n isMember: user.isMember,\n monthlyLimit: budget.limit,\n usedTokens: usage.used,\n remainingTokens: Math.max(0, budget.limit - usage.used),\n month: usage.month,\n });\n }),\n);\n\napp.post(\n '/api/agent/chat',\n asyncHandler(async (req, res) => {\n const message = safeUserText(req.body?.message);\n if (!message) {\n return jsonResponse(res, 400, { success: false, message: 'message is required' });\n }\n\n const user = await resolveUser(req, req.body || {});\n const promptEstimate = estimateTokens(message);\n const estimated = promptEstimate + 600;\n const budget = enforceTokenBudget(user, estimated);\n const maxCompletionTokens = completionBudgetFor(budget, promptEstimate);\n const createdAt = nowIso();\n const conversationId = safeUserText(req.body?.conversationId) || crypto.randomUUID();\n const mode = normalizeAgentMode(req.body?.mode);\n const stream = req.body?.stream === true;\n let conversation = statements.getConversation.get(conversationId, user.userId);\n const isNewConversation = !conversation;\n\n if (isNewConversation) {\n conversation = { id: conversationId, user_id: user.userId, title: titleFromMessage(message) };\n statements.insertConversation.run({\n id: conversationId,\n userId: user.userId,\n title: conversation.title,\n provider: 'agent',\n model: openClawAgentId,\n createdAt,\n updatedAt: createdAt,\n });\n }\n\n const messageId = safeUserText(req.body?.messageId) || crypto.randomUUID();\n statements.insertMessage.run({\n id: messageId,\n conversationId,\n role: 'user',\n content: message,\n promptTokens: 0,\n completionTokens: 0,\n totalTokens: 0,\n createdAt,\n });\n\n const runId = `run_${crypto.randomUUID().replaceAll('-', '')}`;\n const sessionKey = openClawSessionKey(user, conversationId);\n const openClawRuntime = createOpenClawRuntime();\n statements.insertAgentRun.run({\n id: runId,\n userId: user.userId,\n conversationId,\n messageId,\n openClawRunId: null,\n openClawSessionKey: sessionKey,\n status: stream ? 'queued' : 'running',\n mode,\n provider: openClawRuntime.enabled ? openClawRuntime.provider : 'dacheng-ai-fallback',\n model: openClawRuntime.enabled ? openClawAgentId : deepseekModel,\n startedAt: createdAt,\n });\n\n if (stream) {\n return jsonResponse(res, 200, {\n success: true,\n runId,\n conversationId,\n streamUrl: `/api/agent/runs/${runId}/events`,\n });\n }\n\n const controller = new AbortController();\n activeAgentRuns.set(runId, controller);\n try {\n const skillResult = await runResourceFinderSkill({ message, user });\n const skillContext = resourceContextMessage(skillResult);\n const historyRows = statements.listMessages.all(conversationId).slice(-14);\n const modelMessages = [\n { role: 'system', content: systemPrompt },\n ...(skillContext ? [{ role: 'system', content: skillContext }] : []),\n ...normalizeMessages(historyRows),\n ];\n\n const aiResult = await runAgentModel({\n messages: modelMessages,\n user,\n conversationId,\n mode,\n signal: controller.signal,\n maxCompletionTokens,\n });\n const usage = {\n promptTokens: aiResult.usage?.promptTokens || estimateTokens(JSON.stringify(modelMessages)),\n completionTokens: aiResult.usage?.completionTokens || estimateTokens(aiResult.message),\n totalTokens:\n aiResult.usage?.totalTokens ||\n estimateTokens(JSON.stringify(modelMessages)) + estimateTokens(aiResult.message),\n };\n recordUsage(user.userId, usage.totalTokens);\n const answeredAt = nowIso();\n const assistantMessageId = crypto.randomUUID();\n statements.insertMessage.run({\n id: assistantMessageId,\n conversationId,\n role: 'assistant',\n content: aiResult.message,\n promptTokens: usage.promptTokens,\n completionTokens: usage.completionTokens,\n totalTokens: usage.totalTokens,\n createdAt: answeredAt,\n });\n statements.updateConversation.run({\n id: conversationId,\n userId: user.userId,\n title: isNewConversation ? titleFromMessage(message) : conversation.title,\n updatedAt: answeredAt,\n });\n updateAgentRunStatus({ id: runId, user_id: user.userId }, 'completed', {\n openClawRunId: aiResult.runId,\n inputTokens: usage.promptTokens,\n outputTokens: usage.completionTokens,\n });\n const action = determineClientAction(skillResult, message);\n return jsonResponse(res, 200, {\n success: true,\n runId,\n conversationId,\n messageId: assistantMessageId,\n provider: aiResult.provider,\n model: aiResult.model,\n message: aiResult.message,\n usage: agentUsagePayload(usage, user, budget),\n ...(action ? { clientAction: action } : {}),\n });\n } catch (error) {\n const status = error.name === 'AbortError' ? 'cancelled' : 'failed';\n updateAgentRunStatus({ id: runId, user_id: user.userId }, status, {\n errorCode: error.name || 'AGENT_RUN_FAILED',\n errorMessage: publicAiErrorMessage(error),\n });\n if (status === 'cancelled') {\n return jsonResponse(res, 499, { success: false, runId, status: 'cancelled' });\n }\n throw error;\n } finally {\n activeAgentRuns.delete(runId);\n }\n }),\n);\n\napp.get(\n '/api/agent/runs/:runId/events',\n asyncHandler(async (req, res) => {\n const user = await resolveUser(req, {});\n const run = statements.getAgentRun.get(req.params.runId, user.userId);\n if (!run) {\n return jsonResponse(res, 404, { success: false, message: 'run not found' });\n }\n if (run.status === 'completed' || run.status === 'failed' || run.status === 'cancelled') {\n return jsonResponse(res, 409, { success: false, message: `run is already ${run.status}` });\n }\n\n sseHeaders(res);\n const controller = new AbortController();\n activeAgentRuns.set(run.id, controller);\n updateAgentRunStatus(run, 'running');\n writeSse(res, 'run.started', { runId: run.id, conversationId: run.conversation_id });\n\n try {\n let streamedText = '';\n const modelMessages = buildAgentMessages(run.conversation_id);\n const lastUserMsg = [...modelMessages].reverse().find(m => m.role === 'user');\n const userMsgText = lastUserMsg ? lastUserMsg.content : '';\n const skillResult = userMsgText ? await runResourceFinderSkill({\n message: userMsgText,\n user,\n onStep: (step) => writeSse(res, 'tool.call.completed', {\n tool: 'agent.step',\n displayName: step.title || 'Agent step',\n summary: step.message || '',\n }),\n }) : null;\n const skillContext = resourceContextMessage(skillResult);\n const finalMessages = skillContext ? [\n modelMessages[0],\n { role: 'system', content: skillContext },\n ...modelMessages.slice(1)\n ] : modelMessages;\n\n const promptEstimate = estimateTokens(JSON.stringify(finalMessages));\n const estimated = promptEstimate + 600;\n const budget = enforceTokenBudget(user, estimated);\n const maxCompletionTokens = completionBudgetFor(budget, promptEstimate);\n const aiResult = await runAgentModel({\n messages: finalMessages,\n user,\n conversationId: run.conversation_id,\n mode: run.mode,\n signal: controller.signal,\n maxCompletionTokens,\n callbacks: {\n onStep: (step) => writeSse(res, 'tool.call.completed', {\n tool: 'agent.step',\n displayName: step.title || 'Agent step',\n summary: step.message || '',\n }),\n onToken: (delta) => {\n streamedText += delta;\n writeSse(res, 'assistant.delta', { text: delta });\n },\n },\n });\n const message = aiResult.message || streamedText.trim();\n const usage = {\n promptTokens: aiResult.usage?.promptTokens || estimateTokens(JSON.stringify(finalMessages)),\n completionTokens: aiResult.usage?.completionTokens || estimateTokens(message),\n totalTokens:\n aiResult.usage?.totalTokens || estimateTokens(JSON.stringify(finalMessages)) + estimateTokens(message),\n };\n recordUsage(user.userId, usage.totalTokens);\n const assistantMessageId = crypto.randomUUID();\n const answeredAt = nowIso();\n statements.insertMessage.run({\n id: assistantMessageId,\n conversationId: run.conversation_id,\n role: 'assistant',\n content: message,\n promptTokens: usage.promptTokens,\n completionTokens: usage.completionTokens,\n totalTokens: usage.totalTokens,\n createdAt: answeredAt,\n });\n statements.updateConversation.run({\n id: run.conversation_id,\n userId: user.userId,\n title: titleFromMessage(message),\n updatedAt: answeredAt,\n });\n updateAgentRunStatus(run, 'completed', {\n openClawRunId: aiResult.runId,\n inputTokens: usage.promptTokens,\n outputTokens: usage.completionTokens,\n });\n const action = determineClientAction(skillResult, userMsgText);\n writeSse(res, 'assistant.message', { messageId: assistantMessageId, content: message });\n writeSse(res, 'run.completed', {\n usage: agentUsagePayload(usage, user, budget),\n ...(action ? { clientAction: action } : {}),\n });\n res.end();\n } catch (error) {\n const status = error.name === 'AbortError' ? 'cancelled' : 'failed';\n updateAgentRunStatus(run, status, {\n errorCode: error.name || 'AGENT_RUN_FAILED',\n errorMessage: publicAiErrorMessage(error),\n });\n writeSse(res, status === 'cancelled' ? 'run.cancelled' : 'run.failed', {\n runId: run.id,\n message: publicAiErrorMessage(error),\n });\n res.end();\n } finally {\n activeAgentRuns.delete(run.id);\n }\n }),\n);\n\napp.post(\n '/api/agent/runs/:runId/cancel',\n asyncHandler(async (req, res) => {\n const user = await resolveUser(req, req.body || {});\n const run = statements.getAgentRun.get(req.params.runId, user.userId);\n if (!run) {\n return jsonResponse(res, 404, { success: false, message: 'run not found' });\n }\n const controller = activeAgentRuns.get(run.id);\n if (controller) controller.abort();\n updateAgentRunStatus(run, 'cancelled');\n return jsonResponse(res, 200, { success: true, runId: run.id, status: 'cancelled' });\n }),\n);\n\napp.post(\n '/api/agent/messages/:messageId/feedback',\n asyncHandler(async (req, res) => {\n const user = await resolveUser(req, req.body || {});\n const rating = safeUserText(req.body?.rating);\n if (!['up', 'down'].includes(rating)) {\n return jsonResponse(res, 400, { success: false, message: 'rating must be up or down' });\n }\n statements.insertAgentFeedback.run({\n id: crypto.randomUUID(),\n messageId: req.params.messageId,\n userId: user.userId,\n rating,\n reason: safeUserText(req.body?.reason),\n comment: safeUserText(req.body?.comment),\n createdAt: nowIso(),\n });\n return jsonResponse(res, 200, { success: true });\n }),\n);\n\napp.post(\n '/api/ai/chat',\n asyncHandler(async (req, res) => {\n const message = safeUserText(req.body?.message);\n if (!message) {\n return jsonResponse(res, 400, { success: false, message: 'message is required' });\n }\n\n const user = await resolveUser(req, req.body || {});\n const promptEstimate = estimateTokens(message);\n const estimated = promptEstimate + 600;\n const budget = enforceTokenBudget(user, estimated);\n const maxCompletionTokens = completionBudgetFor(budget, promptEstimate);\n const createdAt = nowIso();\n const conversationId = safeUserText(req.body?.conversationId) || crypto.randomUUID();\n const requestedModel = normalizeDeepSeekModelName(req.body?.model);\n const clientContext = clientContextMessage(req.body?.client);\n let conversation = statements.getConversation.get(conversationId, user.userId);\n const isNewConversation = !conversation;\n\n if (isNewConversation) {\n conversation = {\n id: conversationId,\n user_id: user.userId,\n title: titleFromMessage(message),\n };\n statements.insertConversation.run({\n id: conversationId,\n userId: user.userId,\n title: conversation.title,\n provider: 'deepseek',\n model: requestedModel,\n createdAt,\n updatedAt: createdAt,\n });\n }\n\n statements.insertMessage.run({\n id: crypto.randomUUID(),\n conversationId,\n role: 'user',\n content: message,\n promptTokens: 0,\n completionTokens: 0,\n totalTokens: 0,\n createdAt,\n });\n\n const libreChatRuntime = createLibreChatAgentRuntime();\n const skillResult = await runResourceFinderSkill({ message, user });\n const skillContext = resourceContextMessage(skillResult);\n const historyRows = statements.listMessages.all(conversationId).slice(-14);\n const modelMessages = [\n { role: 'system', content: systemPrompt },\n ...(clientContext ? [{ role: 'system', content: clientContext }] : []),\n ...(skillContext ? [{ role: 'system', content: skillContext }] : []),\n ...normalizeMessages(historyRows),\n ];\n\n const openClawRuntime = createOpenClawRuntime();\n const codexRuntime = createCodexDeepSeekRuntime();\n let provider = openClawRuntime.enabled\n ? openClawRuntime.provider\n : libreChatRuntime.enabled\n ? libreChatRuntime.provider\n : 'deepseek';\n let responseModel = openClawRuntime.enabled\n ? openClawRuntime.model\n : libreChatRuntime.enabled\n ? libreChatRuntime.model\n : requestedModel;\n let aiResult;\n try {\n if (openClawRuntime.enabled) {\n aiResult = await callOpenClawAgent({\n messages: modelMessages,\n user,\n conversationId,\n mode: normalizeAgentMode(req.body?.mode),\n });\n provider = aiResult.provider || provider;\n responseModel = aiResult.model || responseModel;\n } else if (libreChatRuntime.enabled) {\n aiResult = await callLibreChatAgent(modelMessages);\n } else if (codexRuntime.enabled) {\n provider = codexRuntime.provider;\n aiResult = await callCodexSdkDeepSeek(modelMessages);\n } else {\n aiResult = await callDeepSeek(modelMessages, { model: requestedModel, maxCompletionTokens });\n }\n } catch (error) {\n if (libreChatRuntime.enabled) {\n logger.warn({ error: String(error) }, 'LibreChat Agent chat failed, falling back to Codex SDK or DeepSeek direct');\n provider = codexRuntime.enabled ? codexRuntime.provider : 'deepseek';\n responseModel = requestedModel;\n if (codexRuntime.enabled) {\n try {\n aiResult = await callCodexSdkDeepSeek(modelMessages);\n } catch (codexError) {\n if (isDeepSeekQuotaError(codexError)) throw codexError;\n logger.warn({ error: String(codexError) }, 'Codex SDK chat failed, falling back to DeepSeek direct');\n provider = 'deepseek';\n aiResult = await callDeepSeek(modelMessages, { model: requestedModel, maxCompletionTokens });\n }\n } else {\n aiResult = await callDeepSeek(modelMessages, { model: requestedModel, maxCompletionTokens });\n }\n } else {\n if (!codexRuntime.enabled || isDeepSeekQuotaError(error)) throw error;\n logger.warn({ error: String(error) }, 'Codex SDK chat failed, falling back to DeepSeek direct');\n provider = 'deepseek';\n responseModel = requestedModel;\n aiResult = await callDeepSeek(modelMessages, { model: requestedModel, maxCompletionTokens });\n }\n }\n const usage = {\n promptTokens: aiResult.usage.promptTokens || estimateTokens(JSON.stringify(modelMessages)),\n completionTokens: aiResult.usage.completionTokens || estimateTokens(aiResult.message),\n totalTokens:\n aiResult.usage.totalTokens ||\n estimateTokens(JSON.stringify(modelMessages)) + estimateTokens(aiResult.message),\n };\n recordUsage(user.userId, usage.totalTokens);\n\n const answeredAt = nowIso();\n statements.insertMessage.run({\n id: crypto.randomUUID(),\n conversationId,\n role: 'assistant',\n content: aiResult.message,\n promptTokens: usage.promptTokens,\n completionTokens: usage.completionTokens,\n totalTokens: usage.totalTokens,\n createdAt: answeredAt,\n });\n statements.updateConversation.run({\n id: conversationId,\n userId: user.userId,\n title: isNewConversation ? titleFromMessage(message) : conversation.title,\n updatedAt: answeredAt,\n });\n\n const latestUsage = usageFor(user.userId);\n const monthlyLimit = budget.limit;\n const action = determineClientAction(skillResult, message);\n jsonResponse(res, 200, {\n success: true,\n conversationId,\n provider,\n model: responseModel,\n message: aiResult.message,\n usage: {\n ...usage,\n monthlyLimit,\n remainingTokens: Math.max(0, monthlyLimit - latestUsage.used),\n },\n ...(action ? { clientAction: action } : {}),\n });\n }),\n);\n\napp.post('/api/ai/chat/stream', async (req, res) => {\n sseHeaders(res);\n\n try {\n const message = safeUserText(req.body?.message);\n if (!message) {\n writeSse(res, 'error', { message: 'message is required' });\n res.end();\n return;\n }\n\n const user = await resolveUser(req, req.body || {});\n const promptEstimate = estimateTokens(message);\n const estimated = promptEstimate + 600;\n const budget = enforceTokenBudget(user, estimated);\n const maxCompletionTokens = completionBudgetFor(budget, promptEstimate);\n const createdAt = nowIso();\n const conversationId = safeUserText(req.body?.conversationId) || crypto.randomUUID();\n const requestedModel = normalizeDeepSeekModelName(req.body?.model);\n const clientContext = clientContextMessage(req.body?.client);\n let conversation = statements.getConversation.get(conversationId, user.userId);\n const isNewConversation = !conversation;\n\n if (isNewConversation) {\n conversation = {\n id: conversationId,\n user_id: user.userId,\n title: titleFromMessage(message),\n };\n statements.insertConversation.run({\n id: conversationId,\n userId: user.userId,\n title: conversation.title,\n provider: 'deepseek',\n model: requestedModel,\n createdAt,\n updatedAt: createdAt,\n });\n }\n\n statements.insertMessage.run({\n id: crypto.randomUUID(),\n conversationId,\n role: 'user',\n content: message,\n promptTokens: 0,\n completionTokens: 0,\n totalTokens: 0,\n createdAt,\n });\n\n const codexRuntime = createCodexDeepSeekRuntime();\n const libreChatRuntime = createLibreChatAgentRuntime();\n const initialOpenClawRuntime = createOpenClawRuntime();\n let provider = initialOpenClawRuntime.enabled\n ? initialOpenClawRuntime.provider\n : libreChatRuntime.enabled\n ? libreChatRuntime.provider\n : codexRuntime.enabled\n ? codexRuntime.provider\n : 'deepseek';\n let responseModel = initialOpenClawRuntime.enabled\n ? initialOpenClawRuntime.model\n : libreChatRuntime.enabled\n ? libreChatRuntime.model\n : requestedModel;\n writeSse(res, 'meta', {\n conversationId,\n provider,\n model: responseModel,\n });\n const skillResult = await runResourceFinderSkill({\n message,\n user,\n onStep: (step) => writeSse(res, 'step', step),\n });\n const skillContext = resourceContextMessage(skillResult);\n const historyRows = statements.listMessages.all(conversationId).slice(-14);\n const modelMessages = [\n { role: 'system', content: systemPrompt },\n ...(clientContext ? [{ role: 'system', content: clientContext }] : []),\n ...(skillContext ? [{ role: 'system', content: skillContext }] : []),\n ...normalizeMessages(historyRows),\n ];\n\n let aiResult;\n const openClawRuntime = initialOpenClawRuntime;\n try {\n if (openClawRuntime.enabled) {\n aiResult = await callOpenClawAgent({\n messages: modelMessages,\n user,\n conversationId,\n mode: normalizeAgentMode(req.body?.mode),\n });\n provider = aiResult.provider || provider;\n responseModel = aiResult.model || responseModel;\n writeSse(res, 'delta', { text: aiResult.message });\n } else if (libreChatRuntime.enabled) {\n aiResult = await callLibreChatAgentStream(modelMessages, {\n onToken: (token) => writeSse(res, 'delta', { text: token }),\n });\n } else if (codexRuntime.enabled) {\n aiResult = await callCodexSdkDeepSeek(modelMessages, {\n onToken: (token) => writeSse(res, 'delta', { text: token }),\n onStep: (step) => writeSse(res, 'step', step),\n });\n } else {\n aiResult = await callDeepSeekStream(modelMessages, {\n model: requestedModel,\n maxCompletionTokens,\n onToken: (token) => writeSse(res, 'delta', { text: token }),\n });\n }\n } catch (error) {\n if (libreChatRuntime.enabled) {\n logger.warn({ error: String(error) }, 'LibreChat Agent streaming chat failed, falling back to Codex SDK or DeepSeek direct');\n provider = codexRuntime.enabled ? codexRuntime.provider : 'deepseek';\n responseModel = requestedModel;\n writeSse(res, 'meta', {\n conversationId,\n provider,\n model: responseModel,\n });\n if (codexRuntime.enabled) {\n try {\n aiResult = await callCodexSdkDeepSeek(modelMessages, {\n onToken: (token) => writeSse(res, 'delta', { text: token }),\n onStep: (step) => writeSse(res, 'step', step),\n });\n } catch (codexError) {\n if (isDeepSeekQuotaError(codexError)) throw codexError;\n logger.warn({ error: String(codexError) }, 'Codex SDK streaming chat failed, falling back to DeepSeek direct');\n provider = 'deepseek';\n responseModel = requestedModel;\n writeSse(res, 'meta', {\n conversationId,\n provider,\n model: responseModel,\n });\n aiResult = await callDeepSeekStream(modelMessages, {\n model: requestedModel,\n maxCompletionTokens,\n onToken: (token) => writeSse(res, 'delta', { text: token }),\n });\n }\n } else {\n aiResult = await callDeepSeekStream(modelMessages, {\n model: requestedModel,\n maxCompletionTokens,\n onToken: (token) => writeSse(res, 'delta', { text: token }),\n });\n }\n } else {\n if (!codexRuntime.enabled || isDeepSeekQuotaError(error)) throw error;\n logger.warn({ error: String(error) }, 'Codex SDK streaming chat failed, falling back to DeepSeek direct');\n provider = 'deepseek';\n responseModel = requestedModel;\n writeSse(res, 'meta', {\n conversationId,\n provider,\n model: responseModel,\n });\n aiResult = await callDeepSeekStream(modelMessages, {\n model: requestedModel,\n maxCompletionTokens,\n onToken: (token) => writeSse(res, 'delta', { text: token }),\n });\n }\n }\n\n const usage = {\n promptTokens: aiResult.usage.promptTokens || estimateTokens(JSON.stringify(modelMessages)),\n completionTokens: aiResult.usage.completionTokens || estimateTokens(aiResult.message),\n totalTokens:\n aiResult.usage.totalTokens ||\n estimateTokens(JSON.stringify(modelMessages)) + estimateTokens(aiResult.message),\n };\n recordUsage(user.userId, usage.totalTokens);\n\n const answeredAt = nowIso();\n statements.insertMessage.run({\n id: crypto.randomUUID(),\n conversationId,\n role: 'assistant',\n content: aiResult.message,\n promptTokens: usage.promptTokens,\n completionTokens: usage.completionTokens,\n totalTokens: usage.totalTokens,\n createdAt: answeredAt,\n });\n statements.updateConversation.run({\n id: conversationId,\n userId: user.userId,\n title: isNewConversation ? titleFromMessage(message) : conversation.title,\n updatedAt: answeredAt,\n });\n\n const latestUsage = usageFor(user.userId);\n const monthlyLimit = budget.limit;\n const action = determineClientAction(skillResult, message);\n writeSse(res, 'done', {\n conversationId,\n provider,\n model: responseModel,\n message: aiResult.message,\n usage: {\n ...usage,\n monthlyLimit,\n remainingTokens: Math.max(0, monthlyLimit - latestUsage.used),\n },\n ...(action ? { clientAction: action } : {}),\n });\n res.end();\n } catch (error) {\n logger.error({ error: error.stack || String(error) }, 'streaming chat failed');\n writeSse(res, 'error', {\n message: publicAiErrorMessage(error),\n details: error.details,\n });\n res.end();\n }\n});\n\napp.get(\n '/api/ai/conversations',\n asyncHandler(async (req, res) => {\n const user = await resolveUser(req, { username: req.query.username });\n const items = statements.listConversations.all(user.userId).map((item) => ({\n id: item.id,\n title: item.title,\n updatedAt: item.updatedAt,\n }));\n jsonResponse(res, 200, { success: true, items });\n }),\n);\n\napp.get(\n '/api/ai/conversations/:id',\n asyncHandler(async (req, res) => {\n const user = await resolveUser(req);\n const conversation = statements.getConversation.get(req.params.id, user.userId);\n if (!conversation) {\n return jsonResponse(res, 404, { success: false, message: 'conversation not found' });\n }\n const messages = statements.listMessages.all(req.params.id).map((item) => ({\n role: item.role,\n content: item.content,\n createdAt: item.createdAt,\n }));\n jsonResponse(res, 200, {\n success: true,\n conversation: {\n id: conversation.id,\n title: conversation.title,\n updatedAt: conversation.updated_at,\n },\n messages,\n });\n }),\n);\n\nfunction buildUrl(base, endpoint, params = {}) {\n const url = new URL(endpoint, `${base}/`);\n for (const [key, value] of Object.entries(params)) {\n if (Array.isArray(value)) {\n for (const item of value) {\n if (item !== undefined && item !== null && item !== '') {\n url.searchParams.append(key, String(item));\n }\n }\n } else if (value !== undefined && value !== null && value !== '') {\n url.searchParams.set(key, String(value));\n }\n }\n return url;\n}\n\nasync function fetchJson(url) {\n const response = await fetch(url, {\n headers: { Accept: 'application/json' },\n signal: AbortSignal.timeout(15_000),\n });\n if (!response.ok) {\n throw new Error(`Request failed: ${response.status}`);\n }\n return response.json();\n}\n\nfunction stripHtml(value) {\n return String(value || '')\n .replace(/<script[\\s\\S]*?<\\/script>/gi, ' ')\n .replace(/<style[\\s\\S]*?<\\/style>/gi, ' ')\n .replace(/<[^>]+>/g, ' ')\n .replace(/ /g, ' ')\n .replace(/</g, '<')\n .replace(/>/g, '>')\n .replace(/&/g, '&')\n .replace(/&#(\\d+);/g, (_match, code) => String.fromCharCode(Number(code)))\n .replace(/\\s+/g, ' ')\n .trim();\n}\n\nfunction cleanExtractedText(value) {\n let text = String(value || '')\n .replace(/\\b[A-Z]{1,3}\\d{2}n\\d{4,5}[a-z]?_p?\\d{4}[a-z]\\d{2}\\b/g, ' ')\n .replace(/\\uFFFD/g, '')\n .replace(/\\s+([,。;:!?、」』)》】])/g, '$1')\n .replace(/([「『《(【])\\s+/g, '$1');\n for (let i = 0; i < 3; i += 1) {\n text = text.replace(/([\\u3400-\\u9fff])\\s+([\\u3400-\\u9fff])/g, '$1$2');\n }\n return text.replace(/[ \\t]{2,}/g, ' ').trim();\n}\n\nfunction compactText(value, limit = 140) {\n const text = stripHtml(value);\n return text.length > limit ? `${text.slice(0, limit)}...` : text;\n}\n\nfunction collectSearchResults(data) {\n if (!data || typeof data !== 'object') return [];\n if (Array.isArray(data.results)) return data.results;\n if (Array.isArray(data?.results?.docs)) return data.results.docs;\n if (Array.isArray(data?.response?.docs)) return data.response.docs;\n if (Array.isArray(data.docs)) return data.docs;\n return [];\n}\n\nfunction firstText(item, keys) {\n for (const key of keys) {\n const value = item?.[key];\n if (Array.isArray(value)) {\n const first = value.map((entry) => safeUserText(String(entry))).find(Boolean);\n if (first) return first;\n }\n if (typeof value === 'string' && value.trim()) return value.trim();\n if (typeof value === 'number') return String(value);\n }\n return '';\n}\n\nfunction firstNumber(item, keys, fallback = undefined) {\n for (const key of keys) {\n const value = item?.[key];\n if (typeof value === 'number') return value;\n if (typeof value === 'string' && value.trim() && Number.isFinite(Number(value))) {\n return Number(value);\n }\n }\n return fallback;\n}\n\nasync function searchCbetaTitle(query, limit) {\n const url = buildUrl(cbetaApiRoot, 'search/title', { q: query, start: 0, rows: limit });\n const data = await fetchJson(url);\n return collectSearchResults(data).flatMap((item) => {\n const work = firstText(item, ['work', 'work_id', 'workId']);\n const title = firstText(item, ['content', 'title', 'work_title', 'workTitle']);\n if (!work || !title) return [];\n const juan = firstNumber(item, ['juan', 'juan_num', 'juanNum'], 1);\n return [\n {\n id: `cbeta-title-${work}-${juan}`,\n title,\n sourceName: 'CBETA 佛典',\n url: `cbeta:${work}:${juan}`,\n snippet: firstText(item, ['byline', 'creators', 'time_dynasty']) || '可下载经文正文并用于全球法布施。',\n resourceType: 'scripture',\n work,\n juan,\n },\n ];\n });\n}\n\nasync function searchCbetaContent(query, limit) {\n const endpoints = ['search/fulltext', 'search/content', 'search/all_in_one'];\n const settled = await Promise.allSettled(\n endpoints.map((endpoint) =>\n fetchJson(buildUrl(cbetaApiRoot, endpoint, { q: query, start: 0, rows: limit, field: 'content' })),\n ),\n );\n const results = [];\n for (const response of settled) {\n if (response.status !== 'fulfilled') continue;\n for (const item of collectSearchResults(response.value)) {\n const work = firstText(item, ['work', 'work_id', 'workId', 'sutra', 'id']);\n const snippet = compactText(firstText(item, ['kwic', 'highlight', 'snippet', 'content', 'text', 'body', 'p']));\n if (!work || !snippet) continue;\n const juan = firstNumber(item, ['juan', 'juan_num', 'juanNum', '卷'], 1);\n const title = firstText(item, ['title', 'work_title', 'workTitle', 'sutra_name', 'book', 'name']) || work;\n results.push({\n id: `cbeta-content-${work}-${juan}-${sha256(snippet).slice(0, 8)}`,\n title,\n sourceName: 'CBETA 佛典',\n url: `cbeta:${work}:${juan}`,\n snippet,\n resourceType: 'scripture',\n work,\n juan,\n });\n if (results.length >= limit) return results;\n }\n }\n return results;\n}\n\nfunction directUrlResource(query) {\n const url = safeUserText(query);\n let parsed;\n try {\n parsed = new URL(url);\n } catch {\n return null;\n }\n if (!['http:', 'https:'].includes(parsed.protocol)) return null;\n return {\n id: `url-${sha256(url).slice(0, 12)}`,\n title: parsed.hostname,\n sourceName: '网页资源',\n url,\n snippet: '直接下载并提取这个链接的可读正文。',\n resourceType: 'web',\n };\n}\n\nfunction looksLikeAudioQuery(query) {\n return /音乐|音樂|音频|音訊|mp3|audio|念诵|念誦|诵经|誦經|唱诵|唱誦|读诵|讀誦/.test(\n safeUserText(query).toLowerCase(),\n );\n}\n\nfunction archiveSearchTerm(query) {\n return (\n safeUserText(query)\n .replace(/下载|查找|寻找|找一?个|资源|音频|音訊|音乐|音樂|mp3|audio|念诵|念誦|诵经|誦經|唱诵|唱誦|读诵|讀誦/gi, ' ')\n .replace(/\\s+/g, ' ')\n .trim() || safeUserText(query)\n );\n}\n\nfunction solrPhrase(value) {\n return String(value).replace(/\\\\/g, '\\\\\\\\').replace(/\"/g, '\\\\\"');\n}\n\nasync function searchInternetArchiveAudio(query, limit) {\n if (!looksLikeAudioQuery(query)) return [];\n const term = archiveSearchTerm(query);\n if (!term) return [];\n const variants = Array.from(\n new Set([\n term,\n term.replaceAll('金刚经', '金剛經'),\n term.replaceAll('金剛經', '金刚经'),\n ].filter(Boolean)),\n );\n const phraseQuery = variants\n .flatMap((item) => [`title:\"${solrPhrase(item)}\"`, `description:\"${solrPhrase(item)}\"`])\n .join(' OR ');\n const url = buildUrl('https://archive.org', 'advancedsearch.php', {\n q: `(${phraseQuery}) AND mediatype:audio`,\n rows: Math.min(Math.max(limit, 1), 10),\n output: 'json',\n 'fl[]': ['identifier', 'title', 'description'],\n });\n const data = await fetchJson(url);\n const docs = Array.isArray(data?.response?.docs) ? data.response.docs : [];\n return docs\n .map((item) => {\n const identifier = firstText(item, ['identifier']);\n const title = firstText(item, ['title']) || identifier;\n if (!identifier || !title) return null;\n return {\n id: `ia-audio-${identifier}`,\n title,\n sourceName: 'Internet Archive 音频',\n url: `ia:${identifier}`,\n snippet: compactText(firstText(item, ['description'])) || '公开可访问的音频资源,将下载第一个可用 MP3 文件。',\n resourceType: 'audio',\n identifier,\n };\n })\n .filter(Boolean);\n}\n\nconst curatedCbetaWorks = [\n {\n work: 'T0235',\n juan: 1,\n title: '金剛般若波羅蜜經',\n keywords: ['金剛般若波羅蜜經', '金刚般若波罗蜜经', '金剛經', '金刚经'],\n snippet: '姚秦 鳩摩羅什譯。常用于读诵、共修与法布施的般若经典。',\n },\n {\n work: 'T0251',\n juan: 1,\n title: '般若波羅蜜多心經',\n keywords: ['般若波羅蜜多心經', '般若波罗蜜多心经', '心經', '心经'],\n snippet: '唐 玄奘譯。篇幅短,适合加入禅室功课本与日常持诵。',\n },\n {\n work: 'T0262',\n juan: 1,\n title: '妙法蓮華經',\n keywords: ['妙法蓮華經', '妙法莲华经', '法華經', '法华经'],\n snippet: '姚秦 鳩摩羅什譯。可按卷下载正文,用于长期功课与分享。',\n },\n {\n work: 'T0279',\n juan: 1,\n title: '大方廣佛華嚴經',\n keywords: ['大方廣佛華嚴經', '大方广佛华严经', '華嚴經', '华严经'],\n snippet: '唐 實叉難陀譯。可按卷下载正文,用于系统研读与法布施。',\n },\n {\n work: 'T0360',\n juan: 1,\n title: '佛說無量壽經',\n keywords: ['佛說無量壽經', '佛说无量寿经', '無量壽經', '无量寿经'],\n snippet: '净土经典。可下载正文,用于净土功课本与全球法布施。',\n },\n {\n work: 'T0365',\n juan: 1,\n title: '佛說觀無量壽佛經',\n keywords: ['佛說觀無量壽佛經', '佛说观无量寿佛经', '觀無量壽經', '观无量寿经'],\n snippet: '净土三经之一。适合查找下载后加入净土功课。',\n },\n {\n work: 'T0366',\n juan: 1,\n title: '佛說阿彌陀經',\n keywords: ['佛說阿彌陀經', '佛说阿弥陀经', '阿彌陀經', '阿弥陀经'],\n snippet: '姚秦 鳩摩羅什譯。篇幅适中,适合共修、持诵与分享。',\n },\n {\n work: 'T0475',\n juan: 1,\n title: '維摩詰所說經',\n keywords: ['維摩詰所說經', '维摩诘所说经', '維摩詰經', '维摩诘经'],\n snippet: '姚秦 鳩摩羅什譯。大乘经典,可按卷下载用于研读。',\n },\n {\n work: 'T0676',\n juan: 1,\n title: '解深密經',\n keywords: ['解深密經', '解深密经'],\n snippet: '唯识相关大乘经典。适合研读、整理笔记与法布施。',\n },\n {\n work: 'T1666',\n juan: 1,\n title: '大乘起信論',\n keywords: ['大乘起信論', '大乘起信论', '起信論', '起信论'],\n snippet: '大乘论典。适合下载后作为研习功课资料。',\n },\n];\n\nfunction localCbetaResults(query, limit) {\n const normalizedQuery = safeUserText(query).toLowerCase();\n if (!normalizedQuery) return [];\n return curatedCbetaWorks\n .filter((item) =>\n item.keywords.some((keyword) => {\n const normalizedKeyword = keyword.toLowerCase();\n return normalizedKeyword.includes(normalizedQuery) || normalizedQuery.includes(normalizedKeyword);\n }),\n )\n .slice(0, limit)\n .map((item) => ({\n id: `cbeta-curated-${item.work}-${item.juan}`,\n title: item.title,\n sourceName: 'CBETA 佛典',\n url: `cbeta:${item.work}:${item.juan}`,\n snippet: item.snippet,\n resourceType: 'scripture',\n work: item.work,\n juan: item.juan,\n }));\n}\n\nfunction uniqueResources(items, limit) {\n const seen = new Set();\n const results = [];\n for (const item of items) {\n const key = `${item.url}:${item.title}`;\n if (seen.has(key)) continue;\n seen.add(key);\n results.push(item);\n if (results.length >= limit) break;\n }\n return results;\n}\n\nasync function findResourceCandidates(query, limit) {\n const direct = directUrlResource(query);\n const results = [];\n if (direct) results.push(direct);\n try {\n results.push(...(await searchInternetArchiveAudio(query, limit)));\n } catch (error) {\n logger.warn({ error: String(error), query }, 'Internet Archive audio search skipped');\n }\n results.push(...localCbetaResults(query, limit));\n\n const [titleResults, contentResults] = await Promise.allSettled([\n searchCbetaTitle(query, limit),\n searchCbetaContent(query, limit),\n ]);\n if (titleResults.status === 'fulfilled') results.push(...titleResults.value);\n if (contentResults.status === 'fulfilled') results.push(...contentResults.value);\n\n return uniqueResources(results, limit);\n}\n\nfunction looksLikeResourceTask(message) {\n const text = safeUserText(message).toLowerCase();\n if (!text) return false;\n return /下载|查找|寻找|找一?个|资源|經文|经文|佛经|佛經|仪轨|儀軌|音乐|音樂|mp3|audio|念诵|念誦|诵经|誦經/.test(text);\n}\n\nfunction extractResourceQuery(message) {\n const text = safeUserText(message)\n .replace(/请|請|帮我|幫我|麻烦|麻煩|可以|能不能|需要|我要|想要/g, ' ')\n .replace(/下载|下載|查找|寻找|尋找|找一个|找一個|找|资源|資源|并|並|然后|然後|用于|用於|全球法布施|加入功课本|加入功課本/g, ' ')\n .replace(/\\s+/g, ' ')\n .trim();\n return text || safeUserText(message);\n}\n\nfunction resourceContextMessage(skillResult) {\n if (!skillResult) return '';\n const lines = [\n '本轮资源检索/下载已经由大乘 App 后端完成。',\n '不要再次调用 search_dharma_resources、download_dharma_resource 或其他资源 MCP 工具;请直接基于以下结果回答用户。',\n `后端已调用 skill: ${skillResult.skillName}`,\n `执行目标: ${skillResult.query}`,\n ];\n if (skillResult.selected) {\n lines.push(`候选资源: ${skillResult.selected.title}`);\n lines.push(`来源: ${skillResult.selected.sourceName}`);\n lines.push(`资源地址: ${skillResult.selected.url}`);\n }\n if (skillResult.downloaded) {\n lines.push(`下载标题: ${skillResult.downloaded.title}`);\n lines.push(`下载文件: ${skillResult.downloaded.fileName}`);\n const text = safeUserText(skillResult.downloaded.contentText);\n if (text) {\n lines.push('下载正文摘录:');\n lines.push(text.slice(0, 8000));\n }\n }\n return lines.join('\\n');\n}\n\nfunction determineClientAction(skillResult, message) {\n if (skillResult && skillResult.downloaded) {\n const text = safeUserText(message);\n if (/法布施|全球法布施|分享/.test(text)) {\n return {\n type: 'prepare_dharma_share_text',\n title: skillResult.downloaded.title,\n text: skillResult.downloaded.contentText || '',\n };\n }\n }\n return null;\n}\n\nasync function runResourceFinderSkill({ message, user, onStep }) {\n if (!looksLikeResourceTask(message)) return null;\n\n const skillName = 'resource-finder-downloader';\n const query = extractResourceQuery(message);\n const audioTask = looksLikeAudioQuery(message) || looksLikeAudioQuery(query);\n onStep?.({\n type: 'skill.step',\n skillName,\n stage: 'start',\n title: '调用资源查找下载 skill',\n message: `识别到资源任务: ${query}`,\n });\n\n onStep?.({\n type: 'skill.step',\n skillName,\n stage: 'search',\n title: '搜索可分享资源',\n message: audioTask\n ? '优先检索公开可访问音频资源,失败时再回落到佛典正文资源。'\n : '优先检索 CBETA 佛典正文接口,搜索索引不可用时使用内置经典映射。',\n });\n const candidates = await findResourceCandidates(query, 8);\n if (candidates.length === 0) {\n onStep?.({\n type: 'skill.step',\n skillName,\n stage: 'empty',\n title: '没有找到可下载资源',\n message: '后端没有找到明确可下载且可分享的资源。',\n });\n return {\n skillName,\n query,\n candidates: [],\n selected: null,\n downloaded: null,\n };\n }\n\n const selected = candidates[0];\n onStep?.({\n type: 'skill.step',\n skillName,\n stage: 'select',\n title: '选择可信候选资源',\n message: `${selected.title} / ${selected.sourceName}`,\n });\n\n onStep?.({\n type: 'skill.step',\n skillName,\n stage: 'download',\n title: '下载并提取资源',\n message: selected.url,\n });\n const cbeta = parseCbetaResource(selected);\n const internetArchive = parseInternetArchiveResource(selected);\n const downloaded = cbeta\n ? await downloadCbetaResource({ ...selected, ...cbeta })\n : internetArchive\n ? await downloadInternetArchiveAudio({ ...selected, ...internetArchive })\n : await downloadWebResource(selected);\n const persisted = persistDownloadedResource(user, downloaded);\n\n onStep?.({\n type: 'skill.step',\n skillName,\n stage: 'done',\n title: '资源已准备完成',\n message: downloaded.fileName,\n });\n\n return {\n skillName,\n query,\n candidates,\n selected,\n downloaded: {\n ...downloaded,\n downloadId: persisted.id,\n },\n };\n}\n\napp.post(\n '/api/resources/search',\n asyncHandler(async (req, res) => {\n const query = safeUserText(req.body?.query);\n const limit = Math.min(Math.max(Number(req.body?.limit || 12), 1), 20);\n if (query.length < 2) {\n return jsonResponse(res, 400, { success: false, message: 'query is required' });\n }\n\n const items = await findResourceCandidates(query, limit);\n\n jsonResponse(res, 200, {\n success: true,\n source: 'resource-index',\n items,\n });\n }),\n);\n\nfunction parseCbetaResource(input) {\n const work = safeUserText(input.work);\n const juan = Number(input.juan || 1);\n if (work) return { work, juan: Number.isFinite(juan) ? juan : 1 };\n\n const url = safeUserText(input.url);\n const match = /^cbeta:([^:]+):(\\d+)$/i.exec(url);\n if (!match) return null;\n return {\n work: match[1].toUpperCase(),\n juan: Number(match[2] || 1),\n };\n}\n\nfunction parseInternetArchiveResource(input) {\n const url = safeUserText(input.url);\n const match = /^ia:([^:]+)$/i.exec(url);\n const identifier = match?.[1] || safeUserText(input.identifier);\n if (!identifier) return null;\n return { identifier };\n}\n\nfunction safeFileName(value) {\n const normalized = String(value || 'dharma-resource')\n .replace(/[\\\\/:*?\"<>|]+/g, '-')\n .replace(/\\s+/g, '-')\n .replace(/-+/g, '-')\n .replace(/^-|-$/g, '')\n .slice(0, 80);\n return normalized || 'dharma-resource';\n}\n\nfunction extractCbetaHtml(data) {\n const first = Array.isArray(data?.results) ? data.results[0] : null;\n if (typeof first === 'string') return first;\n if (first && typeof first === 'object' && typeof first.html === 'string') return first.html;\n if (typeof data?.html === 'string') return data.html;\n return '';\n}\n\nasync function downloadCbetaResource(resource) {\n const url = buildUrl(cbetaApiRoot, 'juans', {\n work: resource.work,\n juan: resource.juan,\n work_info: 1,\n toc: 1,\n });\n const data = await fetchJson(url);\n const workInfo = data.work_info || {};\n const title = safeUserText(workInfo.title) || safeUserText(resource.title) || resource.work;\n const html = extractCbetaHtml(data);\n const text = cleanExtractedText(stripHtml(html));\n if (!text) throw new Error('CBETA resource returned empty content');\n return {\n title,\n sourceName: 'CBETA 佛典',\n url: `cbeta:${resource.work}:${resource.juan}`,\n contentText: text.slice(0, maxResourceTextChars),\n fileName: `${resource.work}_${resource.juan}_${safeFileName(title)}.txt`,\n };\n}\n\nasync function downloadInternetArchiveAudio(resource) {\n const identifier = safeUserText(resource.identifier);\n if (!identifier) throw new Error('Internet Archive identifier is required');\n const metadata = await fetchJson(`https://archive.org/metadata/${encodeURIComponent(identifier)}`);\n const title = safeUserText(metadata?.metadata?.title) || safeUserText(resource.title) || identifier;\n const files = Array.isArray(metadata?.files) ? metadata.files : [];\n const audioFile = files.find((file) => {\n const name = safeUserText(file?.name);\n const format = safeUserText(file?.format).toLowerCase();\n return /\\.(mp3|m4a|ogg)$/i.test(name) || format.includes('mp3');\n });\n if (!audioFile?.name) {\n throw new Error('Internet Archive audio item has no downloadable audio file');\n }\n const sourceFileName = audioFile.name;\n const fileUrl = `https://archive.org/download/${encodeURIComponent(identifier)}/${sourceFileName\n .split('/')\n .map((part) => encodeURIComponent(part))\n .join('/')}`;\n const response = await fetch(fileUrl, {\n headers: {\n Accept: 'audio/mpeg, audio/*;q=0.9, */*;q=0.5',\n 'User-Agent': 'DachengResourceDownloader/1.0 (+https://ombhrum.com)',\n },\n signal: AbortSignal.timeout(60_000),\n });\n if (!response.ok) {\n throw new Error(`Internet Archive audio download failed: ${response.status}`);\n }\n const binaryContent = Buffer.from(await response.arrayBuffer());\n if (!binaryContent.length) throw new Error('Internet Archive audio returned empty content');\n const sizeMb = (binaryContent.length / 1024 / 1024).toFixed(1);\n return {\n title,\n sourceName: 'Internet Archive 音频',\n url: fileUrl,\n contentText: `已下载音频资源: ${title}\\n文件名: ${sourceFileName}\\n大小: ${sizeMb} MB\\n来源: ${fileUrl}`,\n binaryContent,\n fileName: safeFileName(sourceFileName),\n };\n}\n\nasync function downloadWebResource(input) {\n const url = safeUserText(input.url);\n const parsed = new URL(url);\n if (!['http:', 'https:'].includes(parsed.protocol)) {\n throw new Error('Only http/https resources can be downloaded');\n }\n\n const response = await fetch(parsed, {\n headers: {\n Accept: 'text/html, text/plain, application/xhtml+xml, application/xml;q=0.8, */*;q=0.5',\n 'User-Agent': 'DachengResourceDownloader/1.0 (+https://ombhrum.com)',\n },\n signal: AbortSignal.timeout(25_000),\n });\n if (!response.ok) throw new Error(`Resource download failed: ${response.status}`);\n const contentType = response.headers.get('content-type') || '';\n const body = await response.text();\n const title = safeUserText(input.title) || parsed.hostname;\n const contentText = contentType.includes('html') ? cleanExtractedText(stripHtml(body)) : body.trim();\n if (!contentText) throw new Error('Resource returned empty content');\n return {\n title,\n sourceName: safeUserText(input.sourceName) || parsed.hostname,\n url,\n contentText: contentText.slice(0, maxResourceTextChars),\n fileName: `${safeFileName(title)}.txt`,\n };\n}\n\nfunction persistDownloadedResource(user, content) {\n const id = crypto.randomUUID();\n const fileName = `${id}-${safeFileName(content.fileName)}`;\n const filePath = path.join(resourcesDir, fileName);\n if (content.binaryContent) {\n fs.writeFileSync(filePath, content.binaryContent);\n } else {\n fs.writeFileSync(filePath, content.contentText, 'utf8');\n }\n statements.insertResourceDownload.run({\n id,\n userId: user.userId,\n title: content.title,\n sourceName: content.sourceName,\n sourceUrl: content.url,\n fileName: content.fileName,\n filePath,\n createdAt: nowIso(),\n });\n return { id, fileName, filePath };\n}\n\nfunction createLibreChatMcpServer() {\n const server = new McpServer({\n name: 'dacheng-ai-tools',\n version: '0.1.0',\n });\n\n server.registerTool(\n 'search_dharma_resources',\n {\n title: 'Search dharma resources',\n description:\n 'Search for legally shareable Buddhist scripture, text, web, or audio resources that can be prepared for Fabushi sharing.',\n inputSchema: {\n query: z.string().min(2).max(200),\n limit: z.number().int().min(1).max(12).optional(),\n },\n outputSchema: {\n query: z.string(),\n items: z.array(\n z.object({\n id: z.string(),\n title: z.string(),\n sourceName: z.string(),\n url: z.string(),\n snippet: z.string(),\n resourceType: z.string(),\n work: z.string().optional(),\n juan: z.number().optional(),\n }),\n ),\n },\n annotations: {\n readOnlyHint: true,\n destructiveHint: false,\n openWorldHint: true,\n },\n },\n async ({ query, limit }) => {\n const items = await findResourceCandidates(query, limit ?? 8);\n return textToolResult(`Found ${items.length} candidate resources for \"${query}\".`, {\n query,\n items,\n });\n },\n );\n\n server.registerTool(\n 'download_dharma_resource',\n {\n title: 'Download dharma resource',\n description:\n 'Download and extract a selected resource. Use the url/work/juan returned by search_dharma_resources whenever possible.',\n inputSchema: {\n url: z.string().min(1).max(1000),\n title: z.string().max(300).optional(),\n sourceName: z.string().max(120).optional(),\n work: z.string().max(40).optional(),\n juan: z.number().int().min(1).optional(),\n identifier: z.string().max(300).optional(),\n },\n outputSchema: {\n title: z.string(),\n sourceName: z.string(),\n url: z.string(),\n fileName: z.string(),\n downloadId: z.string(),\n contentText: z.string(),\n },\n annotations: {\n readOnlyHint: false,\n destructiveHint: false,\n openWorldHint: true,\n },\n },\n async (input) => {\n const user = { userId: 'librechat:mcp' };\n const cbeta = parseCbetaResource(input);\n const internetArchive = parseInternetArchiveResource(input);\n const content = cbeta\n ? await downloadCbetaResource({ ...input, ...cbeta })\n : internetArchive\n ? await downloadInternetArchiveAudio({ ...input, ...internetArchive })\n : await downloadWebResource(input);\n const persisted = persistDownloadedResource(user, content);\n const { binaryContent: _binaryContent, ...publicContent } = content;\n return textToolResult(`Downloaded ${publicContent.title} as ${publicContent.fileName}.`, {\n ...publicContent,\n downloadId: persisted.id,\n });\n },\n );\n\n server.registerTool(\n 'prepare_dharma_share_text',\n {\n title: 'Prepare dharma sharing text',\n description:\n 'Prepare a text payload for the Fabushi app to confirm and add to global dharma sharing. This returns a clientAction; it does not start sending.',\n inputSchema: {\n title: z.string().min(1).max(120),\n text: z.string().min(1).max(20000),\n },\n outputSchema: {\n clientAction: z.object({\n type: z.literal('prepare_dharma_share_text'),\n title: z.string(),\n text: z.string(),\n }),\n },\n annotations: {\n readOnlyHint: false,\n destructiveHint: false,\n openWorldHint: false,\n },\n },\n async ({ title, text }) =>\n textToolResult(`Prepared \"${title}\" for user confirmation in the Fabushi app.`, {\n clientAction: {\n type: 'prepare_dharma_share_text',\n title: safeUserText(title).slice(0, 120),\n text: String(text || '').trim().slice(0, 20000),\n },\n }),\n );\n\n server.registerTool(\n 'prepare_practice_book_item',\n {\n title: 'Prepare practice book item',\n description:\n 'Prepare a scripture, chant, or practice item for the Fabushi app practice book. This returns a clientAction; it does not mutate the app by itself.',\n inputSchema: {\n title: z.string().min(1).max(120),\n sourceName: z.string().max(120).optional(),\n text: z.string().min(1).max(20000),\n },\n outputSchema: {\n clientAction: z.object({\n type: z.literal('prepare_practice_book_item'),\n title: z.string(),\n sourceName: z.string(),\n text: z.string(),\n }),\n },\n annotations: {\n readOnlyHint: false,\n destructiveHint: false,\n openWorldHint: false,\n },\n },\n async ({ title, sourceName, text }) =>\n textToolResult(`Prepared \"${title}\" for the Fabushi practice book confirmation flow.`, {\n clientAction: {\n type: 'prepare_practice_book_item',\n title: safeUserText(title).slice(0, 120),\n sourceName: safeUserText(sourceName || '大乘 AI').slice(0, 120),\n text: String(text || '').trim().slice(0, 20000),\n },\n }),\n );\n\n return server;\n}\n\nasync function handleMcpRequest(req, res) {\n const server = createLibreChatMcpServer();\n const transport = new StreamableHTTPServerTransport({\n sessionIdGenerator: undefined,\n enableJsonResponse: true,\n });\n\n res.on('close', () => {\n transport.close();\n server.close();\n });\n\n await server.connect(transport);\n await transport.handleRequest(req, res, req.body);\n}\n\napp.all('/mcp', async (req, res) => {\n try {\n await handleMcpRequest(req, res);\n } catch (error) {\n logger.error({ error: error.stack || String(error) }, 'MCP request failed');\n if (!res.headersSent) {\n res.status(500).json({\n error: 'MCP request failed',\n message: publicAiErrorMessage(error),\n });\n }\n }\n});\n\napp.post(\n '/api/resources/download',\n asyncHandler(async (req, res) => {\n const user = await resolveUser(req, req.body || {});\n const cbeta = parseCbetaResource(req.body || {});\n const internetArchive = parseInternetArchiveResource(req.body || {});\n const content = cbeta\n ? await downloadCbetaResource({ ...req.body, ...cbeta })\n : internetArchive\n ? await downloadInternetArchiveAudio({ ...req.body, ...internetArchive })\n : await downloadWebResource(req.body || {});\n const persisted = persistDownloadedResource(user, content);\n const { binaryContent: _binaryContent, ...publicContent } = content;\n jsonResponse(res, 200, {\n success: true,\n downloadId: persisted.id,\n ...publicContent,\n });\n }),\n);\n\napp.post(\n '/api/codex/resource-task',\n asyncHandler(async (req, res) => {\n if (process.env.ENABLE_CODEX_SDK !== 'true') {\n return jsonResponse(res, 403, {\n success: false,\n message: 'Codex SDK task runner is disabled. Set ENABLE_CODEX_SDK=true on the server to enable it.',\n });\n }\n const prompt = safeUserText(req.body?.prompt);\n if (!prompt) {\n return jsonResponse(res, 400, { success: false, message: 'prompt is required' });\n }\n const { Codex } = await import('@openai/codex-sdk');\n const codexRuntime = createCodexDeepSeekRuntime();\n const codex = new Codex(codexRuntime.enabled ? codexRuntime.options : {});\n const thread = codex.startThread(\n codexRuntime.enabled\n ? codexRuntime.threadOptions\n : { skipGitRepoCheck: true, networkAccessEnabled: true },\n );\n const result = await thread.run(prompt);\n jsonResponse(res, 200, {\n success: true,\n threadId: thread.id,\n provider: codexRuntime.provider,\n model: deepseekModel,\n result,\n });\n }),\n);\n\napp.use((error, _req, res, _next) => {\n const status = Number(error.statusCode || error.status || 500);\n const safeStatus = status >= 400 && status < 600 ? status : 500;\n logger.error({ error: error.stack || String(error) }, 'request failed');\n jsonResponse(res, safeStatus, {\n success: false,\n message: error.message || 'Internal Server Error',\n details: error.details,\n });\n});\n\nconst port = Number(process.env.PORT || 8788);\napp.listen(port, '0.0.0.0', () => {\n logger.info(\n {\n port,\n dbPath,\n dataDir,\n deepseekModel,\n cbetaApiRoot,\n },\n 'Dacheng AI backend started',\n );\n});\n"}, "2662ff6b6da15829552fd211f3d2aba35ca41fc0": {"t": 1785757978.3252287, "n": 122487, "h": "55426830124499e6", "c": "import crypto from 'node:crypto';\nimport fs from 'node:fs';\nimport path from 'node:path';\nimport process from 'node:process';\nimport { fileURLToPath } from 'node:url';\n\nimport { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';\nimport { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';\nimport Database from 'better-sqlite3';\nimport cors from 'cors';\nimport dotenv from 'dotenv';\nimport express from 'express';\nimport rateLimit from 'express-rate-limit';\nimport helmet from 'helmet';\nimport pino from 'pino';\nimport { z } from 'zod';\n\ndotenv.config();\n\nconst __dirname = path.dirname(fileURLToPath(import.meta.url));\nconst rootDir = path.resolve(__dirname, '..');\nconst dataDir = process.env.DATA_DIR || path.join(rootDir, 'data');\nconst resourcesDir = path.join(dataDir, 'resources');\nconst openClawRuntimeUpdatesDir =\n process.env.OPENCLAW_RUNTIME_UPDATES_DIR || path.join(dataDir, 'openclaw-runtime');\nconst openClawRuntimeEnableMacosUpdates =\n process.env.OPENCLAW_RUNTIME_ENABLE_MACOS_UPDATES === 'true';\nconst codexHomeDir = process.env.CODEX_HOME || path.join(dataDir, 'codex-home');\nconst codexTempDir = process.env.CODEX_TMPDIR || path.join(dataDir, 'codex-tmp');\nconst codexRuntimeDir = process.env.XDG_RUNTIME_DIR || path.join(dataDir, 'codex-runtime');\nconst dbPath = process.env.SQLITE_PATH || path.join(dataDir, 'dacheng-ai.sqlite');\n\nfs.mkdirSync(resourcesDir, { recursive: true });\nfs.mkdirSync(openClawRuntimeUpdatesDir, { recursive: true });\nfs.mkdirSync(codexHomeDir, { recursive: true });\nfs.mkdirSync(codexTempDir, { recursive: true });\nfs.mkdirSync(codexRuntimeDir, { recursive: true });\n\nconst logger = pino({\n level: process.env.LOG_LEVEL || 'info',\n});\n\nconst app = express();\napp.disable('x-powered-by');\napp.set('trust proxy', Number(process.env.TRUST_PROXY_HOPS || 1));\napp.use(helmet());\nconst corsOrigin =\n !process.env.CORS_ORIGIN || process.env.CORS_ORIGIN.trim() === '*'\n ? true\n : process.env.CORS_ORIGIN.split(',').map((item) => item.trim()).filter(Boolean);\napp.use(cors({ origin: corsOrigin }));\napp.use(express.json({ limit: '1mb' }));\napp.use(\n rateLimit({\n windowMs: 60_000,\n limit: Number(process.env.RATE_LIMIT_PER_MINUTE || 90),\n standardHeaders: true,\n legacyHeaders: false,\n }),\n);\n\nconst db = new Database(dbPath);\ndb.pragma('journal_mode = WAL');\ndb.pragma('foreign_keys = ON');\n\ndb.exec(`\n CREATE TABLE IF NOT EXISTS conversations (\n id TEXT PRIMARY KEY,\n user_id TEXT NOT NULL,\n title TEXT NOT NULL,\n provider TEXT NOT NULL,\n model TEXT NOT NULL,\n created_at TEXT NOT NULL,\n updated_at TEXT NOT NULL\n );\n\n CREATE INDEX IF NOT EXISTS idx_conversations_user_updated\n ON conversations (user_id, updated_at DESC);\n\n CREATE TABLE IF NOT EXISTS messages (\n id TEXT PRIMARY KEY,\n conversation_id TEXT NOT NULL,\n role TEXT NOT NULL,\n content TEXT NOT NULL,\n prompt_tokens INTEGER NOT NULL DEFAULT 0,\n completion_tokens INTEGER NOT NULL DEFAULT 0,\n total_tokens INTEGER NOT NULL DEFAULT 0,\n created_at TEXT NOT NULL,\n FOREIGN KEY (conversation_id) REFERENCES conversations(id) ON DELETE CASCADE\n );\n\n CREATE INDEX IF NOT EXISTS idx_messages_conversation_created\n ON messages (conversation_id, created_at ASC);\n\n CREATE TABLE IF NOT EXISTS usage_months (\n user_id TEXT NOT NULL,\n month TEXT NOT NULL,\n total_tokens INTEGER NOT NULL DEFAULT 0,\n updated_at TEXT NOT NULL,\n PRIMARY KEY (user_id, month)\n );\n\n CREATE TABLE IF NOT EXISTS resource_downloads (\n id TEXT PRIMARY KEY,\n user_id TEXT NOT NULL,\n title TEXT NOT NULL,\n source_name TEXT NOT NULL,\n source_url TEXT NOT NULL,\n file_name TEXT NOT NULL,\n file_path TEXT NOT NULL,\n created_at TEXT NOT NULL\n );\n\n CREATE TABLE IF NOT EXISTS agent_runs (\n id TEXT PRIMARY KEY,\n user_id TEXT NOT NULL,\n conversation_id TEXT NOT NULL,\n message_id TEXT NOT NULL,\n openclaw_run_id TEXT,\n openclaw_session_key TEXT NOT NULL,\n status TEXT NOT NULL,\n mode TEXT NOT NULL,\n provider TEXT NOT NULL,\n model TEXT,\n input_tokens INTEGER NOT NULL DEFAULT 0,\n output_tokens INTEGER NOT NULL DEFAULT 0,\n tool_call_count INTEGER NOT NULL DEFAULT 0,\n cost_microusd INTEGER NOT NULL DEFAULT 0,\n started_at TEXT NOT NULL,\n completed_at TEXT,\n failed_at TEXT,\n error_code TEXT,\n error_message TEXT\n );\n\n CREATE INDEX IF NOT EXISTS idx_agent_runs_user_started\n ON agent_runs (user_id, started_at DESC);\n\n CREATE INDEX IF NOT EXISTS idx_agent_runs_status_started\n ON agent_runs (status, started_at DESC);\n\n CREATE TABLE IF NOT EXISTS agent_message_feedback (\n id TEXT PRIMARY KEY,\n message_id TEXT NOT NULL,\n user_id TEXT NOT NULL,\n rating TEXT NOT NULL,\n reason TEXT,\n comment TEXT,\n created_at TEXT NOT NULL\n );\n`);\n\nconst statements = {\n insertConversation: db.prepare(`\n INSERT INTO conversations (id, user_id, title, provider, model, created_at, updated_at)\n VALUES (@id, @userId, @title, @provider, @model, @createdAt, @updatedAt)\n `),\n updateConversation: db.prepare(`\n UPDATE conversations\n SET title = @title, updated_at = @updatedAt\n WHERE id = @id AND user_id = @userId\n `),\n getConversation: db.prepare(`\n SELECT * FROM conversations WHERE id = ? AND user_id = ? LIMIT 1\n `),\n listConversations: db.prepare(`\n SELECT id, title, updated_at AS updatedAt\n FROM conversations\n WHERE user_id = ?\n ORDER BY updated_at DESC\n LIMIT 30\n `),\n insertMessage: db.prepare(`\n INSERT INTO messages (\n id, conversation_id, role, content,\n prompt_tokens, completion_tokens, total_tokens, created_at\n )\n VALUES (\n @id, @conversationId, @role, @content,\n @promptTokens, @completionTokens, @totalTokens, @createdAt\n )\n `),\n listMessages: db.prepare(`\n SELECT role, content, prompt_tokens AS promptTokens,\n completion_tokens AS completionTokens, total_tokens AS totalTokens,\n created_at AS createdAt\n FROM messages\n WHERE conversation_id = ?\n ORDER BY created_at ASC\n `),\n getUsage: db.prepare(`\n SELECT total_tokens AS totalTokens FROM usage_months WHERE user_id = ? AND month = ?\n `),\n upsertUsage: db.prepare(`\n INSERT INTO usage_months (user_id, month, total_tokens, updated_at)\n VALUES (@userId, @month, @totalTokens, @updatedAt)\n ON CONFLICT(user_id, month)\n DO UPDATE SET total_tokens = usage_months.total_tokens + excluded.total_tokens,\n updated_at = excluded.updated_at\n `),\n insertResourceDownload: db.prepare(`\n INSERT INTO resource_downloads (\n id, user_id, title, source_name, source_url, file_name, file_path, created_at\n )\n VALUES (\n @id, @userId, @title, @sourceName, @sourceUrl, @fileName, @filePath, @createdAt\n )\n `),\n insertAgentRun: db.prepare(`\n INSERT INTO agent_runs (\n id, user_id, conversation_id, message_id, openclaw_run_id, openclaw_session_key,\n status, mode, provider, model, started_at\n )\n VALUES (\n @id, @userId, @conversationId, @messageId, @openClawRunId, @openClawSessionKey,\n @status, @mode, @provider, @model, @startedAt\n )\n `),\n getAgentRun: db.prepare(`\n SELECT * FROM agent_runs WHERE id = ? AND user_id = ? LIMIT 1\n `),\n updateAgentRunStatus: db.prepare(`\n UPDATE agent_runs\n SET status = @status,\n openclaw_run_id = COALESCE(@openClawRunId, openclaw_run_id),\n input_tokens = COALESCE(@inputTokens, input_tokens),\n output_tokens = COALESCE(@outputTokens, output_tokens),\n tool_call_count = COALESCE(@toolCallCount, tool_call_count),\n completed_at = @completedAt,\n failed_at = @failedAt,\n error_code = @errorCode,\n error_message = @errorMessage\n WHERE id = @id AND user_id = @userId\n `),\n insertAgentFeedback: db.prepare(`\n INSERT INTO agent_message_feedback (id, message_id, user_id, rating, reason, comment, created_at)\n VALUES (@id, @messageId, @userId, @rating, @reason, @comment, @createdAt)\n `),\n};\n\nconst deepseekApiKey = process.env.DEEPSEEK_API_KEY || '';\nconst deepseekBaseUrl = (process.env.DEEPSEEK_BASE_URL || 'https://api.deepseek.com').replace(/\\/+$/, '');\nconst deepseekModel = process.env.DEEPSEEK_MODEL || 'deepseek-chat';\nconst deepseekAvailableModels = Array.from(\n new Set(\n (process.env.DEEPSEEK_MODELS || `${deepseekModel},deepseek-chat,deepseek-reasoner`)\n .split(',')\n .map((item) => normalizeDeepSeekModelName(item))\n .filter(Boolean),\n ),\n);\nconst cbetaApiRoot = (process.env.CBETA_API_ROOT || 'https://api.ombhrum.com/api/cbeta').replace(/\\/+$/, '');\nconst fabushiApiBaseUrl = (process.env.FABUSHI_API_BASE_URL || 'https://api.ombhrum.com').replace(/\\/+$/, '');\nconst memberMonthlyLimit = Number(process.env.MEMBER_MONTHLY_TOKEN_LIMIT || 1_000_000);\nconst freeMonthlyLimit = Number(process.env.FREE_MONTHLY_TOKEN_LIMIT || 1_000);\nconst requireMemberForAi = process.env.REQUIRE_MEMBER_FOR_AI === 'true';\nconst deepseekMaxCompletionTokens = Math.max(1, Number(process.env.DEEPSEEK_MAX_COMPLETION_TOKENS || 1400));\nconst maxResourceTextChars = Number(process.env.MAX_RESOURCE_TEXT_CHARS || 80_000);\nconst openClawRuntimeManifestPath =\n process.env.OPENCLAW_RUNTIME_MANIFEST_PATH || path.join(openClawRuntimeUpdatesDir, 'manifest.json');\nconst openClawRuntimePublicBaseUrl = (process.env.OPENCLAW_RUNTIME_PUBLIC_BASE_URL || '').replace(/\\/+$/, '');\nconst enableLibreChatAgentChat = process.env.ENABLE_LIBRECHAT_AGENT_CHAT === 'true';\nconst libreChatAgentsBaseUrl = (\n process.env.LIBRECHAT_AGENTS_BASE_URL || 'http://127.0.0.1:3080/api/agents/v1'\n).replace(/\\/+$/, '');\nconst libreChatAgentApiKey = process.env.LIBRECHAT_AGENT_API_KEY || '';\nconst libreChatAgentId = process.env.LIBRECHAT_AGENT_ID || '';\nconst enableCodexSdkChat = process.env.ENABLE_CODEX_SDK_CHAT === 'true';\nconst enableOpenClawAgentChat = process.env.ENABLE_OPENCLAW_AGENT_CHAT === 'true';\nconst openClawGatewayUrl = (process.env.OPENCLAW_GATEWAY_URL || 'http://127.0.0.1:18789').replace(/\\/+$/, '');\nconst openClawGatewayToken = process.env.OPENCLAW_GATEWAY_TOKEN || '';\nconst openClawAgentId = process.env.OPENCLAW_AGENT_ID || 'fabushi-public-agent';\nconst openClawRunsEndpoint = process.env.OPENCLAW_RUNS_ENDPOINT || '';\nconst codexDeepSeekProviderId = 'deepseek-chat-completions';\nconst codexResponsesBaseUrl = (\n process.env.CODEX_DEEPSEEK_RESPONSES_BASE_URL ||\n `http://127.0.0.1:${process.env.PORT || 8788}/codex-deepseek/v1`\n).replace(/\\/+$/, '');\n\nconst systemPrompt = [\n '你是“大乘”App 的 AI 助手。',\n '你的核心任务是帮助用户查找、整理、理解并全球法布施合法可分享的佛法资源。',\n '回答要庄重、简洁、可执行;涉及经典、仪轨或资源时,提醒用户尊重版权、来源和当地法规。',\n '如果用户想找资源、下载经文、寻找音频或准备可分享资料,你必须先在后端自动执行搜索、验证和下载步骤;不要要求用户去前端 + 菜单手动查找。',\n '你可以把执行进度总结给用户,但不要输出隐藏推理原文。',\n].join('\\n');\n\nfunction nowIso() {\n return new Date().toISOString();\n}\n\nfunction monthKey(date = new Date()) {\n return `${date.getUTCFullYear()}-${String(date.getUTCMonth() + 1).padStart(2, '0')}`;\n}\n\nfunction jsonResponse(res, status, payload) {\n res.status(status).json(payload);\n}\n\nasync function readJsonFile(filePath) {\n const raw = await fs.promises.readFile(filePath, 'utf8');\n return JSON.parse(raw);\n}\n\nfunction requestOrigin(req) {\n if (openClawRuntimePublicBaseUrl) return openClawRuntimePublicBaseUrl;\n const proto = req.get('X-Forwarded-Proto') || req.protocol || 'https';\n const host = req.get('X-Forwarded-Host') || req.get('Host');\n return host ? `${proto}://${host}` : '';\n}\n\nfunction absolutizeOpenClawRuntimeUrl(req, value) {\n const text = readText(value);\n if (!text) return '';\n if (/^https?:\\/\\//i.test(text)) return text;\n const normalized = text.startsWith('/')\n ? text\n : `/api/openclaw/runtime/files/${text.replace(/^\\/+/, '')}`;\n const origin = requestOrigin(req);\n return origin ? new URL(normalized, `${origin}/`).toString() : normalized;\n}\n\nfunction textToolResult(text, structuredContent = {}) {\n return {\n structuredContent,\n content: [{ type: 'text', text }],\n };\n}\n\nfunction sseHeaders(res) {\n res.status(200);\n res.setHeader('Content-Type', 'text/event-stream; charset=utf-8');\n res.setHeader('Cache-Control', 'no-cache, no-transform');\n res.setHeader('Connection', 'keep-alive');\n res.setHeader('X-Accel-Buffering', 'no');\n if (typeof res.flushHeaders === 'function') res.flushHeaders();\n}\n\nfunction writeSse(res, event, payload) {\n res.write(`event: ${event}\\n`);\n res.write(`data: ${JSON.stringify(payload)}\\n\\n`);\n}\n\nfunction asyncHandler(fn) {\n return (req, res, next) => {\n Promise.resolve(fn(req, res, next)).catch(next);\n };\n}\n\nfunction bearerToken(req) {\n const delegated = readText(req.get('x-dacheng-auth-token'));\n if (\n delegated &&\n delegated !== 'DACHENG_AUTH_TOKEN' &&\n !delegated.startsWith('secretref-env:') &&\n delegated !== 'dacheng-openclaw-proxy'\n ) {\n return delegated;\n }\n const header = req.get('Authorization') || '';\n if (!header.startsWith('Bearer ')) return '';\n const token = header.slice('Bearer '.length).trim();\n return token === 'dacheng-openclaw-proxy' ? '' : token;\n}\n\nfunction sha256(value) {\n return crypto.createHash('sha256').update(value).digest('hex');\n}\n\nfunction safeUserText(value, fallback = '') {\n if (typeof value !== 'string') return fallback;\n return value.replace(/\\u0000/g, '').trim();\n}\n\nfunction estimateTokens(text) {\n const normalized = String(text || '').trim();\n if (!normalized) return 0;\n return Math.max(1, Math.ceil(normalized.length / 3));\n}\n\nfunction readText(value) {\n if (value === undefined || value === null) return '';\n return String(value).replace(/\\u0000/g, '').trim();\n}\n\nfunction clientContextMessage(client) {\n if (!client || typeof client !== 'object') return '';\n const selectedModel = readText(client.selectedModel || client.model);\n const project = client.project && typeof client.project === 'object' ? client.project : {};\n const projectName = readText(project.name);\n const projectPath = readText(project.path);\n const lines = ['桌面端上下文:'];\n if (selectedModel) lines.push(`用户选择的 DeepSeek 模型:${normalizeDeepSeekModelName(selectedModel)}`);\n if (projectPath) {\n lines.push(`当前项目名称:${projectName || projectPath}`);\n lines.push(`当前项目路径:${projectPath}`);\n lines.push('如果本轮任务涉及读取、修改、运行、分析或整理项目,请优先围绕当前项目路径工作;不要擅自操作其他目录。');\n }\n return lines.length > 1 ? lines.join('\\n') : '';\n}\nfunction unwrapPayload(payload) {\n if (!payload || typeof payload !== 'object') return {};\n return payload.data && typeof payload.data === 'object' ? payload.data : payload;\n}\n\nfunction readFirstText(values) {\n for (const value of values) {\n const text = readText(value);\n if (text) return text;\n }\n return '';\n}\n\nfunction activeFlag(value) {\n if (value === true) return true;\n if (value === 1) return true;\n if (typeof value !== 'string') return false;\n return ['true', '1', 'yes', 'active'].includes(value.trim().toLowerCase());\n}\n\nfunction futureDate(value) {\n const text = readText(value);\n if (!text) return false;\n const timestamp = Date.parse(text);\n return Number.isFinite(timestamp) && timestamp > Date.now();\n}\n\nfunction normalizeMembershipPayload(payload) {\n if (!payload || typeof payload !== 'object') return false;\n const data = unwrapPayload(payload);\n const membership = data.membership && typeof data.membership === 'object' ? data.membership : data;\n const type = readText(membership.type || membership.membershipType || data.membershipType).toLowerCase();\n const status = readText(membership.status || data.status).toLowerCase();\n const expiry = membership.expiresAt ||\n membership.expires_at ||\n membership.membershipExpiry ||\n membership.membership_expires_at ||\n data.membershipExpiry ||\n data.membership_expires_at;\n const hasExplicitActiveFlag =\n membership.isActive !== undefined ||\n membership.active !== undefined ||\n membership.hasPremiumAccess !== undefined ||\n membership.hasPremiumMembership !== undefined ||\n membership.isPremium !== undefined;\n\n if (hasExplicitActiveFlag) {\n return Boolean(\n activeFlag(membership.isActive) ||\n activeFlag(membership.active) ||\n activeFlag(membership.hasPremiumAccess) ||\n activeFlag(membership.hasPremiumMembership) ||\n activeFlag(membership.isPremium),\n );\n }\n\n return Boolean(\n status === 'active' ||\n type === 'paid' ||\n type === 'premium' ||\n type === 'trial' ||\n futureDate(expiry),\n );\n}\n\nfunction extractRemoteAccount(payload) {\n const data = unwrapPayload(payload);\n const nestedUser = data.user && typeof data.user === 'object' ? data.user : {};\n const id = readFirstText([\n data.userId,\n data.user_id,\n data.userNo,\n data.user_no,\n data.id,\n nestedUser.userId,\n nestedUser.user_id,\n nestedUser.userNo,\n nestedUser.user_no,\n nestedUser.id,\n ]);\n const username = readFirstText([data.username, data.name, nestedUser.username, nestedUser.name]);\n const email = readFirstText([data.email, nestedUser.email]);\n const membership = data.membership && typeof data.membership === 'object' ? data.membership : null;\n return {\n authenticated: Boolean(id || username || email || membership || data.success === true),\n id,\n username,\n email,\n isMember: normalizeMembershipPayload(payload),\n membership,\n };\n}\n\nasync function resolveRemoteAccount(token) {\n const fallback = {\n authenticated: false,\n id: '',\n username: '',\n email: '',\n isMember: false,\n membership: null,\n };\n if (!token || !fabushiApiBaseUrl) return fallback;\n\n const endpoints = [\n '/api/stripe/membership-status',\n '/api/alipay/check-membership',\n '/api/auth/user-info',\n ];\n\n for (const endpoint of endpoints) {\n try {\n const response = await fetch(`${fabushiApiBaseUrl}${endpoint}`, {\n headers: {\n Accept: 'application/json',\n Authorization: `Bearer ${token}`,\n },\n signal: AbortSignal.timeout(8_000),\n });\n if (response.status === 401 || response.status === 403) return fallback;\n if (!response.ok) continue;\n const payload = await response.json();\n const account = extractRemoteAccount(payload);\n if (account.authenticated || account.isMember) {\n return {\n ...fallback,\n ...account,\n authenticated: true,\n };\n }\n } catch (error) {\n logger.debug({ endpoint, error: String(error) }, 'membership lookup skipped');\n }\n }\n\n return fallback;\n}\n\nasync function resolveUser(req, body = {}) {\n const token = bearerToken(req);\n const usernameHint = safeUserText(body.username || req.query.username);\n\n const testToken = process.env.TEST_ACCOUNT_TOKEN || 'dacheng-test-token-bypass-limits';\n if (token && token === testToken) {\n return {\n userId: 'user:test_account',\n username: 'TestAccount',\n tokenHash: sha256(token).slice(0, 24),\n isAuthenticated: true,\n isMember: true,\n isTestAccount: true,\n membership: { type: 'lifetime', active: true },\n };\n }\n\n const tokenHash = token ? sha256(token).slice(0, 24) : '';\n const anonymousHash = sha256(`${req.ip}|${req.get('User-Agent') || ''}`).slice(0, 24);\n const remoteAccount = await resolveRemoteAccount(token);\n const remoteIdentity = remoteAccount.id ||\n remoteAccount.username ||\n remoteAccount.email ||\n (remoteAccount.authenticated ? tokenHash : '');\n const userId = remoteAccount.authenticated\n ? `user:${remoteIdentity}`\n : tokenHash\n ? `token:${tokenHash}`\n : `anon:${anonymousHash}`;\n return {\n userId,\n username: remoteAccount.username || usernameHint,\n tokenHash,\n isAuthenticated: remoteAccount.authenticated,\n isMember: remoteAccount.isMember,\n membership: remoteAccount.membership,\n };\n}\n\nfunction usageFor(userId) {\n const month = monthKey();\n const row = statements.getUsage.get(userId, month);\n return {\n month,\n used: Number(row?.totalTokens || 0),\n };\n}\n\nfunction enforceTokenBudget(user, estimatedTokensForRequest) {\n if (user && user.isTestAccount) {\n return { limit: 999999999, usage: { month: monthKey(), used: 0 } };\n }\n if (requireMemberForAi && !user.isMember) {\n const error = new Error('大乘 AI 需要开通会员后使用');\n error.statusCode = 403;\n error.code = 'AI_MEMBERSHIP_REQUIRED';\n error.details = {\n requireMemberForAi,\n monthlyLimit: memberMonthlyLimit,\n remainingTokens: 0,\n };\n throw error;\n }\n\n const limit = user.isMember ? memberMonthlyLimit : freeMonthlyLimit;\n const usage = usageFor(user.userId);\n if (usage.used + estimatedTokensForRequest > limit) {\n const error = new Error('本月 AI token 额度已不足');\n error.statusCode = 429;\n error.code = 'USER_TOKEN_QUOTA_EXCEEDED';\n error.details = {\n monthlyLimit: limit,\n usedTokens: usage.used,\n remainingTokens: Math.max(0, limit - usage.used),\n };\n throw error;\n }\n return { limit, usage };\n}\n\nfunction completionBudgetFor(budget, estimatedPromptTokens) {\n const remainingAfterPrompt = budget.limit - budget.usage.used - estimatedPromptTokens;\n return Math.max(1, Math.min(deepseekMaxCompletionTokens, Math.floor(remainingAfterPrompt)));\n}\n\nfunction recordUsage(userId, totalTokens) {\n if (!totalTokens || totalTokens < 1) return;\n statements.upsertUsage.run({\n userId,\n month: monthKey(),\n totalTokens,\n updatedAt: nowIso(),\n });\n}\n\nfunction titleFromMessage(message) {\n const compact = message.replace(/\\s+/g, ' ').trim();\n if (!compact) return '新对话';\n return compact.length > 22 ? `${compact.slice(0, 22)}...` : compact;\n}\n\nfunction normalizeMessages(rows) {\n return rows.map((row) => ({\n role: row.role,\n content: row.content,\n }));\n}\n\nfunction normalizeMaxCompletionTokens(value) {\n const numeric = Number(value || deepseekMaxCompletionTokens);\n if (!Number.isFinite(numeric) || numeric < 1) return deepseekMaxCompletionTokens;\n return Math.max(1, Math.min(deepseekMaxCompletionTokens, Math.floor(numeric)));\n}\n\nfunction normalizeDeepSeekModelName(value) {\n const raw = readText(value || deepseekModel);\n if (!raw) return deepseekModel;\n const modelId = raw.includes('/') ? raw.split('/').pop() : raw;\n return modelId || deepseekModel;\n}\n\nasync function callDeepSeek(messages, options = {}) {\n if (!deepseekApiKey) {\n const error = new Error('DeepSeek API key is not configured');\n error.statusCode = 503;\n throw error;\n }\n\n const model = normalizeDeepSeekModelName(options.model);\n const response = await fetch(`${deepseekBaseUrl}/chat/completions`, {\n method: 'POST',\n headers: {\n Accept: 'application/json',\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${deepseekApiKey}`,\n },\n body: JSON.stringify({\n model,\n messages,\n temperature: 0.4,\n max_tokens: normalizeMaxCompletionTokens(options.maxCompletionTokens),\n stream: false,\n }),\n signal: AbortSignal.timeout(60_000),\n });\n\n const bodyText = await response.text();\n let payload;\n try {\n payload = bodyText ? JSON.parse(bodyText) : {};\n } catch {\n payload = { error: bodyText };\n }\n\n if (!response.ok) {\n const upstreamMessage = payload?.error?.message || payload?.message || '';\n const friendlyMessage =\n response.status === 402 || /insufficient\\s+balance/i.test(upstreamMessage)\n ? '大乘 AI 服务额度暂不可用,请联系管理员充值 DeepSeek 账户后重试。'\n : upstreamMessage || `DeepSeek request failed: ${response.status}`;\n const error = new Error(friendlyMessage);\n error.statusCode = response.status;\n if (response.status === 402 || /insufficient\\s+balance/i.test(upstreamMessage)) {\n error.code = 'DEEPSEEK_PROVIDER_QUOTA';\n }\n throw error;\n }\n\n const message = payload?.choices?.[0]?.message?.content?.trim();\n if (!message) {\n const error = new Error('DeepSeek returned an empty response');\n error.statusCode = 502;\n throw error;\n }\n\n return {\n message,\n model,\n usage: {\n promptTokens: Number(payload?.usage?.prompt_tokens || 0),\n completionTokens: Number(payload?.usage?.completion_tokens || 0),\n totalTokens: Number(payload?.usage?.total_tokens || 0),\n },\n };\n}\n\nasync function callDeepSeekStream(messages, callbacks = {}) {\n if (!deepseekApiKey) {\n const error = new Error('DeepSeek API key is not configured');\n error.statusCode = 503;\n throw error;\n }\n\n const model = normalizeDeepSeekModelName(callbacks.model);\n const response = await fetch(`${deepseekBaseUrl}/chat/completions`, {\n method: 'POST',\n headers: {\n Accept: 'text/event-stream',\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${deepseekApiKey}`,\n },\n body: JSON.stringify({\n model,\n messages,\n temperature: 0.4,\n max_tokens: normalizeMaxCompletionTokens(callbacks.maxCompletionTokens),\n stream: true,\n stream_options: { include_usage: true },\n }),\n signal: callbacks.signal || AbortSignal.timeout(90_000),\n });\n\n if (!response.ok) {\n const bodyText = await response.text();\n let payload;\n try {\n payload = bodyText ? JSON.parse(bodyText) : {};\n } catch {\n payload = { error: bodyText };\n }\n const upstreamMessage = payload?.error?.message || payload?.message || '';\n const friendlyMessage =\n response.status === 402 || /insufficient\\s+balance/i.test(upstreamMessage)\n ? '大乘 AI 服务额度暂不可用,请联系管理员充值 DeepSeek 账户后重试。'\n : upstreamMessage || `DeepSeek request failed: ${response.status}`;\n const error = new Error(friendlyMessage);\n error.statusCode = response.status;\n if (response.status === 402 || /insufficient\\s+balance/i.test(upstreamMessage)) {\n error.code = 'DEEPSEEK_PROVIDER_QUOTA';\n }\n throw error;\n }\n\n let buffer = '';\n let message = '';\n let usage = {\n promptTokens: 0,\n completionTokens: 0,\n totalTokens: 0,\n };\n const decoder = new TextDecoder();\n\n for await (const chunk of response.body) {\n buffer += decoder.decode(chunk, { stream: true });\n const lines = buffer.split(/\\r?\\n/);\n buffer = lines.pop() || '';\n\n for (const rawLine of lines) {\n const line = rawLine.trim();\n if (!line.startsWith('data:')) continue;\n const data = line.slice('data:'.length).trim();\n if (!data || data === '[DONE]') continue;\n\n let payload;\n try {\n payload = JSON.parse(data);\n } catch {\n continue;\n }\n\n const delta = payload?.choices?.[0]?.delta?.content || '';\n if (delta) {\n message += delta;\n callbacks.onToken?.(delta);\n }\n\n if (payload?.usage) {\n usage = {\n promptTokens: Number(payload.usage.prompt_tokens || usage.promptTokens || 0),\n completionTokens: Number(payload.usage.completion_tokens || usage.completionTokens || 0),\n totalTokens: Number(payload.usage.total_tokens || usage.totalTokens || 0),\n };\n }\n }\n }\n\n message = message.trim();\n if (!message) {\n const error = new Error('DeepSeek returned an empty response');\n error.statusCode = 502;\n throw error;\n }\n\n return { message, model, usage };\n}\n\nfunction createCodexDeepSeekRuntime() {\n if (!enableCodexSdkChat || !deepseekApiKey) {\n return {\n enabled: false,\n provider: 'deepseek-direct',\n reason: enableCodexSdkChat ? 'DeepSeek API key is not configured' : 'ENABLE_CODEX_SDK_CHAT is not true',\n };\n }\n\n return {\n enabled: true,\n provider: 'codex-sdk-deepseek',\n options: {\n apiKey: deepseekApiKey,\n baseUrl: deepseekBaseUrl,\n env: {\n ...process.env,\n DEEPSEEK_API_KEY: deepseekApiKey,\n HOME: codexHomeDir,\n CODEX_HOME: codexHomeDir,\n TMPDIR: codexTempDir,\n XDG_RUNTIME_DIR: codexRuntimeDir,\n XDG_CACHE_HOME: path.join(codexHomeDir, '.cache'),\n XDG_CONFIG_HOME: path.join(codexHomeDir, '.config'),\n XDG_DATA_HOME: path.join(codexHomeDir, '.local', 'share'),\n PATH: process.env.PATH || '/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin',\n },\n config: {\n model_provider: codexDeepSeekProviderId,\n model_providers: {\n [codexDeepSeekProviderId]: {\n name: 'DeepSeek Chat Completions',\n base_url: codexResponsesBaseUrl,\n env_key: 'DEEPSEEK_API_KEY',\n wire_api: 'responses',\n query_params: {},\n },\n },\n },\n },\n threadOptions: {\n model: deepseekModel,\n approvalPolicy: 'never',\n sandboxMode: 'read-only',\n skipGitRepoCheck: true,\n networkAccessEnabled: true,\n },\n };\n}\n\nfunction codexPromptFromMessages(messages) {\n return messages\n .map((message) => {\n const role = String(message.role || 'user').toUpperCase();\n return `${role}:\\n${message.content}`;\n })\n .join('\\n\\n');\n}\n\nfunction responseContentText(content) {\n if (typeof content === 'string') return content;\n if (!Array.isArray(content)) return '';\n return content\n .map((part) => {\n if (typeof part === 'string') return part;\n if (!part || typeof part !== 'object') return '';\n return String(part.text || part.input_text || part.output_text || '');\n })\n .filter(Boolean)\n .join('\\n');\n}\n\nfunction codexResponsesPrompt(body) {\n const input = Array.isArray(body?.input) ? body.input : [];\n const userItems = input.filter((item) => item?.role === 'user');\n const source = userItems[userItems.length - 1] || input[input.length - 1];\n const prompt = responseContentText(source?.content).trim();\n if (prompt) return prompt;\n if (typeof body?.input === 'string') return body.input.trim();\n return '';\n}\n\nfunction writeResponsesEvent(res, event, data) {\n res.write(`event: ${event}\\n`);\n res.write(`data: ${JSON.stringify(data)}\\n\\n`);\n}\n\nfunction responseUsage(usage = {}) {\n return {\n input_tokens: Number(usage.promptTokens || usage.input_tokens || 0),\n cached_input_tokens: 0,\n output_tokens: Number(usage.completionTokens || usage.output_tokens || 0),\n reasoning_output_tokens: 0,\n total_tokens: Number(usage.totalTokens || usage.total_tokens || 0),\n };\n}\n\nfunction createLibreChatAgentRuntime() {\n if (!enableLibreChatAgentChat || !libreChatAgentApiKey || !libreChatAgentId) {\n return {\n enabled: false,\n provider: 'librechat-agent',\n reason: !enableLibreChatAgentChat\n ? 'ENABLE_LIBRECHAT_AGENT_CHAT is not true'\n : !libreChatAgentApiKey\n ? 'LIBRECHAT_AGENT_API_KEY is not configured'\n : 'LIBRECHAT_AGENT_ID is not configured',\n };\n }\n\n return {\n enabled: true,\n provider: 'librechat-agent',\n model: libreChatAgentId,\n chatCompletionsUrl: `${libreChatAgentsBaseUrl}/chat/completions`,\n };\n}\n\nfunction libreChatUsage(usage = {}) {\n return {\n promptTokens: Number(usage.prompt_tokens || usage.input_tokens || usage.promptTokens || 0),\n completionTokens: Number(usage.completion_tokens || usage.output_tokens || usage.completionTokens || 0),\n totalTokens: Number(usage.total_tokens || usage.totalTokens || 0),\n };\n}\n\nasync function callLibreChatAgent(messages) {\n const runtime = createLibreChatAgentRuntime();\n if (!runtime.enabled) {\n const error = new Error(runtime.reason || 'LibreChat Agent chat is not enabled');\n error.statusCode = 503;\n throw error;\n }\n\n const response = await fetch(runtime.chatCompletionsUrl, {\n method: 'POST',\n headers: {\n Accept: 'application/json',\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${libreChatAgentApiKey}`,\n },\n body: JSON.stringify({\n model: runtime.model,\n messages,\n temperature: 0.4,\n stream: false,\n }),\n signal: AbortSignal.timeout(120_000),\n });\n\n const bodyText = await response.text();\n let payload;\n try {\n payload = bodyText ? JSON.parse(bodyText) : {};\n } catch {\n payload = { error: bodyText };\n }\n\n if (!response.ok) {\n const upstreamMessage = payload?.error?.message || payload?.message || bodyText || '';\n const error = new Error(upstreamMessage || `LibreChat Agent request failed: ${response.status}`);\n error.statusCode = response.status;\n throw error;\n }\n\n const message = payload?.choices?.[0]?.message?.content?.trim();\n if (!message) {\n const error = new Error('LibreChat Agent returned an empty response');\n error.statusCode = 502;\n throw error;\n }\n\n return {\n message,\n usage: libreChatUsage(payload?.usage),\n };\n}\n\nasync function callLibreChatAgentStream(messages, callbacks = {}) {\n const runtime = createLibreChatAgentRuntime();\n if (!runtime.enabled) {\n const error = new Error(runtime.reason || 'LibreChat Agent chat is not enabled');\n error.statusCode = 503;\n throw error;\n }\n\n const response = await fetch(runtime.chatCompletionsUrl, {\n method: 'POST',\n headers: {\n Accept: 'text/event-stream',\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${libreChatAgentApiKey}`,\n },\n body: JSON.stringify({\n model: runtime.model,\n messages,\n temperature: 0.4,\n stream: true,\n }),\n signal: callbacks.signal || AbortSignal.timeout(180_000),\n });\n\n if (!response.ok) {\n const bodyText = await response.text();\n let payload;\n try {\n payload = bodyText ? JSON.parse(bodyText) : {};\n } catch {\n payload = { error: bodyText };\n }\n const upstreamMessage = payload?.error?.message || payload?.message || bodyText || '';\n const error = new Error(upstreamMessage || `LibreChat Agent request failed: ${response.status}`);\n error.statusCode = response.status;\n throw error;\n }\n\n let buffer = '';\n let message = '';\n let usage = { promptTokens: 0, completionTokens: 0, totalTokens: 0 };\n const decoder = new TextDecoder();\n\n for await (const chunk of response.body) {\n buffer += decoder.decode(chunk, { stream: true });\n const lines = buffer.split(/\\r?\\n/);\n buffer = lines.pop() || '';\n\n for (const rawLine of lines) {\n const line = rawLine.trim();\n if (!line.startsWith('data:')) continue;\n const data = line.slice('data:'.length).trim();\n if (!data || data === '[DONE]') continue;\n\n let payload;\n try {\n payload = JSON.parse(data);\n } catch {\n continue;\n }\n\n const delta = payload?.choices?.[0]?.delta?.content || '';\n if (delta) {\n message += delta;\n callbacks.onToken?.(delta);\n }\n\n if (payload?.usage) {\n usage = libreChatUsage(payload.usage);\n }\n }\n }\n\n message = message.trim();\n if (!message) {\n const error = new Error('LibreChat Agent returned an empty response');\n error.statusCode = 502;\n throw error;\n }\n\n return { message, usage };\n}\n\nfunction isDeepSeekQuotaError(error) {\n if (error?.code === 'DEEPSEEK_PROVIDER_QUOTA') return true;\n if (error?.code === 'USER_TOKEN_QUOTA_EXCEEDED' || error?.code === 'AI_MEMBERSHIP_REQUIRED') return false;\n const message = String(error?.message || error || '');\n return /insufficient\\s+balance|balance/i.test(message);\n}\n\nfunction publicAiErrorMessage(error) {\n if (isDeepSeekQuotaError(error)) {\n return '大乘 AI 服务额度暂不可用,请联系管理员充值 DeepSeek 账户后重试。';\n }\n return error?.message || 'Internal Server Error';\n}\n\nfunction responsesPayload({\n responseId,\n itemId,\n status,\n model,\n text = '',\n usage = null,\n error = null,\n}) {\n return {\n id: responseId,\n object: 'response',\n status,\n model,\n output:\n status === 'completed'\n ? [\n {\n id: itemId,\n type: 'message',\n role: 'assistant',\n status: 'completed',\n content: [{ type: 'output_text', text }],\n },\n ]\n : [],\n ...(usage ? { usage: responseUsage(usage) } : {}),\n ...(error\n ? {\n error: {\n message: error.message || String(error),\n type: 'server_error',\n },\n }\n : {}),\n };\n}\n\nasync function callCodexSdkDeepSeek(messages, callbacks = {}) {\n const codexRuntime = createCodexDeepSeekRuntime();\n if (!codexRuntime.enabled) {\n const error = new Error(codexRuntime.reason || 'Codex SDK chat is not enabled');\n error.statusCode = 503;\n throw error;\n }\n\n const { Codex } = await import('@openai/codex-sdk');\n const codex = new Codex(codexRuntime.options);\n const thread = codex.startThread(codexRuntime.threadOptions);\n const prompt = codexPromptFromMessages(messages);\n\n if (callbacks.onToken || callbacks.onStep) {\n const { events } = await thread.runStreamed(prompt, { signal: callbacks.signal });\n let finalResponse = '';\n let usage = null;\n\n for await (const event of events) {\n if (event.type === 'item.completed') {\n const item = event.item;\n if (item.type === 'agent_message') {\n finalResponse = item.text || finalResponse;\n callbacks.onToken?.(item.text || '');\n } else if (item.type === 'todo_list') {\n callbacks.onStep?.({\n title: 'Codex SDK 执行计划',\n message: item.items.map((todo) => `${todo.completed ? '✓' : '•'} ${todo.text}`).join('\\n'),\n });\n } else if (item.type === 'command_execution') {\n callbacks.onStep?.({\n title: 'Codex SDK 执行命令',\n message: item.command,\n });\n } else if (item.type === 'mcp_tool_call') {\n callbacks.onStep?.({\n title: 'Codex SDK 调用工具',\n message: `${item.server}.${item.tool}`,\n });\n }\n } else if (event.type === 'turn.completed') {\n usage = event.usage;\n } else if (event.type === 'turn.failed') {\n throw new Error(event.error?.message || 'Codex SDK turn failed');\n } else if (event.type === 'error') {\n throw new Error(event.message || 'Codex SDK stream failed');\n }\n }\n\n if (!finalResponse.trim()) {\n const error = new Error('Codex SDK returned an empty response');\n error.statusCode = 502;\n throw error;\n }\n\n return {\n message: finalResponse.trim(),\n usage: {\n promptTokens: Number(usage?.input_tokens || 0),\n completionTokens: Number(usage?.output_tokens || 0),\n totalTokens: Number((usage?.input_tokens || 0) + (usage?.output_tokens || 0)),\n },\n };\n }\n\n const turn = await thread.run(prompt);\n if (!turn.finalResponse?.trim()) {\n const error = new Error('Codex SDK returned an empty response');\n error.statusCode = 502;\n throw error;\n }\n\n return {\n message: turn.finalResponse.trim(),\n usage: {\n promptTokens: Number(turn.usage?.input_tokens || 0),\n completionTokens: Number(turn.usage?.output_tokens || 0),\n totalTokens: Number((turn.usage?.input_tokens || 0) + (turn.usage?.output_tokens || 0)),\n },\n };\n}\n\nconst activeAgentRuns = new Map();\n\nfunction normalizeAgentMode(value) {\n const mode = safeUserText(value || 'dharma_guide');\n return /^[a-z][a-z0-9_:-]{0,48}$/i.test(mode) ? mode : 'dharma_guide';\n}\n\nfunction openClawSessionKey(user, conversationId) {\n const env = process.env.NODE_ENV === 'production' ? 'prod' : 'dev';\n return `fabushi:env:${env}:user:${user.userId}:conversation:${conversationId}`;\n}\n\nfunction createOpenClawRuntime() {\n if (!enableOpenClawAgentChat || !openClawGatewayToken || !openClawGatewayUrl) {\n return {\n enabled: false,\n provider: 'openclaw-gateway',\n reason: !enableOpenClawAgentChat\n ? 'ENABLE_OPENCLAW_AGENT_CHAT is not true'\n : !openClawGatewayToken\n ? 'OPENCLAW_GATEWAY_TOKEN is not configured'\n : 'OPENCLAW_GATEWAY_URL is not configured',\n };\n }\n\n return {\n enabled: true,\n provider: 'openclaw-gateway',\n model: openClawAgentId,\n runsUrl: openClawRunsEndpoint || `${openClawGatewayUrl}/v1/agents/${encodeURIComponent(openClawAgentId)}/runs`,\n };\n}\n\nfunction extractOpenClawText(payload) {\n if (!payload || typeof payload !== 'object') return '';\n const candidates = [\n payload.message,\n payload.content,\n payload.output_text,\n payload.finalResponse,\n payload.final_response,\n payload.result?.message,\n payload.result?.content,\n payload.output?.message,\n payload.output?.content,\n payload.choices?.[0]?.message?.content,\n ];\n for (const candidate of candidates) {\n if (typeof candidate === 'string' && candidate.trim()) return candidate.trim();\n }\n return '';\n}\n\nfunction extractOpenClawUsage(payload) {\n const usage = payload?.usage || payload?.result?.usage || {};\n return {\n promptTokens: Number(usage.input_tokens || usage.prompt_tokens || usage.promptTokens || 0),\n completionTokens: Number(usage.output_tokens || usage.completion_tokens || usage.completionTokens || 0),\n totalTokens: Number(usage.total_tokens || usage.totalTokens || 0),\n };\n}\n\nasync function callOpenClawAgent({ messages, user, conversationId, mode, signal }) {\n const runtime = createOpenClawRuntime();\n if (!runtime.enabled) {\n const error = new Error(runtime.reason || 'OpenClaw Agent chat is not enabled');\n error.statusCode = 503;\n throw error;\n }\n\n const sessionKey = openClawSessionKey(user, conversationId);\n const response = await fetch(runtime.runsUrl, {\n method: 'POST',\n headers: {\n Accept: 'application/json',\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${openClawGatewayToken}`,\n },\n body: JSON.stringify({\n agentId: openClawAgentId,\n sessionKey,\n mode,\n stream: false,\n input: { messages },\n messages,\n metadata: {\n app: 'fabushi',\n userId: user.userId,\n conversationId,\n toolPolicy: 'public-user-read-mostly',\n },\n }),\n signal: signal || AbortSignal.timeout(180_000),\n });\n\n const bodyText = await response.text();\n let payload;\n try {\n payload = bodyText ? JSON.parse(bodyText) : {};\n } catch {\n payload = { message: bodyText };\n }\n\n if (!response.ok) {\n const error = new Error(payload?.error?.message || payload?.message || `OpenClaw request failed: ${response.status}`);\n error.statusCode = response.status;\n throw error;\n }\n\n const message = extractOpenClawText(payload);\n if (!message) {\n const error = new Error('OpenClaw returned an empty response');\n error.statusCode = 502;\n throw error;\n }\n\n return {\n message,\n runId: payload.runId || payload.id || payload.result?.runId || null,\n provider: runtime.provider,\n model: runtime.model,\n usage: extractOpenClawUsage(payload),\n };\n}\n\nasync function runAgentModel({ messages, user, conversationId, mode, callbacks = {}, signal, maxCompletionTokens }) {\n const openClawRuntime = createOpenClawRuntime();\n if (openClawRuntime.enabled && !callbacks.onToken) {\n return await callOpenClawAgent({ messages, user, conversationId, mode, signal });\n }\n\n const codexRuntime = createCodexDeepSeekRuntime();\n const libreChatRuntime = createLibreChatAgentRuntime();\n let provider = libreChatRuntime.enabled\n ? libreChatRuntime.provider\n : codexRuntime.enabled\n ? codexRuntime.provider\n : 'deepseek';\n let model = libreChatRuntime.enabled ? libreChatRuntime.model : deepseekModel;\n\n try {\n if (libreChatRuntime.enabled) {\n const result = callbacks.onToken\n ? await callLibreChatAgentStream(messages, { ...callbacks, signal })\n : await callLibreChatAgent(messages);\n return { ...result, provider, model };\n }\n if (codexRuntime.enabled) {\n const result = await callCodexSdkDeepSeek(messages, { ...callbacks, signal });\n return { ...result, provider, model };\n }\n const result = callbacks.onToken\n ? await callDeepSeekStream(messages, { ...callbacks, signal, maxCompletionTokens })\n : await callDeepSeek(messages, { maxCompletionTokens });\n return { ...result, provider, model };\n } catch (error) {\n if (!codexRuntime.enabled || isDeepSeekQuotaError(error)) throw error;\n logger.warn({ error: String(error) }, 'Agent model failed, falling back to DeepSeek direct');\n const result = callbacks.onToken\n ? await callDeepSeekStream(messages, { ...callbacks, signal, maxCompletionTokens })\n : await callDeepSeek(messages, { maxCompletionTokens });\n return { ...result, provider: 'deepseek', model: deepseekModel };\n }\n}\n\nfunction updateAgentRunStatus(run, status, fields = {}) {\n statements.updateAgentRunStatus.run({\n id: run.id,\n userId: run.user_id || run.userId,\n status,\n openClawRunId: fields.openClawRunId ?? null,\n inputTokens: fields.inputTokens ?? null,\n outputTokens: fields.outputTokens ?? null,\n toolCallCount: fields.toolCallCount ?? null,\n completedAt: status === 'completed' || status === 'cancelled' ? nowIso() : null,\n failedAt: status === 'failed' ? nowIso() : null,\n errorCode: fields.errorCode ?? null,\n errorMessage: fields.errorMessage ?? null,\n });\n}\n\nfunction buildAgentMessages(conversationId) {\n const historyRows = statements.listMessages.all(conversationId).slice(-14);\n return [{ role: 'system', content: systemPrompt }, ...normalizeMessages(historyRows)];\n}\n\nfunction agentUsagePayload(usage, user, budget) {\n const promptTokens = usage.promptTokens || usage.inputTokens || 0;\n const completionTokens = usage.completionTokens || usage.outputTokens || 0;\n const totalTokens = usage.totalTokens || promptTokens + completionTokens;\n const latestUsage = usageFor(user.userId);\n return {\n inputTokens: promptTokens,\n outputTokens: completionTokens,\n totalTokens,\n monthlyLimit: budget.limit,\n remainingTokens: Math.max(0, budget.limit - latestUsage.used),\n };\n}\n\nfunction openAiContentToText(content) {\n if (typeof content === 'string') return content;\n if (Array.isArray(content)) {\n return content\n .map((part) => {\n if (typeof part === 'string') return part;\n if (!part || typeof part !== 'object') return '';\n return readText(part.text || part.content || part.value);\n })\n .filter(Boolean)\n .join('\\n');\n }\n return readText(content);\n}\n\nfunction normalizeOpenAiMessages(value) {\n if (!Array.isArray(value)) return [];\n const allowedRoles = new Set(['system', 'user', 'assistant', 'tool']);\n return value\n .map((item) => {\n if (!item || typeof item !== 'object') return null;\n const role = allowedRoles.has(item.role) ? item.role : 'user';\n const content = openAiContentToText(item.content);\n if (!content) return null;\n return { role, content };\n })\n .filter(Boolean);\n}\n\nfunction truncateForDeepSeekMessage(text, maxChars = 12_000) {\n const value = readText(text);\n if (value.length <= maxChars) return value;\n return `${value.slice(0, maxChars)}\\n\\n[内容已截断]`;\n}\n\nfunction truncateTailForDeepSeekMessage(text, maxChars = 2400) {\n const value = readText(text);\n if (value.length <= maxChars) return value;\n return `[前文已省略]\\n\\n${value.slice(-maxChars)}`;\n}\n\nfunction stripOpenClawPromptNoise(text) {\n const value = readText(text);\n if (!value) return '';\n\n const finalPromptMatch = value.match(/finalPromptText[\"'\\s::]+([\\s\\S]{1,4000})$/i);\n if (finalPromptMatch?.[1]) {\n return readText(finalPromptMatch[1].replace(/^[\"'`]+|[\"'`,}\\]]+$/g, ''));\n }\n\n const markers = [\n 'final prompt:',\n 'final user prompt:',\n 'user prompt:',\n '用户问题:',\n '用户消息:',\n '用户输入:',\n ];\n const lower = value.toLowerCase();\n for (const marker of markers) {\n const index = lower.lastIndexOf(marker.toLowerCase());\n if (index >= 0) {\n const tail = value.slice(index + marker.length).trim();\n if (tail) return tail;\n }\n }\n\n return value;\n}\n\nfunction compactOpenClawMessagesForDeepSeek(messages) {\n const conversationalMessages = messages\n .filter((message) => message.role === 'user' || message.role === 'assistant')\n .map((message) => ({\n role: message.role,\n content: truncateTailForDeepSeekMessage(stripOpenClawPromptNoise(message.content)),\n }))\n .filter((message) => message.content);\n\n const recent = conversationalMessages.slice(-6);\n while (recent.length > 0 && recent[0].role !== 'user') {\n recent.shift();\n }\n\n if (recent.length === 0) {\n const fallback = messages\n .slice()\n .reverse()\n .map((message) => truncateTailForDeepSeekMessage(stripOpenClawPromptNoise(message.content), 2400))\n .find(Boolean);\n if (!fallback) return messages;\n recent.push({ role: 'user', content: fallback });\n }\n\n return [\n {\n role: 'system',\n content: '你是大乘 App 的桌面 AI 助手。请直接回答用户问题,默认使用简洁、准确的中文。',\n },\n ...recent,\n ];\n}\n\nfunction isDeepSeekBlockedError(error) {\n return (\n Number(error?.statusCode) === 403 &&\n /blocked|被拦截|forbidden/i.test(String(error?.message || error || ''))\n );\n}\n\nfunction shouldRetryWithCompactedOpenClawMessages(error, messages, compactedMessages) {\n if (!isDeepSeekBlockedError(error)) return false;\n return JSON.stringify(messages) !== JSON.stringify(compactedMessages);\n}\n\nfunction openClawBlockedFallbackText(messages) {\n const lastUser = messages\n .slice()\n .reverse()\n .find((message) => message.role === 'user')?.content || '';\n const localActionIntent =\n /(file|folder|directory|project|workspace|path|文件|文件夹|目录|项目|工程|路径|本地)/i.test(lastUser) &&\n /(create|make|mkdir|write|edit|modify|rename|delete|move|copy|save|download|read|list|生成|创建|新建|写入|编辑|修改|重命名|删除|移动|复制|保存|下载|读取|列出)/i.test(lastUser);\n\n if (localActionIntent) {\n return [\n '本机工具规划被模型服务拦截了,所以这次没有实际读取、创建、修改或下载本机文件。',\n '如果只是新建文件夹,请先在底部选择项目后重新发送“新建文件夹”;桌面端会直接在已选项目里创建。',\n '如果是更复杂的本地文件操作,请在“连应用”里重启本机 AI 后重试,或把要操作的文件路径和目标说得更具体。',\n ].join('\\n');\n }\n\n return '模型服务拦截了这次回复,未能生成结果。请换一种更具体、更短的表述后重试。';\n}\nfunction usageWithFallback(usage, messages, text) {\n const promptTokens = Number(usage?.promptTokens || usage?.inputTokens || 0);\n const completionTokens = Number(usage?.completionTokens || usage?.outputTokens || 0);\n const totalTokens = Number(usage?.totalTokens || 0);\n if (totalTokens > 0) {\n return {\n promptTokens,\n completionTokens,\n totalTokens,\n };\n }\n const estimatedPrompt = promptTokens || estimateTokens(JSON.stringify(messages));\n const estimatedCompletion = completionTokens || estimateTokens(text);\n return {\n promptTokens: estimatedPrompt,\n completionTokens: estimatedCompletion,\n totalTokens: estimatedPrompt + estimatedCompletion,\n };\n}\n\nfunction openAiUsagePayload(usage, user, budget) {\n const promptTokens = Number(usage.promptTokens || 0);\n const completionTokens = Number(usage.completionTokens || 0);\n const totalTokens = Number(usage.totalTokens || promptTokens + completionTokens);\n const latestUsage = usageFor(user.userId);\n const remainingTokens = Math.max(0, budget.limit - latestUsage.used);\n return {\n prompt_tokens: promptTokens,\n completion_tokens: completionTokens,\n total_tokens: totalTokens,\n promptTokens,\n completionTokens,\n totalTokens,\n monthlyLimit: budget.limit,\n remainingTokens,\n };\n}\n\nfunction writeOpenAiSse(res, payload) {\n res.write(`data: ${JSON.stringify(payload)}\\n\\n`);\n}\n\napp.post(\n '/api/openclaw/deepseek/v1/chat/completions',\n asyncHandler(async (req, res) => {\n const user = await resolveUser(req, req.body || {});\n const incomingMessages = normalizeOpenAiMessages(req.body?.messages);\n if (incomingMessages.length === 0) {\n res.status(400).json({ error: { message: 'messages is required' } });\n return;\n }\n\n const messages = compactOpenClawMessagesForDeepSeek(incomingMessages);\n const promptEstimate = estimateTokens(JSON.stringify(messages));\n const estimated = promptEstimate + 600;\n const budget = enforceTokenBudget(user, estimated);\n const maxCompletionTokens = Math.min(\n completionBudgetFor(budget, promptEstimate),\n normalizeMaxCompletionTokens(req.body?.max_tokens),\n );\n const model = normalizeDeepSeekModelName(req.body?.model);\n const wantsStream =\n req.body?.stream === true ||\n String(req.get('accept') || '').includes('text/event-stream');\n const id = `chatcmpl_${crypto.randomUUID().replaceAll('-', '')}`;\n const created = Math.floor(Date.now() / 1000);\n const compactedMessages = messages;\n\n if (!wantsStream) {\n let result;\n let usageMessages = messages;\n try {\n result = await callDeepSeek(messages, { model, maxCompletionTokens });\n } catch (error) {\n if (!shouldRetryWithCompactedOpenClawMessages(error, messages, compactedMessages)) {\n throw error;\n }\n logger.warn(\n {\n statusCode: error?.statusCode,\n messageCount: messages.length,\n compactedMessageCount: compactedMessages.length,\n },\n 'OpenClaw DeepSeek proxy retrying with compacted messages',\n );\n usageMessages = compactedMessages;\n result = await callDeepSeek(compactedMessages, { model, maxCompletionTokens });\n }\n const usage = usageWithFallback(result.usage, usageMessages, result.message);\n recordUsage(user.userId, usage.totalTokens);\n res.json({\n id,\n object: 'chat.completion',\n created,\n model: result.model || model,\n choices: [\n {\n index: 0,\n message: { role: 'assistant', content: result.message },\n finish_reason: 'stop',\n },\n ],\n usage: openAiUsagePayload(usage, user, budget),\n });\n return;\n }\n\n const abortController = new AbortController();\n req.on('close', () => abortController.abort());\n\n res.status(200);\n res.set({\n 'Content-Type': 'text/event-stream; charset=utf-8',\n 'Cache-Control': 'no-cache, no-transform',\n Connection: 'keep-alive',\n 'X-Accel-Buffering': 'no',\n });\n res.flushHeaders?.();\n\n const baseChunk = {\n id,\n object: 'chat.completion.chunk',\n created,\n model,\n system_fingerprint: 'fabushi-openclaw-deepseek-proxy',\n };\n writeOpenAiSse(res, {\n ...baseChunk,\n choices: [{ index: 0, delta: { role: 'assistant' }, finish_reason: null }],\n });\n\n let text = '';\n let usageMessages = messages;\n try {\n let result;\n try {\n result = await callDeepSeekStream(messages, {\n model,\n maxCompletionTokens,\n signal: abortController.signal,\n onToken: (delta) => {\n text += delta;\n writeOpenAiSse(res, {\n ...baseChunk,\n choices: [{ index: 0, delta: { content: delta }, finish_reason: null }],\n });\n },\n });\n } catch (error) {\n if (!shouldRetryWithCompactedOpenClawMessages(error, messages, compactedMessages)) {\n throw error;\n }\n logger.warn(\n {\n statusCode: error?.statusCode,\n messageCount: messages.length,\n compactedMessageCount: compactedMessages.length,\n },\n 'OpenClaw DeepSeek proxy stream retrying with compacted messages',\n );\n text = '';\n usageMessages = compactedMessages;\n result = await callDeepSeekStream(compactedMessages, {\n model,\n maxCompletionTokens,\n signal: abortController.signal,\n onToken: (delta) => {\n text += delta;\n writeOpenAiSse(res, {\n ...baseChunk,\n choices: [{ index: 0, delta: { content: delta }, finish_reason: null }],\n });\n },\n });\n }\n text = result.message || text;\n const usage = usageWithFallback(result.usage, usageMessages, text);\n recordUsage(user.userId, usage.totalTokens);\n writeOpenAiSse(res, {\n ...baseChunk,\n model: result.model || model,\n choices: [{ index: 0, delta: {}, finish_reason: 'stop' }],\n });\n writeOpenAiSse(res, {\n ...baseChunk,\n model: result.model || model,\n choices: [],\n usage: openAiUsagePayload(usage, user, budget),\n });\n res.write('data: [DONE]\\n\\n');\n } catch (error) {\n if (isDeepSeekBlockedError(error)) {\n const fallbackMessage = openClawBlockedFallbackText(usageMessages);\n const usage = usageWithFallback(null, usageMessages, fallbackMessage);\n recordUsage(user.userId, usage.totalTokens);\n logger.warn(\n { statusCode: error?.statusCode, code: error?.code },\n 'OpenClaw DeepSeek proxy stream blocked; returning local fallback message',\n );\n writeOpenAiSse(res, {\n ...baseChunk,\n choices: [{ index: 0, delta: { content: fallbackMessage }, finish_reason: null }],\n });\n writeOpenAiSse(res, {\n ...baseChunk,\n choices: [{ index: 0, delta: {}, finish_reason: 'stop' }],\n });\n writeOpenAiSse(res, {\n ...baseChunk,\n choices: [],\n usage: openAiUsagePayload(usage, user, budget),\n });\n res.write('data: [DONE]\\n\\n');\n return;\n }\n logger.error(\n { error: String(error), code: error?.code, statusCode: error?.statusCode },\n 'OpenClaw DeepSeek proxy stream failed',\n );\n writeOpenAiSse(res, {\n ...baseChunk,\n choices: [],\n error: {\n message: publicAiErrorMessage(error),\n code: error?.code || 'OPENCLAW_DEEPSEEK_PROXY_ERROR',\n },\n });\n res.write('data: [DONE]\\n\\n');\n } finally {\n res.end();\n }\n }),\n);\n\napp.post(\n '/codex-deepseek/v1/responses',\n asyncHandler(async (req, res) => {\n const bearer = bearerToken(req);\n if (!deepseekApiKey || bearer !== deepseekApiKey) {\n res.status(401).json({\n error: { message: 'Unauthorized Codex DeepSeek adapter request' },\n });\n return;\n }\n\n const prompt = codexResponsesPrompt(req.body);\n if (!prompt) {\n res.status(400).json({ error: { message: 'Responses input is required' } });\n return;\n }\n\n const model = String(req.body?.model || deepseekModel);\n const responseId = `resp_${crypto.randomUUID().replaceAll('-', '')}`;\n const itemId = `msg_${crypto.randomUUID().replaceAll('-', '')}`;\n const wantsStream =\n req.body?.stream !== false ||\n String(req.get('accept') || '').includes('text/event-stream');\n const messages = [{ role: 'user', content: prompt }];\n\n if (!wantsStream) {\n const result = await callDeepSeek(messages);\n res.json(\n responsesPayload({\n responseId,\n itemId,\n status: 'completed',\n model,\n text: result.message,\n usage: result.usage,\n }),\n );\n return;\n }\n\n res.status(200);\n res.set({\n 'Content-Type': 'text/event-stream; charset=utf-8',\n 'Cache-Control': 'no-cache, no-transform',\n Connection: 'keep-alive',\n 'X-Accel-Buffering': 'no',\n });\n res.flushHeaders?.();\n\n writeResponsesEvent(res, 'response.created', {\n type: 'response.created',\n response: responsesPayload({\n responseId,\n itemId,\n status: 'in_progress',\n model,\n }),\n });\n writeResponsesEvent(res, 'response.output_item.added', {\n type: 'response.output_item.added',\n output_index: 0,\n item: {\n id: itemId,\n type: 'message',\n role: 'assistant',\n status: 'in_progress',\n content: [],\n },\n });\n writeResponsesEvent(res, 'response.content_part.added', {\n type: 'response.content_part.added',\n item_id: itemId,\n output_index: 0,\n content_index: 0,\n part: { type: 'output_text', text: '' },\n });\n\n let text = '';\n try {\n const result = await callDeepSeekStream(messages, {\n onToken: (delta) => {\n text += delta;\n writeResponsesEvent(res, 'response.output_text.delta', {\n type: 'response.output_text.delta',\n item_id: itemId,\n output_index: 0,\n content_index: 0,\n delta,\n });\n },\n });\n text = result.message || text;\n writeResponsesEvent(res, 'response.output_text.done', {\n type: 'response.output_text.done',\n item_id: itemId,\n output_index: 0,\n content_index: 0,\n text,\n });\n writeResponsesEvent(res, 'response.content_part.done', {\n type: 'response.content_part.done',\n item_id: itemId,\n output_index: 0,\n content_index: 0,\n part: { type: 'output_text', text },\n });\n writeResponsesEvent(res, 'response.output_item.done', {\n type: 'response.output_item.done',\n output_index: 0,\n item: {\n id: itemId,\n type: 'message',\n role: 'assistant',\n status: 'completed',\n content: [{ type: 'output_text', text }],\n },\n });\n writeResponsesEvent(res, 'response.completed', {\n type: 'response.completed',\n response: responsesPayload({\n responseId,\n itemId,\n status: 'completed',\n model,\n text,\n usage: result.usage,\n }),\n });\n res.end();\n } catch (error) {\n writeResponsesEvent(res, 'response.failed', {\n type: 'response.failed',\n response: responsesPayload({\n responseId,\n itemId,\n status: 'failed',\n model,\n text,\n error,\n }),\n });\n res.end();\n }\n }),\n);\n\napp.use(\n '/api/openclaw/runtime/files',\n express.static(openClawRuntimeUpdatesDir, {\n fallthrough: false,\n immutable: true,\n maxAge: '7d',\n setHeaders(res) {\n res.setHeader('X-Content-Type-Options', 'nosniff');\n },\n }),\n);\n\napp.get(\n '/api/openclaw/runtime/manifest',\n asyncHandler(async (req, res) => {\n const platform = readText(req.query.platform);\n const currentVersion = readText(req.query.currentVersion);\n const appVersion = readText(req.query.appVersion);\n\n let manifest;\n try {\n manifest = await readJsonFile(openClawRuntimeManifestPath);\n } catch (error) {\n logger.info(\n { manifestPath: openClawRuntimeManifestPath, error: String(error) },\n 'OpenClaw runtime manifest unavailable',\n );\n return jsonResponse(res, 200, {\n success: true,\n schema: 1,\n updateAvailable: false,\n reason: 'manifest-unavailable',\n platform,\n currentVersion,\n appVersion,\n });\n }\n\n const latest = manifest.latest && typeof manifest.latest === 'object'\n ? manifest.latest\n : manifest;\n const platforms = latest.platforms && typeof latest.platforms === 'object'\n ? latest.platforms\n : {};\n const platformRelease = platform ? platforms[platform] : null;\n const version = readText(latest.version || manifest.version);\n\n if (!platform || !platformRelease || typeof platformRelease !== 'object') {\n return jsonResponse(res, 200, {\n success: true,\n schema: Number(manifest.schema || 1),\n channel: readText(manifest.channel || latest.channel || 'stable'),\n updateAvailable: false,\n reason: platform ? 'platform-unavailable' : 'platform-required',\n platform,\n currentVersion,\n appVersion,\n latestVersion: version,\n });\n }\n\n if (platform.startsWith('macos-') && !openClawRuntimeEnableMacosUpdates) {\n return jsonResponse(res, 200, {\n success: true,\n schema: Number(manifest.schema || 1),\n channel: readText(manifest.channel || latest.channel || 'stable'),\n updateAvailable: false,\n reason: 'macos-runtime-updates-disabled',\n platform,\n currentVersion,\n appVersion,\n latestVersion: version,\n });\n }\n\n const downloadUrl = absolutizeOpenClawRuntimeUrl(\n req,\n platformRelease.archiveUrl || platformRelease.url || platformRelease.fileName,\n );\n const release = {\n version,\n channel: readText(manifest.channel || latest.channel || 'stable'),\n minAppVersion: readText(latest.minAppVersion || platformRelease.minAppVersion),\n notes: readText(latest.notes || platformRelease.notes),\n nodeExecutable: readText(\n platformRelease.nodeExecutable || latest.nodeExecutable || 'node/bin/node',\n ),\n cliEntrypoint: readText(\n platformRelease.cliEntrypoint || latest.cliEntrypoint || 'openclaw/openclaw.mjs',\n ),\n defaultPort: Number(latest.defaultPort || manifest.defaultPort || 18789),\n defaultModel: readText(latest.defaultModel || manifest.defaultModel || 'openclaw/default'),\n defaultModelOverride: readText(latest.defaultModelOverride || manifest.defaultModelOverride),\n gatewayArgs: Array.isArray(latest.gatewayArgs)\n ? latest.gatewayArgs.map((item) => String(item))\n : ['gateway', '--port', '{port}', '--force'],\n archiveUrl: downloadUrl,\n sha256: readText(platformRelease.sha256),\n size: Number(platformRelease.size || 0),\n };\n\n jsonResponse(res, 200, {\n success: true,\n schema: Number(manifest.schema || 1),\n updateAvailable: Boolean(version && downloadUrl && version !== currentVersion),\n platform,\n currentVersion,\n appVersion,\n latest: release,\n generatedAt: readText(manifest.generatedAt || latest.generatedAt),\n });\n }),\n);\n\napp.get('/health', (_req, res) => {\n const codexRuntime = createCodexDeepSeekRuntime();\n const libreChatRuntime = createLibreChatAgentRuntime();\n const openClawRuntime = createOpenClawRuntime();\n jsonResponse(res, 200, {\n status: 'ok',\n service: 'dacheng-ai-backend',\n provider: libreChatRuntime.enabled ? libreChatRuntime.provider : 'deepseek',\n model: libreChatRuntime.enabled ? libreChatRuntime.model : deepseekModel,\n deepseekModel,\n deepseekAvailableModels,\n libreChatAgentChatEnabled: libreChatRuntime.enabled,\n libreChatAgentProvider: libreChatRuntime.provider,\n libreChatAgentReason: libreChatRuntime.enabled ? undefined : libreChatRuntime.reason,\n codexSdkAvailable: true,\n codexSdkChatEnabled: codexRuntime.enabled,\n codexSdkProvider: codexRuntime.provider,\n openClawAgentChatEnabled: openClawRuntime.enabled,\n openClawAgentProvider: openClawRuntime.provider,\n openClawAgentReason: openClawRuntime.enabled ? undefined : openClawRuntime.reason,\n requireMemberForAi,\n memberMonthlyLimit,\n freeMonthlyLimit,\n deepseekMaxCompletionTokens,\n openClawRuntimeUpdatesDir,\n openClawRuntimeManifestConfigured: fs.existsSync(openClawRuntimeManifestPath),\n cbetaApiRoot,\n timestamp: nowIso(),\n });\n});\n\napp.get('/api/ai/models', (_req, res) => {\n jsonResponse(res, 200, {\n success: true,\n provider: 'deepseek',\n defaultModel: deepseekModel,\n models: deepseekAvailableModels.map((model) => ({\n id: model,\n label: model === 'deepseek-reasoner' ? 'DeepSeek Reasoner' : 'DeepSeek Chat',\n })),\n });\n});\napp.get(\n '/api/ai/quota',\n asyncHandler(async (req, res) => {\n const user = await resolveUser(req, { username: req.query.username });\n const budget = enforceTokenBudget(user, 0);\n const usage = usageFor(user.userId);\n jsonResponse(res, 200, {\n success: true,\n isMember: user.isMember,\n monthlyLimit: budget.limit,\n usedTokens: usage.used,\n remainingTokens: Math.max(0, budget.limit - usage.used),\n month: usage.month,\n });\n }),\n);\n\napp.post(\n '/api/agent/chat',\n asyncHandler(async (req, res) => {\n const message = safeUserText(req.body?.message);\n if (!message) {\n return jsonResponse(res, 400, { success: false, message: 'message is required' });\n }\n\n const user = await resolveUser(req, req.body || {});\n const promptEstimate = estimateTokens(message);\n const estimated = promptEstimate + 600;\n const budget = enforceTokenBudget(user, estimated);\n const maxCompletionTokens = completionBudgetFor(budget, promptEstimate);\n const createdAt = nowIso();\n const conversationId = safeUserText(req.body?.conversationId) || crypto.randomUUID();\n const mode = normalizeAgentMode(req.body?.mode);\n const stream = req.body?.stream === true;\n let conversation = statements.getConversation.get(conversationId, user.userId);\n const isNewConversation = !conversation;\n\n if (isNewConversation) {\n conversation = { id: conversationId, user_id: user.userId, title: titleFromMessage(message) };\n statements.insertConversation.run({\n id: conversationId,\n userId: user.userId,\n title: conversation.title,\n provider: 'agent',\n model: openClawAgentId,\n createdAt,\n updatedAt: createdAt,\n });\n }\n\n const messageId = safeUserText(req.body?.messageId) || crypto.randomUUID();\n statements.insertMessage.run({\n id: messageId,\n conversationId,\n role: 'user',\n content: message,\n promptTokens: 0,\n completionTokens: 0,\n totalTokens: 0,\n createdAt,\n });\n\n const runId = `run_${crypto.randomUUID().replaceAll('-', '')}`;\n const sessionKey = openClawSessionKey(user, conversationId);\n const openClawRuntime = createOpenClawRuntime();\n statements.insertAgentRun.run({\n id: runId,\n userId: user.userId,\n conversationId,\n messageId,\n openClawRunId: null,\n openClawSessionKey: sessionKey,\n status: stream ? 'queued' : 'running',\n mode,\n provider: openClawRuntime.enabled ? openClawRuntime.provider : 'dacheng-ai-fallback',\n model: openClawRuntime.enabled ? openClawAgentId : deepseekModel,\n startedAt: createdAt,\n });\n\n if (stream) {\n return jsonResponse(res, 200, {\n success: true,\n runId,\n conversationId,\n streamUrl: `/api/agent/runs/${runId}/events`,\n });\n }\n\n const controller = new AbortController();\n activeAgentRuns.set(runId, controller);\n try {\n const skillResult = await runResourceFinderSkill({ message, user });\n const skillContext = resourceContextMessage(skillResult);\n const historyRows = statements.listMessages.all(conversationId).slice(-14);\n const modelMessages = [\n { role: 'system', content: systemPrompt },\n ...(skillContext ? [{ role: 'system', content: skillContext }] : []),\n ...normalizeMessages(historyRows),\n ];\n\n const aiResult = await runAgentModel({\n messages: modelMessages,\n user,\n conversationId,\n mode,\n signal: controller.signal,\n maxCompletionTokens,\n });\n const usage = {\n promptTokens: aiResult.usage?.promptTokens || estimateTokens(JSON.stringify(modelMessages)),\n completionTokens: aiResult.usage?.completionTokens || estimateTokens(aiResult.message),\n totalTokens:\n aiResult.usage?.totalTokens ||\n estimateTokens(JSON.stringify(modelMessages)) + estimateTokens(aiResult.message),\n };\n recordUsage(user.userId, usage.totalTokens);\n const answeredAt = nowIso();\n const assistantMessageId = crypto.randomUUID();\n statements.insertMessage.run({\n id: assistantMessageId,\n conversationId,\n role: 'assistant',\n content: aiResult.message,\n promptTokens: usage.promptTokens,\n completionTokens: usage.completionTokens,\n totalTokens: usage.totalTokens,\n createdAt: answeredAt,\n });\n statements.updateConversation.run({\n id: conversationId,\n userId: user.userId,\n title: isNewConversation ? titleFromMessage(message) : conversation.title,\n updatedAt: answeredAt,\n });\n updateAgentRunStatus({ id: runId, user_id: user.userId }, 'completed', {\n openClawRunId: aiResult.runId,\n inputTokens: usage.promptTokens,\n outputTokens: usage.completionTokens,\n });\n const action = determineClientAction(skillResult, message);\n return jsonResponse(res, 200, {\n success: true,\n runId,\n conversationId,\n messageId: assistantMessageId,\n provider: aiResult.provider,\n model: aiResult.model,\n message: aiResult.message,\n usage: agentUsagePayload(usage, user, budget),\n ...(action ? { clientAction: action } : {}),\n });\n } catch (error) {\n const status = error.name === 'AbortError' ? 'cancelled' : 'failed';\n updateAgentRunStatus({ id: runId, user_id: user.userId }, status, {\n errorCode: error.name || 'AGENT_RUN_FAILED',\n errorMessage: publicAiErrorMessage(error),\n });\n if (status === 'cancelled') {\n return jsonResponse(res, 499, { success: false, runId, status: 'cancelled' });\n }\n throw error;\n } finally {\n activeAgentRuns.delete(runId);\n }\n }),\n);\n\napp.get(\n '/api/agent/runs/:runId/events',\n asyncHandler(async (req, res) => {\n const user = await resolveUser(req, {});\n const run = statements.getAgentRun.get(req.params.runId, user.userId);\n if (!run) {\n return jsonResponse(res, 404, { success: false, message: 'run not found' });\n }\n if (run.status === 'completed' || run.status === 'failed' || run.status === 'cancelled') {\n return jsonResponse(res, 409, { success: false, message: `run is already ${run.status}` });\n }\n\n sseHeaders(res);\n const controller = new AbortController();\n activeAgentRuns.set(run.id, controller);\n updateAgentRunStatus(run, 'running');\n writeSse(res, 'run.started', { runId: run.id, conversationId: run.conversation_id });\n\n try {\n let streamedText = '';\n const modelMessages = buildAgentMessages(run.conversation_id);\n const lastUserMsg = [...modelMessages].reverse().find(m => m.role === 'user');\n const userMsgText = lastUserMsg ? lastUserMsg.content : '';\n const skillResult = userMsgText ? await runResourceFinderSkill({\n message: userMsgText,\n user,\n onStep: (step) => writeSse(res, 'tool.call.completed', {\n tool: 'agent.step',\n displayName: step.title || 'Agent step',\n summary: step.message || '',\n }),\n }) : null;\n const skillContext = resourceContextMessage(skillResult);\n const finalMessages = skillContext ? [\n modelMessages[0],\n { role: 'system', content: skillContext },\n ...modelMessages.slice(1)\n ] : modelMessages;\n\n const promptEstimate = estimateTokens(JSON.stringify(finalMessages));\n const estimated = promptEstimate + 600;\n const budget = enforceTokenBudget(user, estimated);\n const maxCompletionTokens = completionBudgetFor(budget, promptEstimate);\n const aiResult = await runAgentModel({\n messages: finalMessages,\n user,\n conversationId: run.conversation_id,\n mode: run.mode,\n signal: controller.signal,\n maxCompletionTokens,\n callbacks: {\n onStep: (step) => writeSse(res, 'tool.call.completed', {\n tool: 'agent.step',\n displayName: step.title || 'Agent step',\n summary: step.message || '',\n }),\n onToken: (delta) => {\n streamedText += delta;\n writeSse(res, 'assistant.delta', { text: delta });\n },\n },\n });\n const message = aiResult.message || streamedText.trim();\n const usage = {\n promptTokens: aiResult.usage?.promptTokens || estimateTokens(JSON.stringify(finalMessages)),\n completionTokens: aiResult.usage?.completionTokens || estimateTokens(message),\n totalTokens:\n aiResult.usage?.totalTokens || estimateTokens(JSON.stringify(finalMessages)) + estimateTokens(message),\n };\n recordUsage(user.userId, usage.totalTokens);\n const assistantMessageId = crypto.randomUUID();\n const answeredAt = nowIso();\n statements.insertMessage.run({\n id: assistantMessageId,\n conversationId: run.conversation_id,\n role: 'assistant',\n content: message,\n promptTokens: usage.promptTokens,\n completionTokens: usage.completionTokens,\n totalTokens: usage.totalTokens,\n createdAt: answeredAt,\n });\n statements.updateConversation.run({\n id: run.conversation_id,\n userId: user.userId,\n title: titleFromMessage(message),\n updatedAt: answeredAt,\n });\n updateAgentRunStatus(run, 'completed', {\n openClawRunId: aiResult.runId,\n inputTokens: usage.promptTokens,\n outputTokens: usage.completionTokens,\n });\n const action = determineClientAction(skillResult, userMsgText);\n writeSse(res, 'assistant.message', { messageId: assistantMessageId, content: message });\n writeSse(res, 'run.completed', {\n usage: agentUsagePayload(usage, user, budget),\n ...(action ? { clientAction: action } : {}),\n });\n res.end();\n } catch (error) {\n const status = error.name === 'AbortError' ? 'cancelled' : 'failed';\n updateAgentRunStatus(run, status, {\n errorCode: error.name || 'AGENT_RUN_FAILED',\n errorMessage: publicAiErrorMessage(error),\n });\n writeSse(res, status === 'cancelled' ? 'run.cancelled' : 'run.failed', {\n runId: run.id,\n message: publicAiErrorMessage(error),\n });\n res.end();\n } finally {\n activeAgentRuns.delete(run.id);\n }\n }),\n);\n\napp.post(\n '/api/agent/runs/:runId/cancel',\n asyncHandler(async (req, res) => {\n const user = await resolveUser(req, req.body || {});\n const run = statements.getAgentRun.get(req.params.runId, user.userId);\n if (!run) {\n return jsonResponse(res, 404, { success: false, message: 'run not found' });\n }\n const controller = activeAgentRuns.get(run.id);\n if (controller) controller.abort();\n updateAgentRunStatus(run, 'cancelled');\n return jsonResponse(res, 200, { success: true, runId: run.id, status: 'cancelled' });\n }),\n);\n\napp.post(\n '/api/agent/messages/:messageId/feedback',\n asyncHandler(async (req, res) => {\n const user = await resolveUser(req, req.body || {});\n const rating = safeUserText(req.body?.rating);\n if (!['up', 'down'].includes(rating)) {\n return jsonResponse(res, 400, { success: false, message: 'rating must be up or down' });\n }\n statements.insertAgentFeedback.run({\n id: crypto.randomUUID(),\n messageId: req.params.messageId,\n userId: user.userId,\n rating,\n reason: safeUserText(req.body?.reason),\n comment: safeUserText(req.body?.comment),\n createdAt: nowIso(),\n });\n return jsonResponse(res, 200, { success: true });\n }),\n);\n\napp.post(\n '/api/ai/chat',\n asyncHandler(async (req, res) => {\n const message = safeUserText(req.body?.message);\n if (!message) {\n return jsonResponse(res, 400, { success: false, message: 'message is required' });\n }\n\n const user = await resolveUser(req, req.body || {});\n const promptEstimate = estimateTokens(message);\n const estimated = promptEstimate + 600;\n const budget = enforceTokenBudget(user, estimated);\n const maxCompletionTokens = completionBudgetFor(budget, promptEstimate);\n const createdAt = nowIso();\n const conversationId = safeUserText(req.body?.conversationId) || crypto.randomUUID();\n const requestedModel = normalizeDeepSeekModelName(req.body?.model);\n const clientContext = clientContextMessage(req.body?.client);\n let conversation = statements.getConversation.get(conversationId, user.userId);\n const isNewConversation = !conversation;\n\n if (isNewConversation) {\n conversation = {\n id: conversationId,\n user_id: user.userId,\n title: titleFromMessage(message),\n };\n statements.insertConversation.run({\n id: conversationId,\n userId: user.userId,\n title: conversation.title,\n provider: 'deepseek',\n model: requestedModel,\n createdAt,\n updatedAt: createdAt,\n });\n }\n\n statements.insertMessage.run({\n id: crypto.randomUUID(),\n conversationId,\n role: 'user',\n content: message,\n promptTokens: 0,\n completionTokens: 0,\n totalTokens: 0,\n createdAt,\n });\n\n const libreChatRuntime = createLibreChatAgentRuntime();\n const skillResult = await runResourceFinderSkill({ message, user });\n const skillContext = resourceContextMessage(skillResult);\n const historyRows = statements.listMessages.all(conversationId).slice(-14);\n const modelMessages = [\n { role: 'system', content: systemPrompt },\n ...(clientContext ? [{ role: 'system', content: clientContext }] : []),\n ...(skillContext ? [{ role: 'system', content: skillContext }] : []),\n ...normalizeMessages(historyRows),\n ];\n\n const openClawRuntime = createOpenClawRuntime();\n const codexRuntime = createCodexDeepSeekRuntime();\n let provider = openClawRuntime.enabled\n ? openClawRuntime.provider\n : libreChatRuntime.enabled\n ? libreChatRuntime.provider\n : 'deepseek';\n let responseModel = openClawRuntime.enabled\n ? openClawRuntime.model\n : libreChatRuntime.enabled\n ? libreChatRuntime.model\n : requestedModel;\n let aiResult;\n try {\n if (openClawRuntime.enabled) {\n aiResult = await callOpenClawAgent({\n messages: modelMessages,\n user,\n conversationId,\n mode: normalizeAgentMode(req.body?.mode),\n });\n provider = aiResult.provider || provider;\n responseModel = aiResult.model || responseModel;\n } else if (libreChatRuntime.enabled) {\n aiResult = await callLibreChatAgent(modelMessages);\n } else if (codexRuntime.enabled) {\n provider = codexRuntime.provider;\n aiResult = await callCodexSdkDeepSeek(modelMessages);\n } else {\n aiResult = await callDeepSeek(modelMessages, { model: requestedModel, maxCompletionTokens });\n }\n } catch (error) {\n if (libreChatRuntime.enabled) {\n logger.warn({ error: String(error) }, 'LibreChat Agent chat failed, falling back to Codex SDK or DeepSeek direct');\n provider = codexRuntime.enabled ? codexRuntime.provider : 'deepseek';\n responseModel = requestedModel;\n if (codexRuntime.enabled) {\n try {\n aiResult = await callCodexSdkDeepSeek(modelMessages);\n } catch (codexError) {\n if (isDeepSeekQuotaError(codexError)) throw codexError;\n logger.warn({ error: String(codexError) }, 'Codex SDK chat failed, falling back to DeepSeek direct');\n provider = 'deepseek';\n aiResult = await callDeepSeek(modelMessages, { model: requestedModel, maxCompletionTokens });\n }\n } else {\n aiResult = await callDeepSeek(modelMessages, { model: requestedModel, maxCompletionTokens });\n }\n } else {\n if (!codexRuntime.enabled || isDeepSeekQuotaError(error)) throw error;\n logger.warn({ error: String(error) }, 'Codex SDK chat failed, falling back to DeepSeek direct');\n provider = 'deepseek';\n responseModel = requestedModel;\n aiResult = await callDeepSeek(modelMessages, { model: requestedModel, maxCompletionTokens });\n }\n }\n const usage = {\n promptTokens: aiResult.usage.promptTokens || estimateTokens(JSON.stringify(modelMessages)),\n completionTokens: aiResult.usage.completionTokens || estimateTokens(aiResult.message),\n totalTokens:\n aiResult.usage.totalTokens ||\n estimateTokens(JSON.stringify(modelMessages)) + estimateTokens(aiResult.message),\n };\n recordUsage(user.userId, usage.totalTokens);\n\n const answeredAt = nowIso();\n statements.insertMessage.run({\n id: crypto.randomUUID(),\n conversationId,\n role: 'assistant',\n content: aiResult.message,\n promptTokens: usage.promptTokens,\n completionTokens: usage.completionTokens,\n totalTokens: usage.totalTokens,\n createdAt: answeredAt,\n });\n statements.updateConversation.run({\n id: conversationId,\n userId: user.userId,\n title: isNewConversation ? titleFromMessage(message) : conversation.title,\n updatedAt: answeredAt,\n });\n\n const latestUsage = usageFor(user.userId);\n const monthlyLimit = budget.limit;\n const action = determineClientAction(skillResult, message);\n jsonResponse(res, 200, {\n success: true,\n conversationId,\n provider,\n model: responseModel,\n message: aiResult.message,\n usage: {\n ...usage,\n monthlyLimit,\n remainingTokens: Math.max(0, monthlyLimit - latestUsage.used),\n },\n ...(action ? { clientAction: action } : {}),\n });\n }),\n);\n\napp.post('/api/ai/chat/stream', async (req, res) => {\n sseHeaders(res);\n\n try {\n const message = safeUserText(req.body?.message);\n if (!message) {\n writeSse(res, 'error', { message: 'message is required' });\n res.end();\n return;\n }\n\n const user = await resolveUser(req, req.body || {});\n const promptEstimate = estimateTokens(message);\n const estimated = promptEstimate + 600;\n const budget = enforceTokenBudget(user, estimated);\n const maxCompletionTokens = completionBudgetFor(budget, promptEstimate);\n const createdAt = nowIso();\n const conversationId = safeUserText(req.body?.conversationId) || crypto.randomUUID();\n const requestedModel = normalizeDeepSeekModelName(req.body?.model);\n const clientContext = clientContextMessage(req.body?.client);\n let conversation = statements.getConversation.get(conversationId, user.userId);\n const isNewConversation = !conversation;\n\n if (isNewConversation) {\n conversation = {\n id: conversationId,\n user_id: user.userId,\n title: titleFromMessage(message),\n };\n statements.insertConversation.run({\n id: conversationId,\n userId: user.userId,\n title: conversation.title,\n provider: 'deepseek',\n model: requestedModel,\n createdAt,\n updatedAt: createdAt,\n });\n }\n\n statements.insertMessage.run({\n id: crypto.randomUUID(),\n conversationId,\n role: 'user',\n content: message,\n promptTokens: 0,\n completionTokens: 0,\n totalTokens: 0,\n createdAt,\n });\n\n const codexRuntime = createCodexDeepSeekRuntime();\n const libreChatRuntime = createLibreChatAgentRuntime();\n const initialOpenClawRuntime = createOpenClawRuntime();\n let provider = initialOpenClawRuntime.enabled\n ? initialOpenClawRuntime.provider\n : libreChatRuntime.enabled\n ? libreChatRuntime.provider\n : codexRuntime.enabled\n ? codexRuntime.provider\n : 'deepseek';\n let responseModel = initialOpenClawRuntime.enabled\n ? initialOpenClawRuntime.model\n : libreChatRuntime.enabled\n ? libreChatRuntime.model\n : requestedModel;\n writeSse(res, 'meta', {\n conversationId,\n provider,\n model: responseModel,\n });\n const skillResult = await runResourceFinderSkill({\n message,\n user,\n onStep: (step) => writeSse(res, 'step', step),\n });\n const skillContext = resourceContextMessage(skillResult);\n const historyRows = statements.listMessages.all(conversationId).slice(-14);\n const modelMessages = [\n { role: 'system', content: systemPrompt },\n ...(clientContext ? [{ role: 'system', content: clientContext }] : []),\n ...(skillContext ? [{ role: 'system', content: skillContext }] : []),\n ...normalizeMessages(historyRows),\n ];\n\n let aiResult;\n const openClawRuntime = initialOpenClawRuntime;\n try {\n if (openClawRuntime.enabled) {\n aiResult = await callOpenClawAgent({\n messages: modelMessages,\n user,\n conversationId,\n mode: normalizeAgentMode(req.body?.mode),\n });\n provider = aiResult.provider || provider;\n responseModel = aiResult.model || responseModel;\n writeSse(res, 'delta', { text: aiResult.message });\n } else if (libreChatRuntime.enabled) {\n aiResult = await callLibreChatAgentStream(modelMessages, {\n onToken: (token) => writeSse(res, 'delta', { text: token }),\n });\n } else if (codexRuntime.enabled) {\n aiResult = await callCodexSdkDeepSeek(modelMessages, {\n onToken: (token) => writeSse(res, 'delta', { text: token }),\n onStep: (step) => writeSse(res, 'step', step),\n });\n } else {\n aiResult = await callDeepSeekStream(modelMessages, {\n model: requestedModel,\n maxCompletionTokens,\n onToken: (token) => writeSse(res, 'delta', { text: token }),\n });\n }\n } catch (error) {\n if (libreChatRuntime.enabled) {\n logger.warn({ error: String(error) }, 'LibreChat Agent streaming chat failed, falling back to Codex SDK or DeepSeek direct');\n provider = codexRuntime.enabled ? codexRuntime.provider : 'deepseek';\n responseModel = requestedModel;\n writeSse(res, 'meta', {\n conversationId,\n provider,\n model: responseModel,\n });\n if (codexRuntime.enabled) {\n try {\n aiResult = await callCodexSdkDeepSeek(modelMessages, {\n onToken: (token) => writeSse(res, 'delta', { text: token }),\n onStep: (step) => writeSse(res, 'step', step),\n });\n } catch (codexError) {\n if (isDeepSeekQuotaError(codexError)) throw codexError;\n logger.warn({ error: String(codexError) }, 'Codex SDK streaming chat failed, falling back to DeepSeek direct');\n provider = 'deepseek';\n responseModel = requestedModel;\n writeSse(res, 'meta', {\n conversationId,\n provider,\n model: responseModel,\n });\n aiResult = await callDeepSeekStream(modelMessages, {\n model: requestedModel,\n maxCompletionTokens,\n onToken: (token) => writeSse(res, 'delta', { text: token }),\n });\n }\n } else {\n aiResult = await callDeepSeekStream(modelMessages, {\n model: requestedModel,\n maxCompletionTokens,\n onToken: (token) => writeSse(res, 'delta', { text: token }),\n });\n }\n } else {\n if (!codexRuntime.enabled || isDeepSeekQuotaError(error)) throw error;\n logger.warn({ error: String(error) }, 'Codex SDK streaming chat failed, falling back to DeepSeek direct');\n provider = 'deepseek';\n responseModel = requestedModel;\n writeSse(res, 'meta', {\n conversationId,\n provider,\n model: responseModel,\n });\n aiResult = await callDeepSeekStream(modelMessages, {\n model: requestedModel,\n maxCompletionTokens,\n onToken: (token) => writeSse(res, 'delta', { text: token }),\n });\n }\n }\n\n const usage = {\n promptTokens: aiResult.usage.promptTokens || estimateTokens(JSON.stringify(modelMessages)),\n completionTokens: aiResult.usage.completionTokens || estimateTokens(aiResult.message),\n totalTokens:\n aiResult.usage.totalTokens ||\n estimateTokens(JSON.stringify(modelMessages)) + estimateTokens(aiResult.message),\n };\n recordUsage(user.userId, usage.totalTokens);\n\n const answeredAt = nowIso();\n statements.insertMessage.run({\n id: crypto.randomUUID(),\n conversationId,\n role: 'assistant',\n content: aiResult.message,\n promptTokens: usage.promptTokens,\n completionTokens: usage.completionTokens,\n totalTokens: usage.totalTokens,\n createdAt: answeredAt,\n });\n statements.updateConversation.run({\n id: conversationId,\n userId: user.userId,\n title: isNewConversation ? titleFromMessage(message) : conversation.title,\n updatedAt: answeredAt,\n });\n\n const latestUsage = usageFor(user.userId);\n const monthlyLimit = budget.limit;\n const action = determineClientAction(skillResult, message);\n writeSse(res, 'done', {\n conversationId,\n provider,\n model: responseModel,\n message: aiResult.message,\n usage: {\n ...usage,\n monthlyLimit,\n remainingTokens: Math.max(0, monthlyLimit - latestUsage.used),\n },\n ...(action ? { clientAction: action } : {}),\n });\n res.end();\n } catch (error) {\n logger.error({ error: error.stack || String(error) }, 'streaming chat failed');\n writeSse(res, 'error', {\n message: publicAiErrorMessage(error),\n details: error.details,\n });\n res.end();\n }\n});\n\napp.get(\n '/api/ai/conversations',\n asyncHandler(async (req, res) => {\n const user = await resolveUser(req, { username: req.query.username });\n const items = statements.listConversations.all(user.userId).map((item) => ({\n id: item.id,\n title: item.title,\n updatedAt: item.updatedAt,\n }));\n jsonResponse(res, 200, { success: true, items });\n }),\n);\n\napp.get(\n '/api/ai/conversations/:id',\n asyncHandler(async (req, res) => {\n const user = await resolveUser(req);\n const conversation = statements.getConversation.get(req.params.id, user.userId);\n if (!conversation) {\n return jsonResponse(res, 404, { success: false, message: 'conversation not found' });\n }\n const messages = statements.listMessages.all(req.params.id).map((item) => ({\n role: item.role,\n content: item.content,\n createdAt: item.createdAt,\n }));\n jsonResponse(res, 200, {\n success: true,\n conversation: {\n id: conversation.id,\n title: conversation.title,\n updatedAt: conversation.updated_at,\n },\n messages,\n });\n }),\n);\n\nfunction buildUrl(base, endpoint, params = {}) {\n const url = new URL(endpoint, `${base}/`);\n for (const [key, value] of Object.entries(params)) {\n if (Array.isArray(value)) {\n for (const item of value) {\n if (item !== undefined && item !== null && item !== '') {\n url.searchParams.append(key, String(item));\n }\n }\n } else if (value !== undefined && value !== null && value !== '') {\n url.searchParams.set(key, String(value));\n }\n }\n return url;\n}\n\nasync function fetchJson(url) {\n const response = await fetch(url, {\n headers: { Accept: 'application/json' },\n signal: AbortSignal.timeout(15_000),\n });\n if (!response.ok) {\n throw new Error(`Request failed: ${response.status}`);\n }\n return response.json();\n}\n\nfunction stripHtml(value) {\n return String(value || '')\n .replace(/<script[\\s\\S]*?<\\/script>/gi, ' ')\n .replace(/<style[\\s\\S]*?<\\/style>/gi, ' ')\n .replace(/<[^>]+>/g, ' ')\n .replace(/ /g, ' ')\n .replace(/</g, '<')\n .replace(/>/g, '>')\n .replace(/&/g, '&')\n .replace(/&#(\\d+);/g, (_match, code) => String.fromCharCode(Number(code)))\n .replace(/\\s+/g, ' ')\n .trim();\n}\n\nfunction cleanExtractedText(value) {\n let text = String(value || '')\n .replace(/\\b[A-Z]{1,3}\\d{2}n\\d{4,5}[a-z]?_p?\\d{4}[a-z]\\d{2}\\b/g, ' ')\n .replace(/\\uFFFD/g, '')\n .replace(/\\s+([,。;:!?、」』)》】])/g, '$1')\n .replace(/([「『《(【])\\s+/g, '$1');\n for (let i = 0; i < 3; i += 1) {\n text = text.replace(/([\\u3400-\\u9fff])\\s+([\\u3400-\\u9fff])/g, '$1$2');\n }\n return text.replace(/[ \\t]{2,}/g, ' ').trim();\n}\n\nfunction compactText(value, limit = 140) {\n const text = stripHtml(value);\n return text.length > limit ? `${text.slice(0, limit)}...` : text;\n}\n\nfunction collectSearchResults(data) {\n if (!data || typeof data !== 'object') return [];\n if (Array.isArray(data.results)) return data.results;\n if (Array.isArray(data?.results?.docs)) return data.results.docs;\n if (Array.isArray(data?.response?.docs)) return data.response.docs;\n if (Array.isArray(data.docs)) return data.docs;\n return [];\n}\n\nfunction firstText(item, keys) {\n for (const key of keys) {\n const value = item?.[key];\n if (Array.isArray(value)) {\n const first = value.map((entry) => safeUserText(String(entry))).find(Boolean);\n if (first) return first;\n }\n if (typeof value === 'string' && value.trim()) return value.trim();\n if (typeof value === 'number') return String(value);\n }\n return '';\n}\n\nfunction firstNumber(item, keys, fallback = undefined) {\n for (const key of keys) {\n const value = item?.[key];\n if (typeof value === 'number') return value;\n if (typeof value === 'string' && value.trim() && Number.isFinite(Number(value))) {\n return Number(value);\n }\n }\n return fallback;\n}\n\nasync function searchCbetaTitle(query, limit) {\n const url = buildUrl(cbetaApiRoot, 'search/title', { q: query, start: 0, rows: limit });\n const data = await fetchJson(url);\n return collectSearchResults(data).flatMap((item) => {\n const work = firstText(item, ['work', 'work_id', 'workId']);\n const title = firstText(item, ['content', 'title', 'work_title', 'workTitle']);\n if (!work || !title) return [];\n const juan = firstNumber(item, ['juan', 'juan_num', 'juanNum'], 1);\n return [\n {\n id: `cbeta-title-${work}-${juan}`,\n title,\n sourceName: 'CBETA 佛典',\n url: `cbeta:${work}:${juan}`,\n snippet: firstText(item, ['byline', 'creators', 'time_dynasty']) || '可下载经文正文并用于全球法布施。',\n resourceType: 'scripture',\n work,\n juan,\n },\n ];\n });\n}\n\nasync function searchCbetaContent(query, limit) {\n const endpoints = ['search/fulltext', 'search/content', 'search/all_in_one'];\n const settled = await Promise.allSettled(\n endpoints.map((endpoint) =>\n fetchJson(buildUrl(cbetaApiRoot, endpoint, { q: query, start: 0, rows: limit, field: 'content' })),\n ),\n );\n const results = [];\n for (const response of settled) {\n if (response.status !== 'fulfilled') continue;\n for (const item of collectSearchResults(response.value)) {\n const work = firstText(item, ['work', 'work_id', 'workId', 'sutra', 'id']);\n const snippet = compactText(firstText(item, ['kwic', 'highlight', 'snippet', 'content', 'text', 'body', 'p']));\n if (!work || !snippet) continue;\n const juan = firstNumber(item, ['juan', 'juan_num', 'juanNum', '卷'], 1);\n const title = firstText(item, ['title', 'work_title', 'workTitle', 'sutra_name', 'book', 'name']) || work;\n results.push({\n id: `cbeta-content-${work}-${juan}-${sha256(snippet).slice(0, 8)}`,\n title,\n sourceName: 'CBETA 佛典',\n url: `cbeta:${work}:${juan}`,\n snippet,\n resourceType: 'scripture',\n work,\n juan,\n });\n if (results.length >= limit) return results;\n }\n }\n return results;\n}\n\nfunction directUrlResource(query) {\n const url = safeUserText(query);\n let parsed;\n try {\n parsed = new URL(url);\n } catch {\n return null;\n }\n if (!['http:', 'https:'].includes(parsed.protocol)) return null;\n return {\n id: `url-${sha256(url).slice(0, 12)}`,\n title: parsed.hostname,\n sourceName: '网页资源',\n url,\n snippet: '直接下载并提取这个链接的可读正文。',\n resourceType: 'web',\n };\n}\n\nfunction looksLikeAudioQuery(query) {\n return /音乐|音樂|音频|音訊|mp3|audio|念诵|念誦|诵经|誦經|唱诵|唱誦|读诵|讀誦/.test(\n safeUserText(query).toLowerCase(),\n );\n}\n\nfunction archiveSearchTerm(query) {\n return (\n safeUserText(query)\n .replace(/下载|查找|寻找|找一?个|资源|音频|音訊|音乐|音樂|mp3|audio|念诵|念誦|诵经|誦經|唱诵|唱誦|读诵|讀誦/gi, ' ')\n .replace(/\\s+/g, ' ')\n .trim() || safeUserText(query)\n );\n}\n\nfunction solrPhrase(value) {\n return String(value).replace(/\\\\/g, '\\\\\\\\').replace(/\"/g, '\\\\\"');\n}\n\nasync function searchInternetArchiveAudio(query, limit) {\n if (!looksLikeAudioQuery(query)) return [];\n const term = archiveSearchTerm(query);\n if (!term) return [];\n const variants = Array.from(\n new Set([\n term,\n term.replaceAll('金刚经', '金剛經'),\n term.replaceAll('金剛經', '金刚经'),\n ].filter(Boolean)),\n );\n const phraseQuery = variants\n .flatMap((item) => [`title:\"${solrPhrase(item)}\"`, `description:\"${solrPhrase(item)}\"`])\n .join(' OR ');\n const url = buildUrl('https://archive.org', 'advancedsearch.php', {\n q: `(${phraseQuery}) AND mediatype:audio`,\n rows: Math.min(Math.max(limit, 1), 10),\n output: 'json',\n 'fl[]': ['identifier', 'title', 'description'],\n });\n const data = await fetchJson(url);\n const docs = Array.isArray(data?.response?.docs) ? data.response.docs : [];\n return docs\n .map((item) => {\n const identifier = firstText(item, ['identifier']);\n const title = firstText(item, ['title']) || identifier;\n if (!identifier || !title) return null;\n return {\n id: `ia-audio-${identifier}`,\n title,\n sourceName: 'Internet Archive 音频',\n url: `ia:${identifier}`,\n snippet: compactText(firstText(item, ['description'])) || '公开可访问的音频资源,将下载第一个可用 MP3 文件。',\n resourceType: 'audio',\n identifier,\n };\n })\n .filter(Boolean);\n}\n\nconst curatedCbetaWorks = [\n {\n work: 'T0235',\n juan: 1,\n title: '金剛般若波羅蜜經',\n keywords: ['金剛般若波羅蜜經', '金刚般若波罗蜜经', '金剛經', '金刚经'],\n snippet: '姚秦 鳩摩羅什譯。常用于读诵、共修与法布施的般若经典。',\n },\n {\n work: 'T0251',\n juan: 1,\n title: '般若波羅蜜多心經',\n keywords: ['般若波羅蜜多心經', '般若波罗蜜多心经', '心經', '心经'],\n snippet: '唐 玄奘譯。篇幅短,适合加入禅室功课本与日常持诵。',\n },\n {\n work: 'T0262',\n juan: 1,\n title: '妙法蓮華經',\n keywords: ['妙法蓮華經', '妙法莲华经', '法華經', '法华经'],\n snippet: '姚秦 鳩摩羅什譯。可按卷下载正文,用于长期功课与分享。',\n },\n {\n work: 'T0279',\n juan: 1,\n title: '大方廣佛華嚴經',\n keywords: ['大方廣佛華嚴經', '大方广佛华严经', '華嚴經', '华严经'],\n snippet: '唐 實叉難陀譯。可按卷下载正文,用于系统研读与法布施。',\n },\n {\n work: 'T0360',\n juan: 1,\n title: '佛說無量壽經',\n keywords: ['佛說無量壽經', '佛说无量寿经', '無量壽經', '无量寿经'],\n snippet: '净土经典。可下载正文,用于净土功课本与全球法布施。',\n },\n {\n work: 'T0365',\n juan: 1,\n title: '佛說觀無量壽佛經',\n keywords: ['佛說觀無量壽佛經', '佛说观无量寿佛经', '觀無量壽經', '观无量寿经'],\n snippet: '净土三经之一。适合查找下载后加入净土功课。',\n },\n {\n work: 'T0366',\n juan: 1,\n title: '佛說阿彌陀經',\n keywords: ['佛說阿彌陀經', '佛说阿弥陀经', '阿彌陀經', '阿弥陀经'],\n snippet: '姚秦 鳩摩羅什譯。篇幅适中,适合共修、持诵与分享。',\n },\n {\n work: 'T0475',\n juan: 1,\n title: '維摩詰所說經',\n keywords: ['維摩詰所說經', '维摩诘所说经', '維摩詰經', '维摩诘经'],\n snippet: '姚秦 鳩摩羅什譯。大乘经典,可按卷下载用于研读。',\n },\n {\n work: 'T0676',\n juan: 1,\n title: '解深密經',\n keywords: ['解深密經', '解深密经'],\n snippet: '唯识相关大乘经典。适合研读、整理笔记与法布施。',\n },\n {\n work: 'T1666',\n juan: 1,\n title: '大乘起信論',\n keywords: ['大乘起信論', '大乘起信论', '起信論', '起信论'],\n snippet: '大乘论典。适合下载后作为研习功课资料。',\n },\n];\n\nfunction localCbetaResults(query, limit) {\n const normalizedQuery = safeUserText(query).toLowerCase();\n if (!normalizedQuery) return [];\n return curatedCbetaWorks\n .filter((item) =>\n item.keywords.some((keyword) => {\n const normalizedKeyword = keyword.toLowerCase();\n return normalizedKeyword.includes(normalizedQuery) || normalizedQuery.includes(normalizedKeyword);\n }),\n )\n .slice(0, limit)\n .map((item) => ({\n id: `cbeta-curated-${item.work}-${item.juan}`,\n title: item.title,\n sourceName: 'CBETA 佛典',\n url: `cbeta:${item.work}:${item.juan}`,\n snippet: item.snippet,\n resourceType: 'scripture',\n work: item.work,\n juan: item.juan,\n }));\n}\n\nfunction uniqueResources(items, limit) {\n const seen = new Set();\n const results = [];\n for (const item of items) {\n const key = `${item.url}:${item.title}`;\n if (seen.has(key)) continue;\n seen.add(key);\n results.push(item);\n if (results.length >= limit) break;\n }\n return results;\n}\n\nasync function findResourceCandidates(query, limit) {\n const direct = directUrlResource(query);\n const results = [];\n if (direct) results.push(direct);\n try {\n results.push(...(await searchInternetArchiveAudio(query, limit)));\n } catch (error) {\n logger.warn({ error: String(error), query }, 'Internet Archive audio search skipped');\n }\n results.push(...localCbetaResults(query, limit));\n\n const [titleResults, contentResults] = await Promise.allSettled([\n searchCbetaTitle(query, limit),\n searchCbetaContent(query, limit),\n ]);\n if (titleResults.status === 'fulfilled') results.push(...titleResults.value);\n if (contentResults.status === 'fulfilled') results.push(...contentResults.value);\n\n return uniqueResources(results, limit);\n}\n\nfunction looksLikeResourceTask(message) {\n const text = safeUserText(message).toLowerCase();\n if (!text) return false;\n return /下载|查找|寻找|找一?个|资源|經文|经文|佛经|佛經|仪轨|儀軌|音乐|音樂|mp3|audio|念诵|念誦|诵经|誦經/.test(text);\n}\n\nfunction extractResourceQuery(message) {\n const text = safeUserText(message)\n .replace(/请|請|帮我|幫我|麻烦|麻煩|可以|能不能|需要|我要|想要/g, ' ')\n .replace(/下载|下載|查找|寻找|尋找|找一个|找一個|找|资源|資源|并|並|然后|然後|用于|用於|全球法布施|加入功课本|加入功課本/g, ' ')\n .replace(/\\s+/g, ' ')\n .trim();\n return text || safeUserText(message);\n}\n\nfunction resourceContextMessage(skillResult) {\n if (!skillResult) return '';\n const lines = [\n '本轮资源检索/下载已经由大乘 App 后端完成。',\n '不要再次调用 search_dharma_resources、download_dharma_resource 或其他资源 MCP 工具;请直接基于以下结果回答用户。',\n `后端已调用 skill: ${skillResult.skillName}`,\n `执行目标: ${skillResult.query}`,\n ];\n if (skillResult.selected) {\n lines.push(`候选资源: ${skillResult.selected.title}`);\n lines.push(`来源: ${skillResult.selected.sourceName}`);\n lines.push(`资源地址: ${skillResult.selected.url}`);\n }\n if (skillResult.downloaded) {\n lines.push(`下载标题: ${skillResult.downloaded.title}`);\n lines.push(`下载文件: ${skillResult.downloaded.fileName}`);\n const text = safeUserText(skillResult.downloaded.contentText);\n if (text) {\n lines.push('下载正文摘录:');\n lines.push(text.slice(0, 8000));\n }\n }\n return lines.join('\\n');\n}\n\nfunction determineClientAction(skillResult, message) {\n if (skillResult && skillResult.downloaded) {\n const text = safeUserText(message);\n if (/法布施|全球法布施|分享/.test(text)) {\n return {\n type: 'prepare_dharma_share_text',\n title: skillResult.downloaded.title,\n text: skillResult.downloaded.contentText || '',\n };\n }\n }\n return null;\n}\n\nasync function runResourceFinderSkill({ message, user, onStep }) {\n if (!looksLikeResourceTask(message)) return null;\n\n const skillName = 'resource-finder-downloader';\n const query = extractResourceQuery(message);\n const audioTask = looksLikeAudioQuery(message) || looksLikeAudioQuery(query);\n onStep?.({\n type: 'skill.step',\n skillName,\n stage: 'start',\n title: '调用资源查找下载 skill',\n message: `识别到资源任务: ${query}`,\n });\n\n onStep?.({\n type: 'skill.step',\n skillName,\n stage: 'search',\n title: '搜索可分享资源',\n message: audioTask\n ? '优先检索公开可访问音频资源,失败时再回落到佛典正文资源。'\n : '优先检索 CBETA 佛典正文接口,搜索索引不可用时使用内置经典映射。',\n });\n const candidates = await findResourceCandidates(query, 8);\n if (candidates.length === 0) {\n onStep?.({\n type: 'skill.step',\n skillName,\n stage: 'empty',\n title: '没有找到可下载资源',\n message: '后端没有找到明确可下载且可分享的资源。',\n });\n return {\n skillName,\n query,\n candidates: [],\n selected: null,\n downloaded: null,\n };\n }\n\n const selected = candidates[0];\n onStep?.({\n type: 'skill.step',\n skillName,\n stage: 'select',\n title: '选择可信候选资源',\n message: `${selected.title} / ${selected.sourceName}`,\n });\n\n onStep?.({\n type: 'skill.step',\n skillName,\n stage: 'download',\n title: '下载并提取资源',\n message: selected.url,\n });\n const cbeta = parseCbetaResource(selected);\n const internetArchive = parseInternetArchiveResource(selected);\n const downloaded = cbeta\n ? await downloadCbetaResource({ ...selected, ...cbeta })\n : internetArchive\n ? await downloadInternetArchiveAudio({ ...selected, ...internetArchive })\n : await downloadWebResource(selected);\n const persisted = persistDownloadedResource(user, downloaded);\n\n onStep?.({\n type: 'skill.step',\n skillName,\n stage: 'done',\n title: '资源已准备完成',\n message: downloaded.fileName,\n });\n\n return {\n skillName,\n query,\n candidates,\n selected,\n downloaded: {\n ...downloaded,\n downloadId: persisted.id,\n },\n };\n}\n\napp.post(\n '/api/resources/search',\n asyncHandler(async (req, res) => {\n const query = safeUserText(req.body?.query);\n const limit = Math.min(Math.max(Number(req.body?.limit || 12), 1), 20);\n if (query.length < 2) {\n return jsonResponse(res, 400, { success: false, message: 'query is required' });\n }\n\n const items = await findResourceCandidates(query, limit);\n\n jsonResponse(res, 200, {\n success: true,\n source: 'resource-index',\n items,\n });\n }),\n);\n\nfunction parseCbetaResource(input) {\n const work = safeUserText(input.work);\n const juan = Number(input.juan || 1);\n if (work) return { work, juan: Number.isFinite(juan) ? juan : 1 };\n\n const url = safeUserText(input.url);\n const match = /^cbeta:([^:]+):(\\d+)$/i.exec(url);\n if (!match) return null;\n return {\n work: match[1].toUpperCase(),\n juan: Number(match[2] || 1),\n };\n}\n\nfunction parseInternetArchiveResource(input) {\n const url = safeUserText(input.url);\n const match = /^ia:([^:]+)$/i.exec(url);\n const identifier = match?.[1] || safeUserText(input.identifier);\n if (!identifier) return null;\n return { identifier };\n}\n\nfunction safeFileName(value) {\n const normalized = String(value || 'dharma-resource')\n .replace(/[\\\\/:*?\"<>|]+/g, '-')\n .replace(/\\s+/g, '-')\n .replace(/-+/g, '-')\n .replace(/^-|-$/g, '')\n .slice(0, 80);\n return normalized || 'dharma-resource';\n}\n\nfunction extractCbetaHtml(data) {\n const first = Array.isArray(data?.results) ? data.results[0] : null;\n if (typeof first === 'string') return first;\n if (first && typeof first === 'object' && typeof first.html === 'string') return first.html;\n if (typeof data?.html === 'string') return data.html;\n return '';\n}\n\nasync function downloadCbetaResource(resource) {\n const url = buildUrl(cbetaApiRoot, 'juans', {\n work: resource.work,\n juan: resource.juan,\n work_info: 1,\n toc: 1,\n });\n const data = await fetchJson(url);\n const workInfo = data.work_info || {};\n const title = safeUserText(workInfo.title) || safeUserText(resource.title) || resource.work;\n const html = extractCbetaHtml(data);\n const text = cleanExtractedText(stripHtml(html));\n if (!text) throw new Error('CBETA resource returned empty content');\n return {\n title,\n sourceName: 'CBETA 佛典',\n url: `cbeta:${resource.work}:${resource.juan}`,\n contentText: text.slice(0, maxResourceTextChars),\n fileName: `${resource.work}_${resource.juan}_${safeFileName(title)}.txt`,\n };\n}\n\nasync function downloadInternetArchiveAudio(resource) {\n const identifier = safeUserText(resource.identifier);\n if (!identifier) throw new Error('Internet Archive identifier is required');\n const metadata = await fetchJson(`https://archive.org/metadata/${encodeURIComponent(identifier)}`);\n const title = safeUserText(metadata?.metadata?.title) || safeUserText(resource.title) || identifier;\n const files = Array.isArray(metadata?.files) ? metadata.files : [];\n const audioFile = files.find((file) => {\n const name = safeUserText(file?.name);\n const format = safeUserText(file?.format).toLowerCase();\n return /\\.(mp3|m4a|ogg)$/i.test(name) || format.includes('mp3');\n });\n if (!audioFile?.name) {\n throw new Error('Internet Archive audio item has no downloadable audio file');\n }\n const sourceFileName = audioFile.name;\n const fileUrl = `https://archive.org/download/${encodeURIComponent(identifier)}/${sourceFileName\n .split('/')\n .map((part) => encodeURIComponent(part))\n .join('/')}`;\n const response = await fetch(fileUrl, {\n headers: {\n Accept: 'audio/mpeg, audio/*;q=0.9, */*;q=0.5',\n 'User-Agent': 'DachengResourceDownloader/1.0 (+https://ombhrum.com)',\n },\n signal: AbortSignal.timeout(60_000),\n });\n if (!response.ok) {\n throw new Error(`Internet Archive audio download failed: ${response.status}`);\n }\n const binaryContent = Buffer.from(await response.arrayBuffer());\n if (!binaryContent.length) throw new Error('Internet Archive audio returned empty content');\n const sizeMb = (binaryContent.length / 1024 / 1024).toFixed(1);\n return {\n title,\n sourceName: 'Internet Archive 音频',\n url: fileUrl,\n contentText: `已下载音频资源: ${title}\\n文件名: ${sourceFileName}\\n大小: ${sizeMb} MB\\n来源: ${fileUrl}`,\n binaryContent,\n fileName: safeFileName(sourceFileName),\n };\n}\n\nasync function downloadWebResource(input) {\n const url = safeUserText(input.url);\n const parsed = new URL(url);\n if (!['http:', 'https:'].includes(parsed.protocol)) {\n throw new Error('Only http/https resources can be downloaded');\n }\n\n const response = await fetch(parsed, {\n headers: {\n Accept: 'text/html, text/plain, application/xhtml+xml, application/xml;q=0.8, */*;q=0.5',\n 'User-Agent': 'DachengResourceDownloader/1.0 (+https://ombhrum.com)',\n },\n signal: AbortSignal.timeout(25_000),\n });\n if (!response.ok) throw new Error(`Resource download failed: ${response.status}`);\n const contentType = response.headers.get('content-type') || '';\n const body = await response.text();\n const title = safeUserText(input.title) || parsed.hostname;\n const contentText = contentType.includes('html') ? cleanExtractedText(stripHtml(body)) : body.trim();\n if (!contentText) throw new Error('Resource returned empty content');\n return {\n title,\n sourceName: safeUserText(input.sourceName) || parsed.hostname,\n url,\n contentText: contentText.slice(0, maxResourceTextChars),\n fileName: `${safeFileName(title)}.txt`,\n };\n}\n\nfunction persistDownloadedResource(user, content) {\n const id = crypto.randomUUID();\n const fileName = `${id}-${safeFileName(content.fileName)}`;\n const filePath = path.join(resourcesDir, fileName);\n if (content.binaryContent) {\n fs.writeFileSync(filePath, content.binaryContent);\n } else {\n fs.writeFileSync(filePath, content.contentText, 'utf8');\n }\n statements.insertResourceDownload.run({\n id,\n userId: user.userId,\n title: content.title,\n sourceName: content.sourceName,\n sourceUrl: content.url,\n fileName: content.fileName,\n filePath,\n createdAt: nowIso(),\n });\n return { id, fileName, filePath };\n}\n\nfunction createLibreChatMcpServer() {\n const server = new McpServer({\n name: 'dacheng-ai-tools',\n version: '0.1.0',\n });\n\n server.registerTool(\n 'search_dharma_resources',\n {\n title: 'Search dharma resources',\n description:\n 'Search for legally shareable Buddhist scripture, text, web, or audio resources that can be prepared for Fabushi sharing.',\n inputSchema: {\n query: z.string().min(2).max(200),\n limit: z.number().int().min(1).max(12).optional(),\n },\n outputSchema: {\n query: z.string(),\n items: z.array(\n z.object({\n id: z.string(),\n title: z.string(),\n sourceName: z.string(),\n url: z.string(),\n snippet: z.string(),\n resourceType: z.string(),\n work: z.string().optional(),\n juan: z.number().optional(),\n }),\n ),\n },\n annotations: {\n readOnlyHint: true,\n destructiveHint: false,\n openWorldHint: true,\n },\n },\n async ({ query, limit }) => {\n const items = await findResourceCandidates(query, limit ?? 8);\n return textToolResult(`Found ${items.length} candidate resources for \"${query}\".`, {\n query,\n items,\n });\n },\n );\n\n server.registerTool(\n 'download_dharma_resource',\n {\n title: 'Download dharma resource',\n description:\n 'Download and extract a selected resource. Use the url/work/juan returned by search_dharma_resources whenever possible.',\n inputSchema: {\n url: z.string().min(1).max(1000),\n title: z.string().max(300).optional(),\n sourceName: z.string().max(120).optional(),\n work: z.string().max(40).optional(),\n juan: z.number().int().min(1).optional(),\n identifier: z.string().max(300).optional(),\n },\n outputSchema: {\n title: z.string(),\n sourceName: z.string(),\n url: z.string(),\n fileName: z.string(),\n downloadId: z.string(),\n contentText: z.string(),\n },\n annotations: {\n readOnlyHint: false,\n destructiveHint: false,\n openWorldHint: true,\n },\n },\n async (input) => {\n const user = { userId: 'librechat:mcp' };\n const cbeta = parseCbetaResource(input);\n const internetArchive = parseInternetArchiveResource(input);\n const content = cbeta\n ? await downloadCbetaResource({ ...input, ...cbeta })\n : internetArchive\n ? await downloadInternetArchiveAudio({ ...input, ...internetArchive })\n : await downloadWebResource(input);\n const persisted = persistDownloadedResource(user, content);\n const { binaryContent: _binaryContent, ...publicContent } = content;\n return textToolResult(`Downloaded ${publicContent.title} as ${publicContent.fileName}.`, {\n ...publicContent,\n downloadId: persisted.id,\n });\n },\n );\n\n server.registerTool(\n 'prepare_dharma_share_text',\n {\n title: 'Prepare dharma sharing text',\n description:\n 'Prepare a text payload for the Fabushi app to confirm and add to global dharma sharing. This returns a clientAction; it does not start sending.',\n inputSchema: {\n title: z.string().min(1).max(120),\n text: z.string().min(1).max(20000),\n },\n outputSchema: {\n clientAction: z.object({\n type: z.literal('prepare_dharma_share_text'),\n title: z.string(),\n text: z.string(),\n }),\n },\n annotations: {\n readOnlyHint: false,\n destructiveHint: false,\n openWorldHint: false,\n },\n },\n async ({ title, text }) =>\n textToolResult(`Prepared \"${title}\" for user confirmation in the Fabushi app.`, {\n clientAction: {\n type: 'prepare_dharma_share_text',\n title: safeUserText(title).slice(0, 120),\n text: String(text || '').trim().slice(0, 20000),\n },\n }),\n );\n\n server.registerTool(\n 'prepare_practice_book_item',\n {\n title: 'Prepare practice book item',\n description:\n 'Prepare a scripture, chant, or practice item for the Fabushi app practice book. This returns a clientAction; it does not mutate the app by itself.',\n inputSchema: {\n title: z.string().min(1).max(120),\n sourceName: z.string().max(120).optional(),\n text: z.string().min(1).max(20000),\n },\n outputSchema: {\n clientAction: z.object({\n type: z.literal('prepare_practice_book_item'),\n title: z.string(),\n sourceName: z.string(),\n text: z.string(),\n }),\n },\n annotations: {\n readOnlyHint: false,\n destructiveHint: false,\n openWorldHint: false,\n },\n },\n async ({ title, sourceName, text }) =>\n textToolResult(`Prepared \"${title}\" for the Fabushi practice book confirmation flow.`, {\n clientAction: {\n type: 'prepare_practice_book_item',\n title: safeUserText(title).slice(0, 120),\n sourceName: safeUserText(sourceName || '大乘 AI').slice(0, 120),\n text: String(text || '').trim().slice(0, 20000),\n },\n }),\n );\n\n return server;\n}\n\nasync function handleMcpRequest(req, res) {\n const server = createLibreChatMcpServer();\n const transport = new StreamableHTTPServerTransport({\n sessionIdGenerator: undefined,\n enableJsonResponse: true,\n });\n\n res.on('close', () => {\n transport.close();\n server.close();\n });\n\n await server.connect(transport);\n await transport.handleRequest(req, res, req.body);\n}\n\napp.all('/mcp', async (req, res) => {\n try {\n await handleMcpRequest(req, res);\n } catch (error) {\n logger.error({ error: error.stack || String(error) }, 'MCP request failed');\n if (!res.headersSent) {\n res.status(500).json({\n error: 'MCP request failed',\n message: publicAiErrorMessage(error),\n });\n }\n }\n});\n\napp.post(\n '/api/resources/download',\n asyncHandler(async (req, res) => {\n const user = await resolveUser(req, req.body || {});\n const cbeta = parseCbetaResource(req.body || {});\n const internetArchive = parseInternetArchiveResource(req.body || {});\n const content = cbeta\n ? await downloadCbetaResource({ ...req.body, ...cbeta })\n : internetArchive\n ? await downloadInternetArchiveAudio({ ...req.body, ...internetArchive })\n : await downloadWebResource(req.body || {});\n const persisted = persistDownloadedResource(user, content);\n const { binaryContent: _binaryContent, ...publicContent } = content;\n jsonResponse(res, 200, {\n success: true,\n downloadId: persisted.id,\n ...publicContent,\n });\n }),\n);\n\napp.post(\n '/api/codex/resource-task',\n asyncHandler(async (req, res) => {\n if (process.env.ENABLE_CODEX_SDK !== 'true') {\n return jsonResponse(res, 403, {\n success: false,\n message: 'Codex SDK task runner is disabled. Set ENABLE_CODEX_SDK=true on the server to enable it.',\n });\n }\n const prompt = safeUserText(req.body?.prompt);\n if (!prompt) {\n return jsonResponse(res, 400, { success: false, message: 'prompt is required' });\n }\n const { Codex } = await import('@openai/codex-sdk');\n const codexRuntime = createCodexDeepSeekRuntime();\n const codex = new Codex(codexRuntime.enabled ? codexRuntime.options : {});\n const thread = codex.startThread(\n codexRuntime.enabled\n ? codexRuntime.threadOptions\n : { skipGitRepoCheck: true, networkAccessEnabled: true },\n );\n const result = await thread.run(prompt);\n jsonResponse(res, 200, {\n success: true,\n threadId: thread.id,\n provider: codexRuntime.provider,\n model: deepseekModel,\n result,\n });\n }),\n);\n\napp.use((error, _req, res, _next) => {\n const status = Number(error.statusCode || error.status || 500);\n const safeStatus = status >= 400 && status < 600 ? status : 500;\n logger.error({ error: error.stack || String(error) }, 'request failed');\n jsonResponse(res, safeStatus, {\n success: false,\n message: error.message || 'Internal Server Error',\n details: error.details,\n });\n});\n\nconst port = Number(process.env.PORT || 8788);\napp.listen(port, '0.0.0.0', () => {\n logger.info(\n {\n port,\n dbPath,\n dataDir,\n deepseekModel,\n cbetaApiRoot,\n },\n 'Dacheng AI backend started',\n );\n});\n"}, "15d299ace35e37138c1dfcd36c1f182f2e26a4ab": {"t": 1785757980.3850043, "n": 116302, "h": "7bf5d2b2cafd25c6", "c": "import crypto from 'node:crypto';\nimport fs from 'node:fs';\nimport path from 'node:path';\nimport process from 'node:process';\nimport { fileURLToPath } from 'node:url';\n\nimport { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';\nimport { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';\nimport Database from 'better-sqlite3';\nimport cors from 'cors';\nimport dotenv from 'dotenv';\nimport express from 'express';\nimport rateLimit from 'express-rate-limit';\nimport helmet from 'helmet';\nimport pino from 'pino';\nimport { z } from 'zod';\n\ndotenv.config();\n\nconst __dirname = path.dirname(fileURLToPath(import.meta.url));\nconst rootDir = path.resolve(__dirname, '..');\nconst dataDir = process.env.DATA_DIR || path.join(rootDir, 'data');\nconst resourcesDir = path.join(dataDir, 'resources');\nconst openClawRuntimeUpdatesDir =\n process.env.OPENCLAW_RUNTIME_UPDATES_DIR || path.join(dataDir, 'openclaw-runtime');\nconst openClawRuntimeEnableMacosUpdates =\n process.env.OPENCLAW_RUNTIME_ENABLE_MACOS_UPDATES === 'true';\nconst codexHomeDir = process.env.CODEX_HOME || path.join(dataDir, 'codex-home');\nconst codexTempDir = process.env.CODEX_TMPDIR || path.join(dataDir, 'codex-tmp');\nconst codexRuntimeDir = process.env.XDG_RUNTIME_DIR || path.join(dataDir, 'codex-runtime');\nconst dbPath = process.env.SQLITE_PATH || path.join(dataDir, 'dacheng-ai.sqlite');\n\nfs.mkdirSync(resourcesDir, { recursive: true });\nfs.mkdirSync(openClawRuntimeUpdatesDir, { recursive: true });\nfs.mkdirSync(codexHomeDir, { recursive: true });\nfs.mkdirSync(codexTempDir, { recursive: true });\nfs.mkdirSync(codexRuntimeDir, { recursive: true });\n\nconst logger = pino({\n level: process.env.LOG_LEVEL || 'info',\n});\n\nconst app = express();\napp.disable('x-powered-by');\napp.set('trust proxy', Number(process.env.TRUST_PROXY_HOPS || 1));\napp.use(helmet());\nconst corsOrigin =\n !process.env.CORS_ORIGIN || process.env.CORS_ORIGIN.trim() === '*'\n ? true\n : process.env.CORS_ORIGIN.split(',').map((item) => item.trim()).filter(Boolean);\napp.use(cors({ origin: corsOrigin }));\napp.use(express.json({ limit: '1mb' }));\napp.use(\n rateLimit({\n windowMs: 60_000,\n limit: Number(process.env.RATE_LIMIT_PER_MINUTE || 90),\n standardHeaders: true,\n legacyHeaders: false,\n }),\n);\n\nconst db = new Database(dbPath);\ndb.pragma('journal_mode = WAL');\ndb.pragma('foreign_keys = ON');\n\ndb.exec(`\n CREATE TABLE IF NOT EXISTS conversations (\n id TEXT PRIMARY KEY,\n user_id TEXT NOT NULL,\n title TEXT NOT NULL,\n provider TEXT NOT NULL,\n model TEXT NOT NULL,\n created_at TEXT NOT NULL,\n updated_at TEXT NOT NULL\n );\n\n CREATE INDEX IF NOT EXISTS idx_conversations_user_updated\n ON conversations (user_id, updated_at DESC);\n\n CREATE TABLE IF NOT EXISTS messages (\n id TEXT PRIMARY KEY,\n conversation_id TEXT NOT NULL,\n role TEXT NOT NULL,\n content TEXT NOT NULL,\n prompt_tokens INTEGER NOT NULL DEFAULT 0,\n completion_tokens INTEGER NOT NULL DEFAULT 0,\n total_tokens INTEGER NOT NULL DEFAULT 0,\n created_at TEXT NOT NULL,\n FOREIGN KEY (conversation_id) REFERENCES conversations(id) ON DELETE CASCADE\n );\n\n CREATE INDEX IF NOT EXISTS idx_messages_conversation_created\n ON messages (conversation_id, created_at ASC);\n\n CREATE TABLE IF NOT EXISTS usage_months (\n user_id TEXT NOT NULL,\n month TEXT NOT NULL,\n total_tokens INTEGER NOT NULL DEFAULT 0,\n updated_at TEXT NOT NULL,\n PRIMARY KEY (user_id, month)\n );\n\n CREATE TABLE IF NOT EXISTS resource_downloads (\n id TEXT PRIMARY KEY,\n user_id TEXT NOT NULL,\n title TEXT NOT NULL,\n source_name TEXT NOT NULL,\n source_url TEXT NOT NULL,\n file_name TEXT NOT NULL,\n file_path TEXT NOT NULL,\n created_at TEXT NOT NULL\n );\n\n CREATE TABLE IF NOT EXISTS agent_runs (\n id TEXT PRIMARY KEY,\n user_id TEXT NOT NULL,\n conversation_id TEXT NOT NULL,\n message_id TEXT NOT NULL,\n openclaw_run_id TEXT,\n openclaw_session_key TEXT NOT NULL,\n status TEXT NOT NULL,\n mode TEXT NOT NULL,\n provider TEXT NOT NULL,\n model TEXT,\n input_tokens INTEGER NOT NULL DEFAULT 0,\n output_tokens INTEGER NOT NULL DEFAULT 0,\n tool_call_count INTEGER NOT NULL DEFAULT 0,\n cost_microusd INTEGER NOT NULL DEFAULT 0,\n started_at TEXT NOT NULL,\n completed_at TEXT,\n failed_at TEXT,\n error_code TEXT,\n error_message TEXT\n );\n\n CREATE INDEX IF NOT EXISTS idx_agent_runs_user_started\n ON agent_runs (user_id, started_at DESC);\n\n CREATE INDEX IF NOT EXISTS idx_agent_runs_status_started\n ON agent_runs (status, started_at DESC);\n\n CREATE TABLE IF NOT EXISTS agent_message_feedback (\n id TEXT PRIMARY KEY,\n message_id TEXT NOT NULL,\n user_id TEXT NOT NULL,\n rating TEXT NOT NULL,\n reason TEXT,\n comment TEXT,\n created_at TEXT NOT NULL\n );\n`);\n\nconst statements = {\n insertConversation: db.prepare(`\n INSERT INTO conversations (id, user_id, title, provider, model, created_at, updated_at)\n VALUES (@id, @userId, @title, @provider, @model, @createdAt, @updatedAt)\n `),\n updateConversation: db.prepare(`\n UPDATE conversations\n SET title = @title, updated_at = @updatedAt\n WHERE id = @id AND user_id = @userId\n `),\n getConversation: db.prepare(`\n SELECT * FROM conversations WHERE id = ? AND user_id = ? LIMIT 1\n `),\n listConversations: db.prepare(`\n SELECT id, title, updated_at AS updatedAt\n FROM conversations\n WHERE user_id = ?\n ORDER BY updated_at DESC\n LIMIT 30\n `),\n insertMessage: db.prepare(`\n INSERT INTO messages (\n id, conversation_id, role, content,\n prompt_tokens, completion_tokens, total_tokens, created_at\n )\n VALUES (\n @id, @conversationId, @role, @content,\n @promptTokens, @completionTokens, @totalTokens, @createdAt\n )\n `),\n listMessages: db.prepare(`\n SELECT role, content, prompt_tokens AS promptTokens,\n completion_tokens AS completionTokens, total_tokens AS totalTokens,\n created_at AS createdAt\n FROM messages\n WHERE conversation_id = ?\n ORDER BY created_at ASC\n `),\n getUsage: db.prepare(`\n SELECT total_tokens AS totalTokens FROM usage_months WHERE user_id = ? AND month = ?\n `),\n upsertUsage: db.prepare(`\n INSERT INTO usage_months (user_id, month, total_tokens, updated_at)\n VALUES (@userId, @month, @totalTokens, @updatedAt)\n ON CONFLICT(user_id, month)\n DO UPDATE SET total_tokens = usage_months.total_tokens + excluded.total_tokens,\n updated_at = excluded.updated_at\n `),\n insertResourceDownload: db.prepare(`\n INSERT INTO resource_downloads (\n id, user_id, title, source_name, source_url, file_name, file_path, created_at\n )\n VALUES (\n @id, @userId, @title, @sourceName, @sourceUrl, @fileName, @filePath, @createdAt\n )\n `),\n insertAgentRun: db.prepare(`\n INSERT INTO agent_runs (\n id, user_id, conversation_id, message_id, openclaw_run_id, openclaw_session_key,\n status, mode, provider, model, started_at\n )\n VALUES (\n @id, @userId, @conversationId, @messageId, @openClawRunId, @openClawSessionKey,\n @status, @mode, @provider, @model, @startedAt\n )\n `),\n getAgentRun: db.prepare(`\n SELECT * FROM agent_runs WHERE id = ? AND user_id = ? LIMIT 1\n `),\n updateAgentRunStatus: db.prepare(`\n UPDATE agent_runs\n SET status = @status,\n openclaw_run_id = COALESCE(@openClawRunId, openclaw_run_id),\n input_tokens = COALESCE(@inputTokens, input_tokens),\n output_tokens = COALESCE(@outputTokens, output_tokens),\n tool_call_count = COALESCE(@toolCallCount, tool_call_count),\n completed_at = @completedAt,\n failed_at = @failedAt,\n error_code = @errorCode,\n error_message = @errorMessage\n WHERE id = @id AND user_id = @userId\n `),\n insertAgentFeedback: db.prepare(`\n INSERT INTO agent_message_feedback (id, message_id, user_id, rating, reason, comment, created_at)\n VALUES (@id, @messageId, @userId, @rating, @reason, @comment, @createdAt)\n `),\n};\n\nconst deepseekApiKey = process.env.DEEPSEEK_API_KEY || '';\nconst deepseekBaseUrl = (process.env.DEEPSEEK_BASE_URL || 'https://api.deepseek.com').replace(/\\/+$/, '');\nconst deepseekModel = process.env.DEEPSEEK_MODEL || 'deepseek-chat';\nconst cbetaApiRoot = (process.env.CBETA_API_ROOT || 'https://api.ombhrum.com/api/cbeta').replace(/\\/+$/, '');\nconst fabushiApiBaseUrl = (process.env.FABUSHI_API_BASE_URL || 'https://api.ombhrum.com').replace(/\\/+$/, '');\nconst memberMonthlyLimit = Number(process.env.MEMBER_MONTHLY_TOKEN_LIMIT || 1_000_000);\nconst freeMonthlyLimit = Number(process.env.FREE_MONTHLY_TOKEN_LIMIT || 1_000);\nconst requireMemberForAi = process.env.REQUIRE_MEMBER_FOR_AI === 'true';\nconst deepseekMaxCompletionTokens = Math.max(1, Number(process.env.DEEPSEEK_MAX_COMPLETION_TOKENS || 1400));\nconst maxResourceTextChars = Number(process.env.MAX_RESOURCE_TEXT_CHARS || 80_000);\nconst openClawRuntimeManifestPath =\n process.env.OPENCLAW_RUNTIME_MANIFEST_PATH || path.join(openClawRuntimeUpdatesDir, 'manifest.json');\nconst openClawRuntimePublicBaseUrl = (process.env.OPENCLAW_RUNTIME_PUBLIC_BASE_URL || '').replace(/\\/+$/, '');\nconst enableLibreChatAgentChat = process.env.ENABLE_LIBRECHAT_AGENT_CHAT === 'true';\nconst libreChatAgentsBaseUrl = (\n process.env.LIBRECHAT_AGENTS_BASE_URL || 'http://127.0.0.1:3080/api/agents/v1'\n).replace(/\\/+$/, '');\nconst libreChatAgentApiKey = process.env.LIBRECHAT_AGENT_API_KEY || '';\nconst libreChatAgentId = process.env.LIBRECHAT_AGENT_ID || '';\nconst enableCodexSdkChat = process.env.ENABLE_CODEX_SDK_CHAT === 'true';\nconst enableOpenClawAgentChat = process.env.ENABLE_OPENCLAW_AGENT_CHAT === 'true';\nconst openClawGatewayUrl = (process.env.OPENCLAW_GATEWAY_URL || 'http://127.0.0.1:18789').replace(/\\/+$/, '');\nconst openClawGatewayToken = process.env.OPENCLAW_GATEWAY_TOKEN || '';\nconst openClawAgentId = process.env.OPENCLAW_AGENT_ID || 'fabushi-public-agent';\nconst openClawRunsEndpoint = process.env.OPENCLAW_RUNS_ENDPOINT || '';\nconst codexDeepSeekProviderId = 'deepseek-chat-completions';\nconst codexResponsesBaseUrl = (\n process.env.CODEX_DEEPSEEK_RESPONSES_BASE_URL ||\n `http://127.0.0.1:${process.env.PORT || 8788}/codex-deepseek/v1`\n).replace(/\\/+$/, '');\n\nconst systemPrompt = [\n '你是“大乘”App 的 AI 助手。',\n '你的核心任务是帮助用户查找、整理、理解并全球法布施合法可分享的佛法资源。',\n '回答要庄重、简洁、可执行;涉及经典、仪轨或资源时,提醒用户尊重版权、来源和当地法规。',\n '如果用户想找资源、下载经文、寻找音频或准备可分享资料,你必须先在后端自动执行搜索、验证和下载步骤;不要要求用户去前端 + 菜单手动查找。',\n '你可以把执行进度总结给用户,但不要输出隐藏推理原文。',\n].join('\\n');\n\nfunction nowIso() {\n return new Date().toISOString();\n}\n\nfunction monthKey(date = new Date()) {\n return `${date.getUTCFullYear()}-${String(date.getUTCMonth() + 1).padStart(2, '0')}`;\n}\n\nfunction jsonResponse(res, status, payload) {\n res.status(status).json(payload);\n}\n\nasync function readJsonFile(filePath) {\n const raw = await fs.promises.readFile(filePath, 'utf8');\n return JSON.parse(raw);\n}\n\nfunction requestOrigin(req) {\n if (openClawRuntimePublicBaseUrl) return openClawRuntimePublicBaseUrl;\n const proto = req.get('X-Forwarded-Proto') || req.protocol || 'https';\n const host = req.get('X-Forwarded-Host') || req.get('Host');\n return host ? `${proto}://${host}` : '';\n}\n\nfunction absolutizeOpenClawRuntimeUrl(req, value) {\n const text = readText(value);\n if (!text) return '';\n if (/^https?:\\/\\//i.test(text)) return text;\n const normalized = text.startsWith('/')\n ? text\n : `/api/openclaw/runtime/files/${text.replace(/^\\/+/, '')}`;\n const origin = requestOrigin(req);\n return origin ? new URL(normalized, `${origin}/`).toString() : normalized;\n}\n\nfunction textToolResult(text, structuredContent = {}) {\n return {\n structuredContent,\n content: [{ type: 'text', text }],\n };\n}\n\nfunction sseHeaders(res) {\n res.status(200);\n res.setHeader('Content-Type', 'text/event-stream; charset=utf-8');\n res.setHeader('Cache-Control', 'no-cache, no-transform');\n res.setHeader('Connection', 'keep-alive');\n res.setHeader('X-Accel-Buffering', 'no');\n if (typeof res.flushHeaders === 'function') res.flushHeaders();\n}\n\nfunction writeSse(res, event, payload) {\n res.write(`event: ${event}\\n`);\n res.write(`data: ${JSON.stringify(payload)}\\n\\n`);\n}\n\nfunction asyncHandler(fn) {\n return (req, res, next) => {\n Promise.resolve(fn(req, res, next)).catch(next);\n };\n}\n\nfunction bearerToken(req) {\n const delegated = readText(req.get('x-dacheng-auth-token'));\n if (\n delegated &&\n delegated !== 'DACHENG_AUTH_TOKEN' &&\n !delegated.startsWith('secretref-env:') &&\n delegated !== 'dacheng-openclaw-proxy'\n ) {\n return delegated;\n }\n const header = req.get('Authorization') || '';\n if (!header.startsWith('Bearer ')) return '';\n const token = header.slice('Bearer '.length).trim();\n return token === 'dacheng-openclaw-proxy' ? '' : token;\n}\n\nfunction sha256(value) {\n return crypto.createHash('sha256').update(value).digest('hex');\n}\n\nfunction safeUserText(value, fallback = '') {\n if (typeof value !== 'string') return fallback;\n return value.replace(/\\u0000/g, '').trim();\n}\n\nfunction estimateTokens(text) {\n const normalized = String(text || '').trim();\n if (!normalized) return 0;\n return Math.max(1, Math.ceil(normalized.length / 3));\n}\n\nfunction readText(value) {\n if (value === undefined || value === null) return '';\n return String(value).replace(/\\u0000/g, '').trim();\n}\n\nfunction unwrapPayload(payload) {\n if (!payload || typeof payload !== 'object') return {};\n return payload.data && typeof payload.data === 'object' ? payload.data : payload;\n}\n\nfunction readFirstText(values) {\n for (const value of values) {\n const text = readText(value);\n if (text) return text;\n }\n return '';\n}\n\nfunction activeFlag(value) {\n if (value === true) return true;\n if (value === 1) return true;\n if (typeof value !== 'string') return false;\n return ['true', '1', 'yes', 'active'].includes(value.trim().toLowerCase());\n}\n\nfunction futureDate(value) {\n const text = readText(value);\n if (!text) return false;\n const timestamp = Date.parse(text);\n return Number.isFinite(timestamp) && timestamp > Date.now();\n}\n\nfunction normalizeMembershipPayload(payload) {\n if (!payload || typeof payload !== 'object') return false;\n const data = unwrapPayload(payload);\n const membership = data.membership && typeof data.membership === 'object' ? data.membership : data;\n const type = readText(membership.type || membership.membershipType || data.membershipType).toLowerCase();\n const status = readText(membership.status || data.status).toLowerCase();\n const expiry = membership.expiresAt ||\n membership.expires_at ||\n membership.membershipExpiry ||\n membership.membership_expires_at ||\n data.membershipExpiry ||\n data.membership_expires_at;\n const hasExplicitActiveFlag =\n membership.isActive !== undefined ||\n membership.active !== undefined ||\n membership.hasPremiumAccess !== undefined ||\n membership.hasPremiumMembership !== undefined ||\n membership.isPremium !== undefined;\n\n if (hasExplicitActiveFlag) {\n return Boolean(\n activeFlag(membership.isActive) ||\n activeFlag(membership.active) ||\n activeFlag(membership.hasPremiumAccess) ||\n activeFlag(membership.hasPremiumMembership) ||\n activeFlag(membership.isPremium),\n );\n }\n\n return Boolean(\n status === 'active' ||\n type === 'paid' ||\n type === 'premium' ||\n type === 'trial' ||\n futureDate(expiry),\n );\n}\n\nfunction extractRemoteAccount(payload) {\n const data = unwrapPayload(payload);\n const nestedUser = data.user && typeof data.user === 'object' ? data.user : {};\n const id = readFirstText([\n data.userId,\n data.user_id,\n data.userNo,\n data.user_no,\n data.id,\n nestedUser.userId,\n nestedUser.user_id,\n nestedUser.userNo,\n nestedUser.user_no,\n nestedUser.id,\n ]);\n const username = readFirstText([data.username, data.name, nestedUser.username, nestedUser.name]);\n const email = readFirstText([data.email, nestedUser.email]);\n const membership = data.membership && typeof data.membership === 'object' ? data.membership : null;\n return {\n authenticated: Boolean(id || username || email || membership || data.success === true),\n id,\n username,\n email,\n isMember: normalizeMembershipPayload(payload),\n membership,\n };\n}\n\nasync function resolveRemoteAccount(token) {\n const fallback = {\n authenticated: false,\n id: '',\n username: '',\n email: '',\n isMember: false,\n membership: null,\n };\n if (!token || !fabushiApiBaseUrl) return fallback;\n\n const endpoints = [\n '/api/stripe/membership-status',\n '/api/alipay/check-membership',\n '/api/auth/user-info',\n ];\n\n for (const endpoint of endpoints) {\n try {\n const response = await fetch(`${fabushiApiBaseUrl}${endpoint}`, {\n headers: {\n Accept: 'application/json',\n Authorization: `Bearer ${token}`,\n },\n signal: AbortSignal.timeout(8_000),\n });\n if (response.status === 401 || response.status === 403) return fallback;\n if (!response.ok) continue;\n const payload = await response.json();\n const account = extractRemoteAccount(payload);\n if (account.authenticated || account.isMember) {\n return {\n ...fallback,\n ...account,\n authenticated: true,\n };\n }\n } catch (error) {\n logger.debug({ endpoint, error: String(error) }, 'membership lookup skipped');\n }\n }\n\n return fallback;\n}\n\nasync function resolveUser(req, body = {}) {\n const token = bearerToken(req);\n const usernameHint = safeUserText(body.username || req.query.username);\n const tokenHash = token ? sha256(token).slice(0, 24) : '';\n const anonymousHash = sha256(`${req.ip}|${req.get('User-Agent') || ''}`).slice(0, 24);\n const remoteAccount = await resolveRemoteAccount(token);\n const remoteIdentity = remoteAccount.id ||\n remoteAccount.username ||\n remoteAccount.email ||\n (remoteAccount.authenticated ? tokenHash : '');\n const userId = remoteAccount.authenticated\n ? `user:${remoteIdentity}`\n : tokenHash\n ? `token:${tokenHash}`\n : `anon:${anonymousHash}`;\n return {\n userId,\n username: remoteAccount.username || usernameHint,\n tokenHash,\n isAuthenticated: remoteAccount.authenticated,\n isMember: remoteAccount.isMember,\n membership: remoteAccount.membership,\n };\n}\n\nfunction usageFor(userId) {\n const month = monthKey();\n const row = statements.getUsage.get(userId, month);\n return {\n month,\n used: Number(row?.totalTokens || 0),\n };\n}\n\nfunction enforceTokenBudget(user, estimatedTokensForRequest) {\n if (requireMemberForAi && !user.isMember) {\n const error = new Error('大乘 AI 需要开通会员后使用');\n error.statusCode = 403;\n error.code = 'AI_MEMBERSHIP_REQUIRED';\n error.details = {\n requireMemberForAi,\n monthlyLimit: memberMonthlyLimit,\n remainingTokens: 0,\n };\n throw error;\n }\n\n const limit = user.isMember ? memberMonthlyLimit : freeMonthlyLimit;\n const usage = usageFor(user.userId);\n if (usage.used + estimatedTokensForRequest > limit) {\n const error = new Error('本月 AI token 额度已不足');\n error.statusCode = 429;\n error.code = 'USER_TOKEN_QUOTA_EXCEEDED';\n error.details = {\n monthlyLimit: limit,\n usedTokens: usage.used,\n remainingTokens: Math.max(0, limit - usage.used),\n };\n throw error;\n }\n return { limit, usage };\n}\n\nfunction completionBudgetFor(budget, estimatedPromptTokens) {\n const remainingAfterPrompt = budget.limit - budget.usage.used - estimatedPromptTokens;\n return Math.max(1, Math.min(deepseekMaxCompletionTokens, Math.floor(remainingAfterPrompt)));\n}\n\nfunction recordUsage(userId, totalTokens) {\n if (!totalTokens || totalTokens < 1) return;\n statements.upsertUsage.run({\n userId,\n month: monthKey(),\n totalTokens,\n updatedAt: nowIso(),\n });\n}\n\nfunction titleFromMessage(message) {\n const compact = message.replace(/\\s+/g, ' ').trim();\n if (!compact) return '新对话';\n return compact.length > 22 ? `${compact.slice(0, 22)}...` : compact;\n}\n\nfunction normalizeMessages(rows) {\n return rows.map((row) => ({\n role: row.role,\n content: row.content,\n }));\n}\n\nfunction normalizeMaxCompletionTokens(value) {\n const numeric = Number(value || deepseekMaxCompletionTokens);\n if (!Number.isFinite(numeric) || numeric < 1) return deepseekMaxCompletionTokens;\n return Math.max(1, Math.min(deepseekMaxCompletionTokens, Math.floor(numeric)));\n}\n\nfunction normalizeDeepSeekModelName(value) {\n const raw = readText(value || deepseekModel);\n if (!raw) return deepseekModel;\n const modelId = raw.includes('/') ? raw.split('/').pop() : raw;\n return modelId || deepseekModel;\n}\n\nasync function callDeepSeek(messages, options = {}) {\n if (!deepseekApiKey) {\n const error = new Error('DeepSeek API key is not configured');\n error.statusCode = 503;\n throw error;\n }\n\n const model = normalizeDeepSeekModelName(options.model);\n const response = await fetch(`${deepseekBaseUrl}/chat/completions`, {\n method: 'POST',\n headers: {\n Accept: 'application/json',\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${deepseekApiKey}`,\n },\n body: JSON.stringify({\n model,\n messages,\n temperature: 0.4,\n max_tokens: normalizeMaxCompletionTokens(options.maxCompletionTokens),\n stream: false,\n }),\n signal: AbortSignal.timeout(60_000),\n });\n\n const bodyText = await response.text();\n let payload;\n try {\n payload = bodyText ? JSON.parse(bodyText) : {};\n } catch {\n payload = { error: bodyText };\n }\n\n if (!response.ok) {\n const upstreamMessage = payload?.error?.message || payload?.message || '';\n const friendlyMessage =\n response.status === 402 || /insufficient\\s+balance/i.test(upstreamMessage)\n ? '大乘 AI 服务额度暂不可用,请联系管理员充值 DeepSeek 账户后重试。'\n : upstreamMessage || `DeepSeek request failed: ${response.status}`;\n const error = new Error(friendlyMessage);\n error.statusCode = response.status;\n if (response.status === 402 || /insufficient\\s+balance/i.test(upstreamMessage)) {\n error.code = 'DEEPSEEK_PROVIDER_QUOTA';\n }\n throw error;\n }\n\n const message = payload?.choices?.[0]?.message?.content?.trim();\n if (!message) {\n const error = new Error('DeepSeek returned an empty response');\n error.statusCode = 502;\n throw error;\n }\n\n return {\n message,\n model,\n usage: {\n promptTokens: Number(payload?.usage?.prompt_tokens || 0),\n completionTokens: Number(payload?.usage?.completion_tokens || 0),\n totalTokens: Number(payload?.usage?.total_tokens || 0),\n },\n };\n}\n\nasync function callDeepSeekStream(messages, callbacks = {}) {\n if (!deepseekApiKey) {\n const error = new Error('DeepSeek API key is not configured');\n error.statusCode = 503;\n throw error;\n }\n\n const model = normalizeDeepSeekModelName(callbacks.model);\n const response = await fetch(`${deepseekBaseUrl}/chat/completions`, {\n method: 'POST',\n headers: {\n Accept: 'text/event-stream',\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${deepseekApiKey}`,\n },\n body: JSON.stringify({\n model,\n messages,\n temperature: 0.4,\n max_tokens: normalizeMaxCompletionTokens(callbacks.maxCompletionTokens),\n stream: true,\n stream_options: { include_usage: true },\n }),\n signal: callbacks.signal || AbortSignal.timeout(90_000),\n });\n\n if (!response.ok) {\n const bodyText = await response.text();\n let payload;\n try {\n payload = bodyText ? JSON.parse(bodyText) : {};\n } catch {\n payload = { error: bodyText };\n }\n const upstreamMessage = payload?.error?.message || payload?.message || '';\n const friendlyMessage =\n response.status === 402 || /insufficient\\s+balance/i.test(upstreamMessage)\n ? '大乘 AI 服务额度暂不可用,请联系管理员充值 DeepSeek 账户后重试。'\n : upstreamMessage || `DeepSeek request failed: ${response.status}`;\n const error = new Error(friendlyMessage);\n error.statusCode = response.status;\n if (response.status === 402 || /insufficient\\s+balance/i.test(upstreamMessage)) {\n error.code = 'DEEPSEEK_PROVIDER_QUOTA';\n }\n throw error;\n }\n\n let buffer = '';\n let message = '';\n let usage = {\n promptTokens: 0,\n completionTokens: 0,\n totalTokens: 0,\n };\n const decoder = new TextDecoder();\n\n for await (const chunk of response.body) {\n buffer += decoder.decode(chunk, { stream: true });\n const lines = buffer.split(/\\r?\\n/);\n buffer = lines.pop() || '';\n\n for (const rawLine of lines) {\n const line = rawLine.trim();\n if (!line.startsWith('data:')) continue;\n const data = line.slice('data:'.length).trim();\n if (!data || data === '[DONE]') continue;\n\n let payload;\n try {\n payload = JSON.parse(data);\n } catch {\n continue;\n }\n\n const delta = payload?.choices?.[0]?.delta?.content || '';\n if (delta) {\n message += delta;\n callbacks.onToken?.(delta);\n }\n\n if (payload?.usage) {\n usage = {\n promptTokens: Number(payload.usage.prompt_tokens || usage.promptTokens || 0),\n completionTokens: Number(payload.usage.completion_tokens || usage.completionTokens || 0),\n totalTokens: Number(payload.usage.total_tokens || usage.totalTokens || 0),\n };\n }\n }\n }\n\n message = message.trim();\n if (!message) {\n const error = new Error('DeepSeek returned an empty response');\n error.statusCode = 502;\n throw error;\n }\n\n return { message, model, usage };\n}\n\nfunction createCodexDeepSeekRuntime() {\n if (!enableCodexSdkChat || !deepseekApiKey) {\n return {\n enabled: false,\n provider: 'deepseek-direct',\n reason: enableCodexSdkChat ? 'DeepSeek API key is not configured' : 'ENABLE_CODEX_SDK_CHAT is not true',\n };\n }\n\n return {\n enabled: true,\n provider: 'codex-sdk-deepseek',\n options: {\n apiKey: deepseekApiKey,\n baseUrl: deepseekBaseUrl,\n env: {\n ...process.env,\n DEEPSEEK_API_KEY: deepseekApiKey,\n HOME: codexHomeDir,\n CODEX_HOME: codexHomeDir,\n TMPDIR: codexTempDir,\n XDG_RUNTIME_DIR: codexRuntimeDir,\n XDG_CACHE_HOME: path.join(codexHomeDir, '.cache'),\n XDG_CONFIG_HOME: path.join(codexHomeDir, '.config'),\n XDG_DATA_HOME: path.join(codexHomeDir, '.local', 'share'),\n PATH: process.env.PATH || '/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin',\n },\n config: {\n model_provider: codexDeepSeekProviderId,\n model_providers: {\n [codexDeepSeekProviderId]: {\n name: 'DeepSeek Chat Completions',\n base_url: codexResponsesBaseUrl,\n env_key: 'DEEPSEEK_API_KEY',\n wire_api: 'responses',\n query_params: {},\n },\n },\n },\n },\n threadOptions: {\n model: deepseekModel,\n approvalPolicy: 'never',\n sandboxMode: 'read-only',\n skipGitRepoCheck: true,\n networkAccessEnabled: true,\n },\n };\n}\n\nfunction codexPromptFromMessages(messages) {\n return messages\n .map((message) => {\n const role = String(message.role || 'user').toUpperCase();\n return `${role}:\\n${message.content}`;\n })\n .join('\\n\\n');\n}\n\nfunction responseContentText(content) {\n if (typeof content === 'string') return content;\n if (!Array.isArray(content)) return '';\n return content\n .map((part) => {\n if (typeof part === 'string') return part;\n if (!part || typeof part !== 'object') return '';\n return String(part.text || part.input_text || part.output_text || '');\n })\n .filter(Boolean)\n .join('\\n');\n}\n\nfunction codexResponsesPrompt(body) {\n const input = Array.isArray(body?.input) ? body.input : [];\n const userItems = input.filter((item) => item?.role === 'user');\n const source = userItems[userItems.length - 1] || input[input.length - 1];\n const prompt = responseContentText(source?.content).trim();\n if (prompt) return prompt;\n if (typeof body?.input === 'string') return body.input.trim();\n return '';\n}\n\nfunction writeResponsesEvent(res, event, data) {\n res.write(`event: ${event}\\n`);\n res.write(`data: ${JSON.stringify(data)}\\n\\n`);\n}\n\nfunction responseUsage(usage = {}) {\n return {\n input_tokens: Number(usage.promptTokens || usage.input_tokens || 0),\n cached_input_tokens: 0,\n output_tokens: Number(usage.completionTokens || usage.output_tokens || 0),\n reasoning_output_tokens: 0,\n total_tokens: Number(usage.totalTokens || usage.total_tokens || 0),\n };\n}\n\nfunction createLibreChatAgentRuntime() {\n if (!enableLibreChatAgentChat || !libreChatAgentApiKey || !libreChatAgentId) {\n return {\n enabled: false,\n provider: 'librechat-agent',\n reason: !enableLibreChatAgentChat\n ? 'ENABLE_LIBRECHAT_AGENT_CHAT is not true'\n : !libreChatAgentApiKey\n ? 'LIBRECHAT_AGENT_API_KEY is not configured'\n : 'LIBRECHAT_AGENT_ID is not configured',\n };\n }\n\n return {\n enabled: true,\n provider: 'librechat-agent',\n model: libreChatAgentId,\n chatCompletionsUrl: `${libreChatAgentsBaseUrl}/chat/completions`,\n };\n}\n\nfunction libreChatUsage(usage = {}) {\n return {\n promptTokens: Number(usage.prompt_tokens || usage.input_tokens || usage.promptTokens || 0),\n completionTokens: Number(usage.completion_tokens || usage.output_tokens || usage.completionTokens || 0),\n totalTokens: Number(usage.total_tokens || usage.totalTokens || 0),\n };\n}\n\nasync function callLibreChatAgent(messages) {\n const runtime = createLibreChatAgentRuntime();\n if (!runtime.enabled) {\n const error = new Error(runtime.reason || 'LibreChat Agent chat is not enabled');\n error.statusCode = 503;\n throw error;\n }\n\n const response = await fetch(runtime.chatCompletionsUrl, {\n method: 'POST',\n headers: {\n Accept: 'application/json',\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${libreChatAgentApiKey}`,\n },\n body: JSON.stringify({\n model: runtime.model,\n messages,\n temperature: 0.4,\n stream: false,\n }),\n signal: AbortSignal.timeout(120_000),\n });\n\n const bodyText = await response.text();\n let payload;\n try {\n payload = bodyText ? JSON.parse(bodyText) : {};\n } catch {\n payload = { error: bodyText };\n }\n\n if (!response.ok) {\n const upstreamMessage = payload?.error?.message || payload?.message || bodyText || '';\n const error = new Error(upstreamMessage || `LibreChat Agent request failed: ${response.status}`);\n error.statusCode = response.status;\n throw error;\n }\n\n const message = payload?.choices?.[0]?.message?.content?.trim();\n if (!message) {\n const error = new Error('LibreChat Agent returned an empty response');\n error.statusCode = 502;\n throw error;\n }\n\n return {\n message,\n usage: libreChatUsage(payload?.usage),\n };\n}\n\nasync function callLibreChatAgentStream(messages, callbacks = {}) {\n const runtime = createLibreChatAgentRuntime();\n if (!runtime.enabled) {\n const error = new Error(runtime.reason || 'LibreChat Agent chat is not enabled');\n error.statusCode = 503;\n throw error;\n }\n\n const response = await fetch(runtime.chatCompletionsUrl, {\n method: 'POST',\n headers: {\n Accept: 'text/event-stream',\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${libreChatAgentApiKey}`,\n },\n body: JSON.stringify({\n model: runtime.model,\n messages,\n temperature: 0.4,\n stream: true,\n }),\n signal: callbacks.signal || AbortSignal.timeout(180_000),\n });\n\n if (!response.ok) {\n const bodyText = await response.text();\n let payload;\n try {\n payload = bodyText ? JSON.parse(bodyText) : {};\n } catch {\n payload = { error: bodyText };\n }\n const upstreamMessage = payload?.error?.message || payload?.message || bodyText || '';\n const error = new Error(upstreamMessage || `LibreChat Agent request failed: ${response.status}`);\n error.statusCode = response.status;\n throw error;\n }\n\n let buffer = '';\n let message = '';\n let usage = { promptTokens: 0, completionTokens: 0, totalTokens: 0 };\n const decoder = new TextDecoder();\n\n for await (const chunk of response.body) {\n buffer += decoder.decode(chunk, { stream: true });\n const lines = buffer.split(/\\r?\\n/);\n buffer = lines.pop() || '';\n\n for (const rawLine of lines) {\n const line = rawLine.trim();\n if (!line.startsWith('data:')) continue;\n const data = line.slice('data:'.length).trim();\n if (!data || data === '[DONE]') continue;\n\n let payload;\n try {\n payload = JSON.parse(data);\n } catch {\n continue;\n }\n\n const delta = payload?.choices?.[0]?.delta?.content || '';\n if (delta) {\n message += delta;\n callbacks.onToken?.(delta);\n }\n\n if (payload?.usage) {\n usage = libreChatUsage(payload.usage);\n }\n }\n }\n\n message = message.trim();\n if (!message) {\n const error = new Error('LibreChat Agent returned an empty response');\n error.statusCode = 502;\n throw error;\n }\n\n return { message, usage };\n}\n\nfunction isDeepSeekQuotaError(error) {\n if (error?.code === 'DEEPSEEK_PROVIDER_QUOTA') return true;\n if (error?.code === 'USER_TOKEN_QUOTA_EXCEEDED' || error?.code === 'AI_MEMBERSHIP_REQUIRED') return false;\n const message = String(error?.message || error || '');\n return /insufficient\\s+balance|balance/i.test(message);\n}\n\nfunction publicAiErrorMessage(error) {\n if (isDeepSeekQuotaError(error)) {\n return '大乘 AI 服务额度暂不可用,请联系管理员充值 DeepSeek 账户后重试。';\n }\n return error?.message || 'Internal Server Error';\n}\n\nfunction responsesPayload({\n responseId,\n itemId,\n status,\n model,\n text = '',\n usage = null,\n error = null,\n}) {\n return {\n id: responseId,\n object: 'response',\n status,\n model,\n output:\n status === 'completed'\n ? [\n {\n id: itemId,\n type: 'message',\n role: 'assistant',\n status: 'completed',\n content: [{ type: 'output_text', text }],\n },\n ]\n : [],\n ...(usage ? { usage: responseUsage(usage) } : {}),\n ...(error\n ? {\n error: {\n message: error.message || String(error),\n type: 'server_error',\n },\n }\n : {}),\n };\n}\n\nasync function callCodexSdkDeepSeek(messages, callbacks = {}) {\n const codexRuntime = createCodexDeepSeekRuntime();\n if (!codexRuntime.enabled) {\n const error = new Error(codexRuntime.reason || 'Codex SDK chat is not enabled');\n error.statusCode = 503;\n throw error;\n }\n\n const { Codex } = await import('@openai/codex-sdk');\n const codex = new Codex(codexRuntime.options);\n const thread = codex.startThread(codexRuntime.threadOptions);\n const prompt = codexPromptFromMessages(messages);\n\n if (callbacks.onToken || callbacks.onStep) {\n const { events } = await thread.runStreamed(prompt, { signal: callbacks.signal });\n let finalResponse = '';\n let usage = null;\n\n for await (const event of events) {\n if (event.type === 'item.completed') {\n const item = event.item;\n if (item.type === 'agent_message') {\n finalResponse = item.text || finalResponse;\n callbacks.onToken?.(item.text || '');\n } else if (item.type === 'todo_list') {\n callbacks.onStep?.({\n title: 'Codex SDK 执行计划',\n message: item.items.map((todo) => `${todo.completed ? '✓' : '•'} ${todo.text}`).join('\\n'),\n });\n } else if (item.type === 'command_execution') {\n callbacks.onStep?.({\n title: 'Codex SDK 执行命令',\n message: item.command,\n });\n } else if (item.type === 'mcp_tool_call') {\n callbacks.onStep?.({\n title: 'Codex SDK 调用工具',\n message: `${item.server}.${item.tool}`,\n });\n }\n } else if (event.type === 'turn.completed') {\n usage = event.usage;\n } else if (event.type === 'turn.failed') {\n throw new Error(event.error?.message || 'Codex SDK turn failed');\n } else if (event.type === 'error') {\n throw new Error(event.message || 'Codex SDK stream failed');\n }\n }\n\n if (!finalResponse.trim()) {\n const error = new Error('Codex SDK returned an empty response');\n error.statusCode = 502;\n throw error;\n }\n\n return {\n message: finalResponse.trim(),\n usage: {\n promptTokens: Number(usage?.input_tokens || 0),\n completionTokens: Number(usage?.output_tokens || 0),\n totalTokens: Number((usage?.input_tokens || 0) + (usage?.output_tokens || 0)),\n },\n };\n }\n\n const turn = await thread.run(prompt);\n if (!turn.finalResponse?.trim()) {\n const error = new Error('Codex SDK returned an empty response');\n error.statusCode = 502;\n throw error;\n }\n\n return {\n message: turn.finalResponse.trim(),\n usage: {\n promptTokens: Number(turn.usage?.input_tokens || 0),\n completionTokens: Number(turn.usage?.output_tokens || 0),\n totalTokens: Number((turn.usage?.input_tokens || 0) + (turn.usage?.output_tokens || 0)),\n },\n };\n}\n\nconst activeAgentRuns = new Map();\n\nfunction normalizeAgentMode(value) {\n const mode = safeUserText(value || 'dharma_guide');\n return /^[a-z][a-z0-9_:-]{0,48}$/i.test(mode) ? mode : 'dharma_guide';\n}\n\nfunction openClawSessionKey(user, conversationId) {\n const env = process.env.NODE_ENV === 'production' ? 'prod' : 'dev';\n return `fabushi:env:${env}:user:${user.userId}:conversation:${conversationId}`;\n}\n\nfunction createOpenClawRuntime() {\n if (!enableOpenClawAgentChat || !openClawGatewayToken || !openClawGatewayUrl) {\n return {\n enabled: false,\n provider: 'openclaw-gateway',\n reason: !enableOpenClawAgentChat\n ? 'ENABLE_OPENCLAW_AGENT_CHAT is not true'\n : !openClawGatewayToken\n ? 'OPENCLAW_GATEWAY_TOKEN is not configured'\n : 'OPENCLAW_GATEWAY_URL is not configured',\n };\n }\n\n return {\n enabled: true,\n provider: 'openclaw-gateway',\n model: openClawAgentId,\n runsUrl: openClawRunsEndpoint || `${openClawGatewayUrl}/v1/agents/${encodeURIComponent(openClawAgentId)}/runs`,\n };\n}\n\nfunction extractOpenClawText(payload) {\n if (!payload || typeof payload !== 'object') return '';\n const candidates = [\n payload.message,\n payload.content,\n payload.output_text,\n payload.finalResponse,\n payload.final_response,\n payload.result?.message,\n payload.result?.content,\n payload.output?.message,\n payload.output?.content,\n payload.choices?.[0]?.message?.content,\n ];\n for (const candidate of candidates) {\n if (typeof candidate === 'string' && candidate.trim()) return candidate.trim();\n }\n return '';\n}\n\nfunction extractOpenClawUsage(payload) {\n const usage = payload?.usage || payload?.result?.usage || {};\n return {\n promptTokens: Number(usage.input_tokens || usage.prompt_tokens || usage.promptTokens || 0),\n completionTokens: Number(usage.output_tokens || usage.completion_tokens || usage.completionTokens || 0),\n totalTokens: Number(usage.total_tokens || usage.totalTokens || 0),\n };\n}\n\nasync function callOpenClawAgent({ messages, user, conversationId, mode, signal }) {\n const runtime = createOpenClawRuntime();\n if (!runtime.enabled) {\n const error = new Error(runtime.reason || 'OpenClaw Agent chat is not enabled');\n error.statusCode = 503;\n throw error;\n }\n\n const sessionKey = openClawSessionKey(user, conversationId);\n const response = await fetch(runtime.runsUrl, {\n method: 'POST',\n headers: {\n Accept: 'application/json',\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${openClawGatewayToken}`,\n },\n body: JSON.stringify({\n agentId: openClawAgentId,\n sessionKey,\n mode,\n stream: false,\n input: { messages },\n messages,\n metadata: {\n app: 'fabushi',\n userId: user.userId,\n conversationId,\n toolPolicy: 'public-user-read-mostly',\n },\n }),\n signal: signal || AbortSignal.timeout(180_000),\n });\n\n const bodyText = await response.text();\n let payload;\n try {\n payload = bodyText ? JSON.parse(bodyText) : {};\n } catch {\n payload = { message: bodyText };\n }\n\n if (!response.ok) {\n const error = new Error(payload?.error?.message || payload?.message || `OpenClaw request failed: ${response.status}`);\n error.statusCode = response.status;\n throw error;\n }\n\n const message = extractOpenClawText(payload);\n if (!message) {\n const error = new Error('OpenClaw returned an empty response');\n error.statusCode = 502;\n throw error;\n }\n\n return {\n message,\n runId: payload.runId || payload.id || payload.result?.runId || null,\n provider: runtime.provider,\n model: runtime.model,\n usage: extractOpenClawUsage(payload),\n };\n}\n\nasync function runAgentModel({ messages, user, conversationId, mode, callbacks = {}, signal, maxCompletionTokens }) {\n const openClawRuntime = createOpenClawRuntime();\n if (openClawRuntime.enabled && !callbacks.onToken) {\n return await callOpenClawAgent({ messages, user, conversationId, mode, signal });\n }\n\n const codexRuntime = createCodexDeepSeekRuntime();\n const libreChatRuntime = createLibreChatAgentRuntime();\n let provider = libreChatRuntime.enabled\n ? libreChatRuntime.provider\n : codexRuntime.enabled\n ? codexRuntime.provider\n : 'deepseek';\n let model = libreChatRuntime.enabled ? libreChatRuntime.model : deepseekModel;\n\n try {\n if (libreChatRuntime.enabled) {\n const result = callbacks.onToken\n ? await callLibreChatAgentStream(messages, { ...callbacks, signal })\n : await callLibreChatAgent(messages);\n return { ...result, provider, model };\n }\n if (codexRuntime.enabled) {\n const result = await callCodexSdkDeepSeek(messages, { ...callbacks, signal });\n return { ...result, provider, model };\n }\n const result = callbacks.onToken\n ? await callDeepSeekStream(messages, { ...callbacks, signal, maxCompletionTokens })\n : await callDeepSeek(messages, { maxCompletionTokens });\n return { ...result, provider, model };\n } catch (error) {\n if (!codexRuntime.enabled || isDeepSeekQuotaError(error)) throw error;\n logger.warn({ error: String(error) }, 'Agent model failed, falling back to DeepSeek direct');\n const result = callbacks.onToken\n ? await callDeepSeekStream(messages, { ...callbacks, signal, maxCompletionTokens })\n : await callDeepSeek(messages, { maxCompletionTokens });\n return { ...result, provider: 'deepseek', model: deepseekModel };\n }\n}\n\nfunction updateAgentRunStatus(run, status, fields = {}) {\n statements.updateAgentRunStatus.run({\n id: run.id,\n userId: run.user_id || run.userId,\n status,\n openClawRunId: fields.openClawRunId ?? null,\n inputTokens: fields.inputTokens ?? null,\n outputTokens: fields.outputTokens ?? null,\n toolCallCount: fields.toolCallCount ?? null,\n completedAt: status === 'completed' || status === 'cancelled' ? nowIso() : null,\n failedAt: status === 'failed' ? nowIso() : null,\n errorCode: fields.errorCode ?? null,\n errorMessage: fields.errorMessage ?? null,\n });\n}\n\nfunction buildAgentMessages(conversationId) {\n const historyRows = statements.listMessages.all(conversationId).slice(-14);\n return [{ role: 'system', content: systemPrompt }, ...normalizeMessages(historyRows)];\n}\n\nfunction agentUsagePayload(usage, user, budget) {\n const promptTokens = usage.promptTokens || usage.inputTokens || 0;\n const completionTokens = usage.completionTokens || usage.outputTokens || 0;\n const totalTokens = usage.totalTokens || promptTokens + completionTokens;\n const latestUsage = usageFor(user.userId);\n return {\n inputTokens: promptTokens,\n outputTokens: completionTokens,\n totalTokens,\n monthlyLimit: budget.limit,\n remainingTokens: Math.max(0, budget.limit - latestUsage.used),\n };\n}\n\nfunction openAiContentToText(content) {\n if (typeof content === 'string') return content;\n if (Array.isArray(content)) {\n return content\n .map((part) => {\n if (typeof part === 'string') return part;\n if (!part || typeof part !== 'object') return '';\n return readText(part.text || part.content || part.value);\n })\n .filter(Boolean)\n .join('\\n');\n }\n return readText(content);\n}\n\nfunction normalizeOpenAiMessages(value) {\n if (!Array.isArray(value)) return [];\n const allowedRoles = new Set(['system', 'user', 'assistant', 'tool']);\n return value\n .map((item) => {\n if (!item || typeof item !== 'object') return null;\n const role = allowedRoles.has(item.role) ? item.role : 'user';\n const content = openAiContentToText(item.content);\n if (!content) return null;\n return { role, content };\n })\n .filter(Boolean);\n}\n\nfunction truncateForDeepSeekMessage(text, maxChars = 12_000) {\n const value = readText(text);\n if (value.length <= maxChars) return value;\n return `${value.slice(0, maxChars)}\\n\\n[内容已截断]`;\n}\n\nfunction truncateTailForDeepSeekMessage(text, maxChars = 2400) {\n const value = readText(text);\n if (value.length <= maxChars) return value;\n return `[前文已省略]\\n\\n${value.slice(-maxChars)}`;\n}\n\nfunction stripOpenClawPromptNoise(text) {\n const value = readText(text);\n if (!value) return '';\n\n const finalPromptMatch = value.match(/finalPromptText[\"'\\s::]+([\\s\\S]{1,4000})$/i);\n if (finalPromptMatch?.[1]) {\n return readText(finalPromptMatch[1].replace(/^[\"'`]+|[\"'`,}\\]]+$/g, ''));\n }\n\n const markers = [\n 'final prompt:',\n 'final user prompt:',\n 'user prompt:',\n '用户问题:',\n '用户消息:',\n '用户输入:',\n ];\n const lower = value.toLowerCase();\n for (const marker of markers) {\n const index = lower.lastIndexOf(marker.toLowerCase());\n if (index >= 0) {\n const tail = value.slice(index + marker.length).trim();\n if (tail) return tail;\n }\n }\n\n return value;\n}\n\nfunction compactOpenClawMessagesForDeepSeek(messages) {\n const conversationalMessages = messages\n .filter((message) => message.role === 'user' || message.role === 'assistant')\n .map((message) => ({\n role: message.role,\n content: truncateTailForDeepSeekMessage(stripOpenClawPromptNoise(message.content)),\n }))\n .filter((message) => message.content);\n\n const recent = conversationalMessages.slice(-6);\n while (recent.length > 0 && recent[0].role !== 'user') {\n recent.shift();\n }\n\n if (recent.length === 0) {\n const fallback = messages\n .slice()\n .reverse()\n .map((message) => truncateTailForDeepSeekMessage(stripOpenClawPromptNoise(message.content), 2400))\n .find(Boolean);\n if (!fallback) return messages;\n recent.push({ role: 'user', content: fallback });\n }\n\n return [\n {\n role: 'system',\n content: '你是大乘 App 的桌面 AI 助手。请直接回答用户问题,默认使用简洁、准确的中文。',\n },\n ...recent,\n ];\n}\n\nfunction isDeepSeekBlockedError(error) {\n return (\n Number(error?.statusCode) === 403 &&\n /blocked|被拦截|forbidden/i.test(String(error?.message || error || ''))\n );\n}\n\nfunction shouldRetryWithCompactedOpenClawMessages(error, messages, compactedMessages) {\n if (!isDeepSeekBlockedError(error)) return false;\n return JSON.stringify(messages) !== JSON.stringify(compactedMessages);\n}\n\nfunction usageWithFallback(usage, messages, text) {\n const promptTokens = Number(usage?.promptTokens || usage?.inputTokens || 0);\n const completionTokens = Number(usage?.completionTokens || usage?.outputTokens || 0);\n const totalTokens = Number(usage?.totalTokens || 0);\n if (totalTokens > 0) {\n return {\n promptTokens,\n completionTokens,\n totalTokens,\n };\n }\n const estimatedPrompt = promptTokens || estimateTokens(JSON.stringify(messages));\n const estimatedCompletion = completionTokens || estimateTokens(text);\n return {\n promptTokens: estimatedPrompt,\n completionTokens: estimatedCompletion,\n totalTokens: estimatedPrompt + estimatedCompletion,\n };\n}\n\nfunction openAiUsagePayload(usage, user, budget) {\n const promptTokens = Number(usage.promptTokens || 0);\n const completionTokens = Number(usage.completionTokens || 0);\n const totalTokens = Number(usage.totalTokens || promptTokens + completionTokens);\n const latestUsage = usageFor(user.userId);\n const remainingTokens = Math.max(0, budget.limit - latestUsage.used);\n return {\n prompt_tokens: promptTokens,\n completion_tokens: completionTokens,\n total_tokens: totalTokens,\n promptTokens,\n completionTokens,\n totalTokens,\n monthlyLimit: budget.limit,\n remainingTokens,\n };\n}\n\nfunction writeOpenAiSse(res, payload) {\n res.write(`data: ${JSON.stringify(payload)}\\n\\n`);\n}\n\napp.post(\n '/api/openclaw/deepseek/v1/chat/completions',\n asyncHandler(async (req, res) => {\n const user = await resolveUser(req, req.body || {});\n const incomingMessages = normalizeOpenAiMessages(req.body?.messages);\n if (incomingMessages.length === 0) {\n res.status(400).json({ error: { message: 'messages is required' } });\n return;\n }\n\n const messages = compactOpenClawMessagesForDeepSeek(incomingMessages);\n const promptEstimate = estimateTokens(JSON.stringify(messages));\n const estimated = promptEstimate + 600;\n const budget = enforceTokenBudget(user, estimated);\n const maxCompletionTokens = Math.min(\n completionBudgetFor(budget, promptEstimate),\n normalizeMaxCompletionTokens(req.body?.max_tokens),\n );\n const model = normalizeDeepSeekModelName(req.body?.model);\n const wantsStream =\n req.body?.stream === true ||\n String(req.get('accept') || '').includes('text/event-stream');\n const id = `chatcmpl_${crypto.randomUUID().replaceAll('-', '')}`;\n const created = Math.floor(Date.now() / 1000);\n const compactedMessages = messages;\n\n if (!wantsStream) {\n let result;\n let usageMessages = messages;\n try {\n result = await callDeepSeek(messages, { model, maxCompletionTokens });\n } catch (error) {\n if (!shouldRetryWithCompactedOpenClawMessages(error, messages, compactedMessages)) {\n throw error;\n }\n logger.warn(\n {\n statusCode: error?.statusCode,\n messageCount: messages.length,\n compactedMessageCount: compactedMessages.length,\n },\n 'OpenClaw DeepSeek proxy retrying with compacted messages',\n );\n usageMessages = compactedMessages;\n result = await callDeepSeek(compactedMessages, { model, maxCompletionTokens });\n }\n const usage = usageWithFallback(result.usage, usageMessages, result.message);\n recordUsage(user.userId, usage.totalTokens);\n res.json({\n id,\n object: 'chat.completion',\n created,\n model: result.model || model,\n choices: [\n {\n index: 0,\n message: { role: 'assistant', content: result.message },\n finish_reason: 'stop',\n },\n ],\n usage: openAiUsagePayload(usage, user, budget),\n });\n return;\n }\n\n const abortController = new AbortController();\n req.on('close', () => abortController.abort());\n\n res.status(200);\n res.set({\n 'Content-Type': 'text/event-stream; charset=utf-8',\n 'Cache-Control': 'no-cache, no-transform',\n Connection: 'keep-alive',\n 'X-Accel-Buffering': 'no',\n });\n res.flushHeaders?.();\n\n const baseChunk = {\n id,\n object: 'chat.completion.chunk',\n created,\n model,\n system_fingerprint: 'fabushi-openclaw-deepseek-proxy',\n };\n writeOpenAiSse(res, {\n ...baseChunk,\n choices: [{ index: 0, delta: { role: 'assistant' }, finish_reason: null }],\n });\n\n let text = '';\n let usageMessages = messages;\n try {\n let result;\n try {\n result = await callDeepSeekStream(messages, {\n model,\n maxCompletionTokens,\n signal: abortController.signal,\n onToken: (delta) => {\n text += delta;\n writeOpenAiSse(res, {\n ...baseChunk,\n choices: [{ index: 0, delta: { content: delta }, finish_reason: null }],\n });\n },\n });\n } catch (error) {\n if (!shouldRetryWithCompactedOpenClawMessages(error, messages, compactedMessages)) {\n throw error;\n }\n logger.warn(\n {\n statusCode: error?.statusCode,\n messageCount: messages.length,\n compactedMessageCount: compactedMessages.length,\n },\n 'OpenClaw DeepSeek proxy stream retrying with compacted messages',\n );\n text = '';\n usageMessages = compactedMessages;\n result = await callDeepSeekStream(compactedMessages, {\n model,\n maxCompletionTokens,\n signal: abortController.signal,\n onToken: (delta) => {\n text += delta;\n writeOpenAiSse(res, {\n ...baseChunk,\n choices: [{ index: 0, delta: { content: delta }, finish_reason: null }],\n });\n },\n });\n }\n text = result.message || text;\n const usage = usageWithFallback(result.usage, usageMessages, text);\n recordUsage(user.userId, usage.totalTokens);\n writeOpenAiSse(res, {\n ...baseChunk,\n model: result.model || model,\n choices: [{ index: 0, delta: {}, finish_reason: 'stop' }],\n });\n writeOpenAiSse(res, {\n ...baseChunk,\n model: result.model || model,\n choices: [],\n usage: openAiUsagePayload(usage, user, budget),\n });\n res.write('data: [DONE]\\n\\n');\n } catch (error) {\n logger.error(\n { error: String(error), code: error?.code, statusCode: error?.statusCode },\n 'OpenClaw DeepSeek proxy stream failed',\n );\n writeOpenAiSse(res, {\n ...baseChunk,\n choices: [],\n error: {\n message: publicAiErrorMessage(error),\n code: error?.code || 'OPENCLAW_DEEPSEEK_PROXY_ERROR',\n },\n });\n res.write('data: [DONE]\\n\\n');\n } finally {\n res.end();\n }\n }),\n);\n\napp.post(\n '/codex-deepseek/v1/responses',\n asyncHandler(async (req, res) => {\n const bearer = bearerToken(req);\n if (!deepseekApiKey || bearer !== deepseekApiKey) {\n res.status(401).json({\n error: { message: 'Unauthorized Codex DeepSeek adapter request' },\n });\n return;\n }\n\n const prompt = codexResponsesPrompt(req.body);\n if (!prompt) {\n res.status(400).json({ error: { message: 'Responses input is required' } });\n return;\n }\n\n const model = String(req.body?.model || deepseekModel);\n const responseId = `resp_${crypto.randomUUID().replaceAll('-', '')}`;\n const itemId = `msg_${crypto.randomUUID().replaceAll('-', '')}`;\n const wantsStream =\n req.body?.stream !== false ||\n String(req.get('accept') || '').includes('text/event-stream');\n const messages = [{ role: 'user', content: prompt }];\n\n if (!wantsStream) {\n const result = await callDeepSeek(messages);\n res.json(\n responsesPayload({\n responseId,\n itemId,\n status: 'completed',\n model,\n text: result.message,\n usage: result.usage,\n }),\n );\n return;\n }\n\n res.status(200);\n res.set({\n 'Content-Type': 'text/event-stream; charset=utf-8',\n 'Cache-Control': 'no-cache, no-transform',\n Connection: 'keep-alive',\n 'X-Accel-Buffering': 'no',\n });\n res.flushHeaders?.();\n\n writeResponsesEvent(res, 'response.created', {\n type: 'response.created',\n response: responsesPayload({\n responseId,\n itemId,\n status: 'in_progress',\n model,\n }),\n });\n writeResponsesEvent(res, 'response.output_item.added', {\n type: 'response.output_item.added',\n output_index: 0,\n item: {\n id: itemId,\n type: 'message',\n role: 'assistant',\n status: 'in_progress',\n content: [],\n },\n });\n writeResponsesEvent(res, 'response.content_part.added', {\n type: 'response.content_part.added',\n item_id: itemId,\n output_index: 0,\n content_index: 0,\n part: { type: 'output_text', text: '' },\n });\n\n let text = '';\n try {\n const result = await callDeepSeekStream(messages, {\n onToken: (delta) => {\n text += delta;\n writeResponsesEvent(res, 'response.output_text.delta', {\n type: 'response.output_text.delta',\n item_id: itemId,\n output_index: 0,\n content_index: 0,\n delta,\n });\n },\n });\n text = result.message || text;\n writeResponsesEvent(res, 'response.output_text.done', {\n type: 'response.output_text.done',\n item_id: itemId,\n output_index: 0,\n content_index: 0,\n text,\n });\n writeResponsesEvent(res, 'response.content_part.done', {\n type: 'response.content_part.done',\n item_id: itemId,\n output_index: 0,\n content_index: 0,\n part: { type: 'output_text', text },\n });\n writeResponsesEvent(res, 'response.output_item.done', {\n type: 'response.output_item.done',\n output_index: 0,\n item: {\n id: itemId,\n type: 'message',\n role: 'assistant',\n status: 'completed',\n content: [{ type: 'output_text', text }],\n },\n });\n writeResponsesEvent(res, 'response.completed', {\n type: 'response.completed',\n response: responsesPayload({\n responseId,\n itemId,\n status: 'completed',\n model,\n text,\n usage: result.usage,\n }),\n });\n res.end();\n } catch (error) {\n writeResponsesEvent(res, 'response.failed', {\n type: 'response.failed',\n response: responsesPayload({\n responseId,\n itemId,\n status: 'failed',\n model,\n text,\n error,\n }),\n });\n res.end();\n }\n }),\n);\n\napp.use(\n '/api/openclaw/runtime/files',\n express.static(openClawRuntimeUpdatesDir, {\n fallthrough: false,\n immutable: true,\n maxAge: '7d',\n setHeaders(res) {\n res.setHeader('X-Content-Type-Options', 'nosniff');\n },\n }),\n);\n\napp.get(\n '/api/openclaw/runtime/manifest',\n asyncHandler(async (req, res) => {\n const platform = readText(req.query.platform);\n const currentVersion = readText(req.query.currentVersion);\n const appVersion = readText(req.query.appVersion);\n\n let manifest;\n try {\n manifest = await readJsonFile(openClawRuntimeManifestPath);\n } catch (error) {\n logger.info(\n { manifestPath: openClawRuntimeManifestPath, error: String(error) },\n 'OpenClaw runtime manifest unavailable',\n );\n return jsonResponse(res, 200, {\n success: true,\n schema: 1,\n updateAvailable: false,\n reason: 'manifest-unavailable',\n platform,\n currentVersion,\n appVersion,\n });\n }\n\n const latest = manifest.latest && typeof manifest.latest === 'object'\n ? manifest.latest\n : manifest;\n const platforms = latest.platforms && typeof latest.platforms === 'object'\n ? latest.platforms\n : {};\n const platformRelease = platform ? platforms[platform] : null;\n const version = readText(latest.version || manifest.version);\n\n if (!platform || !platformRelease || typeof platformRelease !== 'object') {\n return jsonResponse(res, 200, {\n success: true,\n schema: Number(manifest.schema || 1),\n channel: readText(manifest.channel || latest.channel || 'stable'),\n updateAvailable: false,\n reason: platform ? 'platform-unavailable' : 'platform-required',\n platform,\n currentVersion,\n appVersion,\n latestVersion: version,\n });\n }\n\n if (platform.startsWith('macos-') && !openClawRuntimeEnableMacosUpdates) {\n return jsonResponse(res, 200, {\n success: true,\n schema: Number(manifest.schema || 1),\n channel: readText(manifest.channel || latest.channel || 'stable'),\n updateAvailable: false,\n reason: 'macos-runtime-updates-disabled',\n platform,\n currentVersion,\n appVersion,\n latestVersion: version,\n });\n }\n\n const downloadUrl = absolutizeOpenClawRuntimeUrl(\n req,\n platformRelease.archiveUrl || platformRelease.url || platformRelease.fileName,\n );\n const release = {\n version,\n channel: readText(manifest.channel || latest.channel || 'stable'),\n minAppVersion: readText(latest.minAppVersion || platformRelease.minAppVersion),\n notes: readText(latest.notes || platformRelease.notes),\n nodeExecutable: readText(\n platformRelease.nodeExecutable || latest.nodeExecutable || 'node/bin/node',\n ),\n cliEntrypoint: readText(\n platformRelease.cliEntrypoint || latest.cliEntrypoint || 'openclaw/openclaw.mjs',\n ),\n defaultPort: Number(latest.defaultPort || manifest.defaultPort || 18789),\n defaultModel: readText(latest.defaultModel || manifest.defaultModel || 'openclaw/default'),\n defaultModelOverride: readText(latest.defaultModelOverride || manifest.defaultModelOverride),\n gatewayArgs: Array.isArray(latest.gatewayArgs)\n ? latest.gatewayArgs.map((item) => String(item))\n : ['gateway', '--port', '{port}', '--force'],\n archiveUrl: downloadUrl,\n sha256: readText(platformRelease.sha256),\n size: Number(platformRelease.size || 0),\n };\n\n jsonResponse(res, 200, {\n success: true,\n schema: Number(manifest.schema || 1),\n updateAvailable: Boolean(version && downloadUrl && version !== currentVersion),\n platform,\n currentVersion,\n appVersion,\n latest: release,\n generatedAt: readText(manifest.generatedAt || latest.generatedAt),\n });\n }),\n);\n\napp.get('/health', (_req, res) => {\n const codexRuntime = createCodexDeepSeekRuntime();\n const libreChatRuntime = createLibreChatAgentRuntime();\n const openClawRuntime = createOpenClawRuntime();\n jsonResponse(res, 200, {\n status: 'ok',\n service: 'dacheng-ai-backend',\n provider: libreChatRuntime.enabled ? libreChatRuntime.provider : 'deepseek',\n model: libreChatRuntime.enabled ? libreChatRuntime.model : deepseekModel,\n libreChatAgentChatEnabled: libreChatRuntime.enabled,\n libreChatAgentProvider: libreChatRuntime.provider,\n libreChatAgentReason: libreChatRuntime.enabled ? undefined : libreChatRuntime.reason,\n codexSdkAvailable: true,\n codexSdkChatEnabled: codexRuntime.enabled,\n codexSdkProvider: codexRuntime.provider,\n openClawAgentChatEnabled: openClawRuntime.enabled,\n openClawAgentProvider: openClawRuntime.provider,\n openClawAgentReason: openClawRuntime.enabled ? undefined : openClawRuntime.reason,\n requireMemberForAi,\n memberMonthlyLimit,\n freeMonthlyLimit,\n deepseekMaxCompletionTokens,\n openClawRuntimeUpdatesDir,\n openClawRuntimeManifestConfigured: fs.existsSync(openClawRuntimeManifestPath),\n cbetaApiRoot,\n timestamp: nowIso(),\n });\n});\n\napp.get(\n '/api/ai/quota',\n asyncHandler(async (req, res) => {\n const user = await resolveUser(req, { username: req.query.username });\n const budget = enforceTokenBudget(user, 0);\n const usage = usageFor(user.userId);\n jsonResponse(res, 200, {\n success: true,\n isMember: user.isMember,\n monthlyLimit: budget.limit,\n usedTokens: usage.used,\n remainingTokens: Math.max(0, budget.limit - usage.used),\n month: usage.month,\n });\n }),\n);\n\napp.post(\n '/api/agent/chat',\n asyncHandler(async (req, res) => {\n const message = safeUserText(req.body?.message);\n if (!message) {\n return jsonResponse(res, 400, { success: false, message: 'message is required' });\n }\n\n const user = await resolveUser(req, req.body || {});\n const promptEstimate = estimateTokens(message);\n const estimated = promptEstimate + 600;\n const budget = enforceTokenBudget(user, estimated);\n const maxCompletionTokens = completionBudgetFor(budget, promptEstimate);\n const createdAt = nowIso();\n const conversationId = safeUserText(req.body?.conversationId) || crypto.randomUUID();\n const mode = normalizeAgentMode(req.body?.mode);\n const stream = req.body?.stream === true;\n let conversation = statements.getConversation.get(conversationId, user.userId);\n const isNewConversation = !conversation;\n\n if (isNewConversation) {\n conversation = { id: conversationId, user_id: user.userId, title: titleFromMessage(message) };\n statements.insertConversation.run({\n id: conversationId,\n userId: user.userId,\n title: conversation.title,\n provider: 'agent',\n model: openClawAgentId,\n createdAt,\n updatedAt: createdAt,\n });\n }\n\n const messageId = safeUserText(req.body?.messageId) || crypto.randomUUID();\n statements.insertMessage.run({\n id: messageId,\n conversationId,\n role: 'user',\n content: message,\n promptTokens: 0,\n completionTokens: 0,\n totalTokens: 0,\n createdAt,\n });\n\n const runId = `run_${crypto.randomUUID().replaceAll('-', '')}`;\n const sessionKey = openClawSessionKey(user, conversationId);\n const openClawRuntime = createOpenClawRuntime();\n statements.insertAgentRun.run({\n id: runId,\n userId: user.userId,\n conversationId,\n messageId,\n openClawRunId: null,\n openClawSessionKey: sessionKey,\n status: stream ? 'queued' : 'running',\n mode,\n provider: openClawRuntime.enabled ? openClawRuntime.provider : 'dacheng-ai-fallback',\n model: openClawRuntime.enabled ? openClawAgentId : deepseekModel,\n startedAt: createdAt,\n });\n\n if (stream) {\n return jsonResponse(res, 200, {\n success: true,\n runId,\n conversationId,\n streamUrl: `/api/agent/runs/${runId}/events`,\n });\n }\n\n const controller = new AbortController();\n activeAgentRuns.set(runId, controller);\n try {\n const modelMessages = buildAgentMessages(conversationId);\n const aiResult = await runAgentModel({\n messages: modelMessages,\n user,\n conversationId,\n mode,\n signal: controller.signal,\n maxCompletionTokens,\n });\n const usage = {\n promptTokens: aiResult.usage?.promptTokens || estimateTokens(JSON.stringify(modelMessages)),\n completionTokens: aiResult.usage?.completionTokens || estimateTokens(aiResult.message),\n totalTokens:\n aiResult.usage?.totalTokens ||\n estimateTokens(JSON.stringify(modelMessages)) + estimateTokens(aiResult.message),\n };\n recordUsage(user.userId, usage.totalTokens);\n const answeredAt = nowIso();\n const assistantMessageId = crypto.randomUUID();\n statements.insertMessage.run({\n id: assistantMessageId,\n conversationId,\n role: 'assistant',\n content: aiResult.message,\n promptTokens: usage.promptTokens,\n completionTokens: usage.completionTokens,\n totalTokens: usage.totalTokens,\n createdAt: answeredAt,\n });\n statements.updateConversation.run({\n id: conversationId,\n userId: user.userId,\n title: isNewConversation ? titleFromMessage(message) : conversation.title,\n updatedAt: answeredAt,\n });\n updateAgentRunStatus({ id: runId, user_id: user.userId }, 'completed', {\n openClawRunId: aiResult.runId,\n inputTokens: usage.promptTokens,\n outputTokens: usage.completionTokens,\n });\n return jsonResponse(res, 200, {\n success: true,\n runId,\n conversationId,\n messageId: assistantMessageId,\n provider: aiResult.provider,\n model: aiResult.model,\n message: aiResult.message,\n usage: agentUsagePayload(usage, user, budget),\n });\n } catch (error) {\n const status = error.name === 'AbortError' ? 'cancelled' : 'failed';\n updateAgentRunStatus({ id: runId, user_id: user.userId }, status, {\n errorCode: error.name || 'AGENT_RUN_FAILED',\n errorMessage: publicAiErrorMessage(error),\n });\n if (status === 'cancelled') {\n return jsonResponse(res, 499, { success: false, runId, status: 'cancelled' });\n }\n throw error;\n } finally {\n activeAgentRuns.delete(runId);\n }\n }),\n);\n\napp.get(\n '/api/agent/runs/:runId/events',\n asyncHandler(async (req, res) => {\n const user = await resolveUser(req, {});\n const run = statements.getAgentRun.get(req.params.runId, user.userId);\n if (!run) {\n return jsonResponse(res, 404, { success: false, message: 'run not found' });\n }\n if (run.status === 'completed' || run.status === 'failed' || run.status === 'cancelled') {\n return jsonResponse(res, 409, { success: false, message: `run is already ${run.status}` });\n }\n\n sseHeaders(res);\n const controller = new AbortController();\n activeAgentRuns.set(run.id, controller);\n updateAgentRunStatus(run, 'running');\n writeSse(res, 'run.started', { runId: run.id, conversationId: run.conversation_id });\n\n try {\n let streamedText = '';\n const modelMessages = buildAgentMessages(run.conversation_id);\n const promptEstimate = estimateTokens(JSON.stringify(modelMessages));\n const estimated = promptEstimate + 600;\n const budget = enforceTokenBudget(user, estimated);\n const maxCompletionTokens = completionBudgetFor(budget, promptEstimate);\n const aiResult = await runAgentModel({\n messages: modelMessages,\n user,\n conversationId: run.conversation_id,\n mode: run.mode,\n signal: controller.signal,\n maxCompletionTokens,\n callbacks: {\n onStep: (step) => writeSse(res, 'tool.call.completed', {\n tool: 'agent.step',\n displayName: step.title || 'Agent step',\n summary: step.message || '',\n }),\n onToken: (delta) => {\n streamedText += delta;\n writeSse(res, 'assistant.delta', { text: delta });\n },\n },\n });\n const message = aiResult.message || streamedText.trim();\n const usage = {\n promptTokens: aiResult.usage?.promptTokens || estimateTokens(JSON.stringify(modelMessages)),\n completionTokens: aiResult.usage?.completionTokens || estimateTokens(message),\n totalTokens:\n aiResult.usage?.totalTokens || estimateTokens(JSON.stringify(modelMessages)) + estimateTokens(message),\n };\n recordUsage(user.userId, usage.totalTokens);\n const assistantMessageId = crypto.randomUUID();\n const answeredAt = nowIso();\n statements.insertMessage.run({\n id: assistantMessageId,\n conversationId: run.conversation_id,\n role: 'assistant',\n content: message,\n promptTokens: usage.promptTokens,\n completionTokens: usage.completionTokens,\n totalTokens: usage.totalTokens,\n createdAt: answeredAt,\n });\n statements.updateConversation.run({\n id: run.conversation_id,\n userId: user.userId,\n title: titleFromMessage(message),\n updatedAt: answeredAt,\n });\n updateAgentRunStatus(run, 'completed', {\n openClawRunId: aiResult.runId,\n inputTokens: usage.promptTokens,\n outputTokens: usage.completionTokens,\n });\n writeSse(res, 'assistant.message', { messageId: assistantMessageId, content: message });\n writeSse(res, 'run.completed', { usage: agentUsagePayload(usage, user, budget) });\n res.end();\n } catch (error) {\n const status = error.name === 'AbortError' ? 'cancelled' : 'failed';\n updateAgentRunStatus(run, status, {\n errorCode: error.name || 'AGENT_RUN_FAILED',\n errorMessage: publicAiErrorMessage(error),\n });\n writeSse(res, status === 'cancelled' ? 'run.cancelled' : 'run.failed', {\n runId: run.id,\n message: publicAiErrorMessage(error),\n });\n res.end();\n } finally {\n activeAgentRuns.delete(run.id);\n }\n }),\n);\n\napp.post(\n '/api/agent/runs/:runId/cancel',\n asyncHandler(async (req, res) => {\n const user = await resolveUser(req, req.body || {});\n const run = statements.getAgentRun.get(req.params.runId, user.userId);\n if (!run) {\n return jsonResponse(res, 404, { success: false, message: 'run not found' });\n }\n const controller = activeAgentRuns.get(run.id);\n if (controller) controller.abort();\n updateAgentRunStatus(run, 'cancelled');\n return jsonResponse(res, 200, { success: true, runId: run.id, status: 'cancelled' });\n }),\n);\n\napp.post(\n '/api/agent/messages/:messageId/feedback',\n asyncHandler(async (req, res) => {\n const user = await resolveUser(req, req.body || {});\n const rating = safeUserText(req.body?.rating);\n if (!['up', 'down'].includes(rating)) {\n return jsonResponse(res, 400, { success: false, message: 'rating must be up or down' });\n }\n statements.insertAgentFeedback.run({\n id: crypto.randomUUID(),\n messageId: req.params.messageId,\n userId: user.userId,\n rating,\n reason: safeUserText(req.body?.reason),\n comment: safeUserText(req.body?.comment),\n createdAt: nowIso(),\n });\n return jsonResponse(res, 200, { success: true });\n }),\n);\n\napp.post(\n '/api/ai/chat',\n asyncHandler(async (req, res) => {\n const message = safeUserText(req.body?.message);\n if (!message) {\n return jsonResponse(res, 400, { success: false, message: 'message is required' });\n }\n\n const user = await resolveUser(req, req.body || {});\n const promptEstimate = estimateTokens(message);\n const estimated = promptEstimate + 600;\n const budget = enforceTokenBudget(user, estimated);\n const maxCompletionTokens = completionBudgetFor(budget, promptEstimate);\n const createdAt = nowIso();\n const conversationId = safeUserText(req.body?.conversationId) || crypto.randomUUID();\n let conversation = statements.getConversation.get(conversationId, user.userId);\n const isNewConversation = !conversation;\n\n if (isNewConversation) {\n conversation = {\n id: conversationId,\n user_id: user.userId,\n title: titleFromMessage(message),\n };\n statements.insertConversation.run({\n id: conversationId,\n userId: user.userId,\n title: conversation.title,\n provider: 'deepseek',\n model: deepseekModel,\n createdAt,\n updatedAt: createdAt,\n });\n }\n\n statements.insertMessage.run({\n id: crypto.randomUUID(),\n conversationId,\n role: 'user',\n content: message,\n promptTokens: 0,\n completionTokens: 0,\n totalTokens: 0,\n createdAt,\n });\n\n const libreChatRuntime = createLibreChatAgentRuntime();\n const skillResult = await runResourceFinderSkill({ message, user });\n const skillContext = resourceContextMessage(skillResult);\n const historyRows = statements.listMessages.all(conversationId).slice(-14);\n const modelMessages = [\n { role: 'system', content: systemPrompt },\n ...(skillContext ? [{ role: 'system', content: skillContext }] : []),\n ...normalizeMessages(historyRows),\n ];\n\n const openClawRuntime = createOpenClawRuntime();\n const codexRuntime = createCodexDeepSeekRuntime();\n let provider = openClawRuntime.enabled\n ? openClawRuntime.provider\n : libreChatRuntime.enabled\n ? libreChatRuntime.provider\n : 'deepseek';\n let responseModel = openClawRuntime.enabled\n ? openClawRuntime.model\n : libreChatRuntime.enabled\n ? libreChatRuntime.model\n : deepseekModel;\n let aiResult;\n try {\n if (openClawRuntime.enabled) {\n aiResult = await callOpenClawAgent({\n messages: modelMessages,\n user,\n conversationId,\n mode: normalizeAgentMode(req.body?.mode),\n });\n provider = aiResult.provider || provider;\n responseModel = aiResult.model || responseModel;\n } else if (libreChatRuntime.enabled) {\n aiResult = await callLibreChatAgent(modelMessages);\n } else if (codexRuntime.enabled) {\n provider = codexRuntime.provider;\n aiResult = await callCodexSdkDeepSeek(modelMessages);\n } else {\n aiResult = await callDeepSeek(modelMessages, { maxCompletionTokens });\n }\n } catch (error) {\n if (libreChatRuntime.enabled) {\n logger.warn({ error: String(error) }, 'LibreChat Agent chat failed, falling back to Codex SDK or DeepSeek direct');\n provider = codexRuntime.enabled ? codexRuntime.provider : 'deepseek';\n responseModel = deepseekModel;\n if (codexRuntime.enabled) {\n try {\n aiResult = await callCodexSdkDeepSeek(modelMessages);\n } catch (codexError) {\n if (isDeepSeekQuotaError(codexError)) throw codexError;\n logger.warn({ error: String(codexError) }, 'Codex SDK chat failed, falling back to DeepSeek direct');\n provider = 'deepseek';\n aiResult = await callDeepSeek(modelMessages, { maxCompletionTokens });\n }\n } else {\n aiResult = await callDeepSeek(modelMessages, { maxCompletionTokens });\n }\n } else {\n if (!codexRuntime.enabled || isDeepSeekQuotaError(error)) throw error;\n logger.warn({ error: String(error) }, 'Codex SDK chat failed, falling back to DeepSeek direct');\n provider = 'deepseek';\n aiResult = await callDeepSeek(modelMessages, { maxCompletionTokens });\n }\n }\n const usage = {\n promptTokens: aiResult.usage.promptTokens || estimateTokens(JSON.stringify(modelMessages)),\n completionTokens: aiResult.usage.completionTokens || estimateTokens(aiResult.message),\n totalTokens:\n aiResult.usage.totalTokens ||\n estimateTokens(JSON.stringify(modelMessages)) + estimateTokens(aiResult.message),\n };\n recordUsage(user.userId, usage.totalTokens);\n\n const answeredAt = nowIso();\n statements.insertMessage.run({\n id: crypto.randomUUID(),\n conversationId,\n role: 'assistant',\n content: aiResult.message,\n promptTokens: usage.promptTokens,\n completionTokens: usage.completionTokens,\n totalTokens: usage.totalTokens,\n createdAt: answeredAt,\n });\n statements.updateConversation.run({\n id: conversationId,\n userId: user.userId,\n title: isNewConversation ? titleFromMessage(message) : conversation.title,\n updatedAt: answeredAt,\n });\n\n const latestUsage = usageFor(user.userId);\n const monthlyLimit = budget.limit;\n jsonResponse(res, 200, {\n success: true,\n conversationId,\n provider,\n model: responseModel,\n message: aiResult.message,\n usage: {\n ...usage,\n monthlyLimit,\n remainingTokens: Math.max(0, monthlyLimit - latestUsage.used),\n },\n });\n }),\n);\n\napp.post('/api/ai/chat/stream', async (req, res) => {\n sseHeaders(res);\n\n try {\n const message = safeUserText(req.body?.message);\n if (!message) {\n writeSse(res, 'error', { message: 'message is required' });\n res.end();\n return;\n }\n\n const user = await resolveUser(req, req.body || {});\n const promptEstimate = estimateTokens(message);\n const estimated = promptEstimate + 600;\n const budget = enforceTokenBudget(user, estimated);\n const maxCompletionTokens = completionBudgetFor(budget, promptEstimate);\n const createdAt = nowIso();\n const conversationId = safeUserText(req.body?.conversationId) || crypto.randomUUID();\n let conversation = statements.getConversation.get(conversationId, user.userId);\n const isNewConversation = !conversation;\n\n if (isNewConversation) {\n conversation = {\n id: conversationId,\n user_id: user.userId,\n title: titleFromMessage(message),\n };\n statements.insertConversation.run({\n id: conversationId,\n userId: user.userId,\n title: conversation.title,\n provider: 'deepseek',\n model: deepseekModel,\n createdAt,\n updatedAt: createdAt,\n });\n }\n\n statements.insertMessage.run({\n id: crypto.randomUUID(),\n conversationId,\n role: 'user',\n content: message,\n promptTokens: 0,\n completionTokens: 0,\n totalTokens: 0,\n createdAt,\n });\n\n const codexRuntime = createCodexDeepSeekRuntime();\n const libreChatRuntime = createLibreChatAgentRuntime();\n const initialOpenClawRuntime = createOpenClawRuntime();\n let provider = initialOpenClawRuntime.enabled\n ? initialOpenClawRuntime.provider\n : libreChatRuntime.enabled\n ? libreChatRuntime.provider\n : codexRuntime.enabled\n ? codexRuntime.provider\n : 'deepseek';\n let responseModel = initialOpenClawRuntime.enabled\n ? initialOpenClawRuntime.model\n : libreChatRuntime.enabled\n ? libreChatRuntime.model\n : deepseekModel;\n writeSse(res, 'meta', {\n conversationId,\n provider,\n model: responseModel,\n });\n const skillResult = await runResourceFinderSkill({\n message,\n user,\n onStep: (step) => writeSse(res, 'step', step),\n });\n const skillContext = resourceContextMessage(skillResult);\n const historyRows = statements.listMessages.all(conversationId).slice(-14);\n const modelMessages = [\n { role: 'system', content: systemPrompt },\n ...(skillContext ? [{ role: 'system', content: skillContext }] : []),\n ...normalizeMessages(historyRows),\n ];\n\n let aiResult;\n const openClawRuntime = initialOpenClawRuntime;\n try {\n if (openClawRuntime.enabled) {\n aiResult = await callOpenClawAgent({\n messages: modelMessages,\n user,\n conversationId,\n mode: normalizeAgentMode(req.body?.mode),\n });\n provider = aiResult.provider || provider;\n responseModel = aiResult.model || responseModel;\n writeSse(res, 'delta', { text: aiResult.message });\n } else if (libreChatRuntime.enabled) {\n aiResult = await callLibreChatAgentStream(modelMessages, {\n onToken: (token) => writeSse(res, 'delta', { text: token }),\n });\n } else if (codexRuntime.enabled) {\n aiResult = await callCodexSdkDeepSeek(modelMessages, {\n onToken: (token) => writeSse(res, 'delta', { text: token }),\n onStep: (step) => writeSse(res, 'step', step),\n });\n } else {\n aiResult = await callDeepSeekStream(modelMessages, {\n maxCompletionTokens,\n onToken: (token) => writeSse(res, 'delta', { text: token }),\n });\n }\n } catch (error) {\n if (libreChatRuntime.enabled) {\n logger.warn({ error: String(error) }, 'LibreChat Agent streaming chat failed, falling back to Codex SDK or DeepSeek direct');\n provider = codexRuntime.enabled ? codexRuntime.provider : 'deepseek';\n responseModel = deepseekModel;\n writeSse(res, 'meta', {\n conversationId,\n provider,\n model: responseModel,\n });\n if (codexRuntime.enabled) {\n try {\n aiResult = await callCodexSdkDeepSeek(modelMessages, {\n onToken: (token) => writeSse(res, 'delta', { text: token }),\n onStep: (step) => writeSse(res, 'step', step),\n });\n } catch (codexError) {\n if (isDeepSeekQuotaError(codexError)) throw codexError;\n logger.warn({ error: String(codexError) }, 'Codex SDK streaming chat failed, falling back to DeepSeek direct');\n provider = 'deepseek';\n responseModel = deepseekModel;\n writeSse(res, 'meta', {\n conversationId,\n provider,\n model: responseModel,\n });\n aiResult = await callDeepSeekStream(modelMessages, {\n maxCompletionTokens,\n onToken: (token) => writeSse(res, 'delta', { text: token }),\n });\n }\n } else {\n aiResult = await callDeepSeekStream(modelMessages, {\n maxCompletionTokens,\n onToken: (token) => writeSse(res, 'delta', { text: token }),\n });\n }\n } else {\n if (!codexRuntime.enabled || isDeepSeekQuotaError(error)) throw error;\n logger.warn({ error: String(error) }, 'Codex SDK streaming chat failed, falling back to DeepSeek direct');\n provider = 'deepseek';\n responseModel = deepseekModel;\n writeSse(res, 'meta', {\n conversationId,\n provider,\n model: responseModel,\n });\n aiResult = await callDeepSeekStream(modelMessages, {\n maxCompletionTokens,\n onToken: (token) => writeSse(res, 'delta', { text: token }),\n });\n }\n }\n\n const usage = {\n promptTokens: aiResult.usage.promptTokens || estimateTokens(JSON.stringify(modelMessages)),\n completionTokens: aiResult.usage.completionTokens || estimateTokens(aiResult.message),\n totalTokens:\n aiResult.usage.totalTokens ||\n estimateTokens(JSON.stringify(modelMessages)) + estimateTokens(aiResult.message),\n };\n recordUsage(user.userId, usage.totalTokens);\n\n const answeredAt = nowIso();\n statements.insertMessage.run({\n id: crypto.randomUUID(),\n conversationId,\n role: 'assistant',\n content: aiResult.message,\n promptTokens: usage.promptTokens,\n completionTokens: usage.completionTokens,\n totalTokens: usage.totalTokens,\n createdAt: answeredAt,\n });\n statements.updateConversation.run({\n id: conversationId,\n userId: user.userId,\n title: isNewConversation ? titleFromMessage(message) : conversation.title,\n updatedAt: answeredAt,\n });\n\n const latestUsage = usageFor(user.userId);\n const monthlyLimit = budget.limit;\n writeSse(res, 'done', {\n conversationId,\n provider,\n model: responseModel,\n message: aiResult.message,\n usage: {\n ...usage,\n monthlyLimit,\n remainingTokens: Math.max(0, monthlyLimit - latestUsage.used),\n },\n });\n res.end();\n } catch (error) {\n logger.error({ error: error.stack || String(error) }, 'streaming chat failed');\n writeSse(res, 'error', {\n message: publicAiErrorMessage(error),\n details: error.details,\n });\n res.end();\n }\n});\n\napp.get(\n '/api/ai/conversations',\n asyncHandler(async (req, res) => {\n const user = await resolveUser(req, { username: req.query.username });\n const items = statements.listConversations.all(user.userId).map((item) => ({\n id: item.id,\n title: item.title,\n updatedAt: item.updatedAt,\n }));\n jsonResponse(res, 200, { success: true, items });\n }),\n);\n\napp.get(\n '/api/ai/conversations/:id',\n asyncHandler(async (req, res) => {\n const user = await resolveUser(req);\n const conversation = statements.getConversation.get(req.params.id, user.userId);\n if (!conversation) {\n return jsonResponse(res, 404, { success: false, message: 'conversation not found' });\n }\n const messages = statements.listMessages.all(req.params.id).map((item) => ({\n role: item.role,\n content: item.content,\n createdAt: item.createdAt,\n }));\n jsonResponse(res, 200, {\n success: true,\n conversation: {\n id: conversation.id,\n title: conversation.title,\n updatedAt: conversation.updated_at,\n },\n messages,\n });\n }),\n);\n\nfunction buildUrl(base, endpoint, params = {}) {\n const url = new URL(endpoint, `${base}/`);\n for (const [key, value] of Object.entries(params)) {\n if (Array.isArray(value)) {\n for (const item of value) {\n if (item !== undefined && item !== null && item !== '') {\n url.searchParams.append(key, String(item));\n }\n }\n } else if (value !== undefined && value !== null && value !== '') {\n url.searchParams.set(key, String(value));\n }\n }\n return url;\n}\n\nasync function fetchJson(url) {\n const response = await fetch(url, {\n headers: { Accept: 'application/json' },\n signal: AbortSignal.timeout(15_000),\n });\n if (!response.ok) {\n throw new Error(`Request failed: ${response.status}`);\n }\n return response.json();\n}\n\nfunction stripHtml(value) {\n return String(value || '')\n .replace(/<script[\\s\\S]*?<\\/script>/gi, ' ')\n .replace(/<style[\\s\\S]*?<\\/style>/gi, ' ')\n .replace(/<[^>]+>/g, ' ')\n .replace(/ /g, ' ')\n .replace(/</g, '<')\n .replace(/>/g, '>')\n .replace(/&/g, '&')\n .replace(/&#(\\d+);/g, (_match, code) => String.fromCharCode(Number(code)))\n .replace(/\\s+/g, ' ')\n .trim();\n}\n\nfunction cleanExtractedText(value) {\n let text = String(value || '')\n .replace(/\\b[A-Z]{1,3}\\d{2}n\\d{4,5}[a-z]?_p?\\d{4}[a-z]\\d{2}\\b/g, ' ')\n .replace(/\\uFFFD/g, '')\n .replace(/\\s+([,。;:!?、」』)》】])/g, '$1')\n .replace(/([「『《(【])\\s+/g, '$1');\n for (let i = 0; i < 3; i += 1) {\n text = text.replace(/([\\u3400-\\u9fff])\\s+([\\u3400-\\u9fff])/g, '$1$2');\n }\n return text.replace(/[ \\t]{2,}/g, ' ').trim();\n}\n\nfunction compactText(value, limit = 140) {\n const text = stripHtml(value);\n return text.length > limit ? `${text.slice(0, limit)}...` : text;\n}\n\nfunction collectSearchResults(data) {\n if (!data || typeof data !== 'object') return [];\n if (Array.isArray(data.results)) return data.results;\n if (Array.isArray(data?.results?.docs)) return data.results.docs;\n if (Array.isArray(data?.response?.docs)) return data.response.docs;\n if (Array.isArray(data.docs)) return data.docs;\n return [];\n}\n\nfunction firstText(item, keys) {\n for (const key of keys) {\n const value = item?.[key];\n if (Array.isArray(value)) {\n const first = value.map((entry) => safeUserText(String(entry))).find(Boolean);\n if (first) return first;\n }\n if (typeof value === 'string' && value.trim()) return value.trim();\n if (typeof value === 'number') return String(value);\n }\n return '';\n}\n\nfunction firstNumber(item, keys, fallback = undefined) {\n for (const key of keys) {\n const value = item?.[key];\n if (typeof value === 'number') return value;\n if (typeof value === 'string' && value.trim() && Number.isFinite(Number(value))) {\n return Number(value);\n }\n }\n return fallback;\n}\n\nasync function searchCbetaTitle(query, limit) {\n const url = buildUrl(cbetaApiRoot, 'search/title', { q: query, start: 0, rows: limit });\n const data = await fetchJson(url);\n return collectSearchResults(data).flatMap((item) => {\n const work = firstText(item, ['work', 'work_id', 'workId']);\n const title = firstText(item, ['content', 'title', 'work_title', 'workTitle']);\n if (!work || !title) return [];\n const juan = firstNumber(item, ['juan', 'juan_num', 'juanNum'], 1);\n return [\n {\n id: `cbeta-title-${work}-${juan}`,\n title,\n sourceName: 'CBETA 佛典',\n url: `cbeta:${work}:${juan}`,\n snippet: firstText(item, ['byline', 'creators', 'time_dynasty']) || '可下载经文正文并用于全球法布施。',\n resourceType: 'scripture',\n work,\n juan,\n },\n ];\n });\n}\n\nasync function searchCbetaContent(query, limit) {\n const endpoints = ['search/fulltext', 'search/content', 'search/all_in_one'];\n const settled = await Promise.allSettled(\n endpoints.map((endpoint) =>\n fetchJson(buildUrl(cbetaApiRoot, endpoint, { q: query, start: 0, rows: limit, field: 'content' })),\n ),\n );\n const results = [];\n for (const response of settled) {\n if (response.status !== 'fulfilled') continue;\n for (const item of collectSearchResults(response.value)) {\n const work = firstText(item, ['work', 'work_id', 'workId', 'sutra', 'id']);\n const snippet = compactText(firstText(item, ['kwic', 'highlight', 'snippet', 'content', 'text', 'body', 'p']));\n if (!work || !snippet) continue;\n const juan = firstNumber(item, ['juan', 'juan_num', 'juanNum', '卷'], 1);\n const title = firstText(item, ['title', 'work_title', 'workTitle', 'sutra_name', 'book', 'name']) || work;\n results.push({\n id: `cbeta-content-${work}-${juan}-${sha256(snippet).slice(0, 8)}`,\n title,\n sourceName: 'CBETA 佛典',\n url: `cbeta:${work}:${juan}`,\n snippet,\n resourceType: 'scripture',\n work,\n juan,\n });\n if (results.length >= limit) return results;\n }\n }\n return results;\n}\n\nfunction directUrlResource(query) {\n const url = safeUserText(query);\n let parsed;\n try {\n parsed = new URL(url);\n } catch {\n return null;\n }\n if (!['http:', 'https:'].includes(parsed.protocol)) return null;\n return {\n id: `url-${sha256(url).slice(0, 12)}`,\n title: parsed.hostname,\n sourceName: '网页资源',\n url,\n snippet: '直接下载并提取这个链接的可读正文。',\n resourceType: 'web',\n };\n}\n\nfunction looksLikeAudioQuery(query) {\n return /音乐|音樂|音频|音訊|mp3|audio|念诵|念誦|诵经|誦經|唱诵|唱誦|读诵|讀誦/.test(\n safeUserText(query).toLowerCase(),\n );\n}\n\nfunction archiveSearchTerm(query) {\n return (\n safeUserText(query)\n .replace(/下载|查找|寻找|找一?个|资源|音频|音訊|音乐|音樂|mp3|audio|念诵|念誦|诵经|誦經|唱诵|唱誦|读诵|讀誦/gi, ' ')\n .replace(/\\s+/g, ' ')\n .trim() || safeUserText(query)\n );\n}\n\nfunction solrPhrase(value) {\n return String(value).replace(/\\\\/g, '\\\\\\\\').replace(/\"/g, '\\\\\"');\n}\n\nasync function searchInternetArchiveAudio(query, limit) {\n if (!looksLikeAudioQuery(query)) return [];\n const term = archiveSearchTerm(query);\n if (!term) return [];\n const variants = Array.from(\n new Set([\n term,\n term.replaceAll('金刚经', '金剛經'),\n term.replaceAll('金剛經', '金刚经'),\n ].filter(Boolean)),\n );\n const phraseQuery = variants\n .flatMap((item) => [`title:\"${solrPhrase(item)}\"`, `description:\"${solrPhrase(item)}\"`])\n .join(' OR ');\n const url = buildUrl('https://archive.org', 'advancedsearch.php', {\n q: `(${phraseQuery}) AND mediatype:audio`,\n rows: Math.min(Math.max(limit, 1), 10),\n output: 'json',\n 'fl[]': ['identifier', 'title', 'description'],\n });\n const data = await fetchJson(url);\n const docs = Array.isArray(data?.response?.docs) ? data.response.docs : [];\n return docs\n .map((item) => {\n const identifier = firstText(item, ['identifier']);\n const title = firstText(item, ['title']) || identifier;\n if (!identifier || !title) return null;\n return {\n id: `ia-audio-${identifier}`,\n title,\n sourceName: 'Internet Archive 音频',\n url: `ia:${identifier}`,\n snippet: compactText(firstText(item, ['description'])) || '公开可访问的音频资源,将下载第一个可用 MP3 文件。',\n resourceType: 'audio',\n identifier,\n };\n })\n .filter(Boolean);\n}\n\nconst curatedCbetaWorks = [\n {\n work: 'T0235',\n juan: 1,\n title: '金剛般若波羅蜜經',\n keywords: ['金剛般若波羅蜜經', '金刚般若波罗蜜经', '金剛經', '金刚经'],\n snippet: '姚秦 鳩摩羅什譯。常用于读诵、共修与法布施的般若经典。',\n },\n {\n work: 'T0251',\n juan: 1,\n title: '般若波羅蜜多心經',\n keywords: ['般若波羅蜜多心經', '般若波罗蜜多心经', '心經', '心经'],\n snippet: '唐 玄奘譯。篇幅短,适合加入禅室功课本与日常持诵。',\n },\n {\n work: 'T0262',\n juan: 1,\n title: '妙法蓮華經',\n keywords: ['妙法蓮華經', '妙法莲华经', '法華經', '法华经'],\n snippet: '姚秦 鳩摩羅什譯。可按卷下载正文,用于长期功课与分享。',\n },\n {\n work: 'T0279',\n juan: 1,\n title: '大方廣佛華嚴經',\n keywords: ['大方廣佛華嚴經', '大方广佛华严经', '華嚴經', '华严经'],\n snippet: '唐 實叉難陀譯。可按卷下载正文,用于系统研读与法布施。',\n },\n {\n work: 'T0360',\n juan: 1,\n title: '佛說無量壽經',\n keywords: ['佛說無量壽經', '佛说无量寿经', '無量壽經', '无量寿经'],\n snippet: '净土经典。可下载正文,用于净土功课本与全球法布施。',\n },\n {\n work: 'T0365',\n juan: 1,\n title: '佛說觀無量壽佛經',\n keywords: ['佛說觀無量壽佛經', '佛说观无量寿佛经', '觀無量壽經', '观无量寿经'],\n snippet: '净土三经之一。适合查找下载后加入净土功课。',\n },\n {\n work: 'T0366',\n juan: 1,\n title: '佛說阿彌陀經',\n keywords: ['佛說阿彌陀經', '佛说阿弥陀经', '阿彌陀經', '阿弥陀经'],\n snippet: '姚秦 鳩摩羅什譯。篇幅适中,适合共修、持诵与分享。',\n },\n {\n work: 'T0475',\n juan: 1,\n title: '維摩詰所說經',\n keywords: ['維摩詰所說經', '维摩诘所说经', '維摩詰經', '维摩诘经'],\n snippet: '姚秦 鳩摩羅什譯。大乘经典,可按卷下载用于研读。',\n },\n {\n work: 'T0676',\n juan: 1,\n title: '解深密經',\n keywords: ['解深密經', '解深密经'],\n snippet: '唯识相关大乘经典。适合研读、整理笔记与法布施。',\n },\n {\n work: 'T1666',\n juan: 1,\n title: '大乘起信論',\n keywords: ['大乘起信論', '大乘起信论', '起信論', '起信论'],\n snippet: '大乘论典。适合下载后作为研习功课资料。',\n },\n];\n\nfunction localCbetaResults(query, limit) {\n const normalizedQuery = safeUserText(query).toLowerCase();\n if (!normalizedQuery) return [];\n return curatedCbetaWorks\n .filter((item) =>\n item.keywords.some((keyword) => {\n const normalizedKeyword = keyword.toLowerCase();\n return normalizedKeyword.includes(normalizedQuery) || normalizedQuery.includes(normalizedKeyword);\n }),\n )\n .slice(0, limit)\n .map((item) => ({\n id: `cbeta-curated-${item.work}-${item.juan}`,\n title: item.title,\n sourceName: 'CBETA 佛典',\n url: `cbeta:${item.work}:${item.juan}`,\n snippet: item.snippet,\n resourceType: 'scripture',\n work: item.work,\n juan: item.juan,\n }));\n}\n\nfunction uniqueResources(items, limit) {\n const seen = new Set();\n const results = [];\n for (const item of items) {\n const key = `${item.url}:${item.title}`;\n if (seen.has(key)) continue;\n seen.add(key);\n results.push(item);\n if (results.length >= limit) break;\n }\n return results;\n}\n\nasync function findResourceCandidates(query, limit) {\n const direct = directUrlResource(query);\n const results = [];\n if (direct) results.push(direct);\n try {\n results.push(...(await searchInternetArchiveAudio(query, limit)));\n } catch (error) {\n logger.warn({ error: String(error), query }, 'Internet Archive audio search skipped');\n }\n results.push(...localCbetaResults(query, limit));\n\n const [titleResults, contentResults] = await Promise.allSettled([\n searchCbetaTitle(query, limit),\n searchCbetaContent(query, limit),\n ]);\n if (titleResults.status === 'fulfilled') results.push(...titleResults.value);\n if (contentResults.status === 'fulfilled') results.push(...contentResults.value);\n\n return uniqueResources(results, limit);\n}\n\nfunction looksLikeResourceTask(message) {\n const text = safeUserText(message).toLowerCase();\n if (!text) return false;\n return /下载|查找|寻找|找一?个|资源|經文|经文|佛经|佛經|仪轨|儀軌|音乐|音樂|mp3|audio|念诵|念誦|诵经|誦經/.test(text);\n}\n\nfunction extractResourceQuery(message) {\n const text = safeUserText(message)\n .replace(/请|請|帮我|幫我|麻烦|麻煩|可以|能不能|需要|我要|想要/g, ' ')\n .replace(/下载|下載|查找|寻找|尋找|找一个|找一個|找|资源|資源|并|並|然后|然後|用于|用於|全球法布施|加入功课本|加入功課本/g, ' ')\n .replace(/\\s+/g, ' ')\n .trim();\n return text || safeUserText(message);\n}\n\nfunction resourceContextMessage(skillResult) {\n if (!skillResult) return '';\n const lines = [\n '本轮资源检索/下载已经由大乘 App 后端完成。',\n '不要再次调用 search_dharma_resources、download_dharma_resource 或其他资源 MCP 工具;请直接基于以下结果回答用户。',\n `后端已调用 skill: ${skillResult.skillName}`,\n `执行目标: ${skillResult.query}`,\n ];\n if (skillResult.selected) {\n lines.push(`候选资源: ${skillResult.selected.title}`);\n lines.push(`来源: ${skillResult.selected.sourceName}`);\n lines.push(`资源地址: ${skillResult.selected.url}`);\n }\n if (skillResult.downloaded) {\n lines.push(`下载标题: ${skillResult.downloaded.title}`);\n lines.push(`下载文件: ${skillResult.downloaded.fileName}`);\n const text = safeUserText(skillResult.downloaded.contentText);\n if (text) {\n lines.push('下载正文摘录:');\n lines.push(text.slice(0, 8000));\n }\n }\n return lines.join('\\n');\n}\n\nasync function runResourceFinderSkill({ message, user, onStep }) {\n if (!looksLikeResourceTask(message)) return null;\n\n const skillName = 'resource-finder-downloader';\n const query = extractResourceQuery(message);\n const audioTask = looksLikeAudioQuery(message) || looksLikeAudioQuery(query);\n onStep?.({\n type: 'skill.step',\n skillName,\n stage: 'start',\n title: '调用资源查找下载 skill',\n message: `识别到资源任务: ${query}`,\n });\n\n onStep?.({\n type: 'skill.step',\n skillName,\n stage: 'search',\n title: '搜索可分享资源',\n message: audioTask\n ? '优先检索公开可访问音频资源,失败时再回落到佛典正文资源。'\n : '优先检索 CBETA 佛典正文接口,搜索索引不可用时使用内置经典映射。',\n });\n const candidates = await findResourceCandidates(query, 8);\n if (candidates.length === 0) {\n onStep?.({\n type: 'skill.step',\n skillName,\n stage: 'empty',\n title: '没有找到可下载资源',\n message: '后端没有找到明确可下载且可分享的资源。',\n });\n return {\n skillName,\n query,\n candidates: [],\n selected: null,\n downloaded: null,\n };\n }\n\n const selected = candidates[0];\n onStep?.({\n type: 'skill.step',\n skillName,\n stage: 'select',\n title: '选择可信候选资源',\n message: `${selected.title} / ${selected.sourceName}`,\n });\n\n onStep?.({\n type: 'skill.step',\n skillName,\n stage: 'download',\n title: '下载并提取资源',\n message: selected.url,\n });\n const cbeta = parseCbetaResource(selected);\n const internetArchive = parseInternetArchiveResource(selected);\n const downloaded = cbeta\n ? await downloadCbetaResource({ ...selected, ...cbeta })\n : internetArchive\n ? await downloadInternetArchiveAudio({ ...selected, ...internetArchive })\n : await downloadWebResource(selected);\n const persisted = persistDownloadedResource(user, downloaded);\n\n onStep?.({\n type: 'skill.step',\n skillName,\n stage: 'done',\n title: '资源已准备完成',\n message: downloaded.fileName,\n });\n\n return {\n skillName,\n query,\n candidates,\n selected,\n downloaded: {\n ...downloaded,\n downloadId: persisted.id,\n },\n };\n}\n\napp.post(\n '/api/resources/search',\n asyncHandler(async (req, res) => {\n const query = safeUserText(req.body?.query);\n const limit = Math.min(Math.max(Number(req.body?.limit || 12), 1), 20);\n if (query.length < 2) {\n return jsonResponse(res, 400, { success: false, message: 'query is required' });\n }\n\n const items = await findResourceCandidates(query, limit);\n\n jsonResponse(res, 200, {\n success: true,\n source: 'resource-index',\n items,\n });\n }),\n);\n\nfunction parseCbetaResource(input) {\n const work = safeUserText(input.work);\n const juan = Number(input.juan || 1);\n if (work) return { work, juan: Number.isFinite(juan) ? juan : 1 };\n\n const url = safeUserText(input.url);\n const match = /^cbeta:([^:]+):(\\d+)$/i.exec(url);\n if (!match) return null;\n return {\n work: match[1].toUpperCase(),\n juan: Number(match[2] || 1),\n };\n}\n\nfunction parseInternetArchiveResource(input) {\n const url = safeUserText(input.url);\n const match = /^ia:([^:]+)$/i.exec(url);\n const identifier = match?.[1] || safeUserText(input.identifier);\n if (!identifier) return null;\n return { identifier };\n}\n\nfunction safeFileName(value) {\n const normalized = String(value || 'dharma-resource')\n .replace(/[\\\\/:*?\"<>|]+/g, '-')\n .replace(/\\s+/g, '-')\n .replace(/-+/g, '-')\n .replace(/^-|-$/g, '')\n .slice(0, 80);\n return normalized || 'dharma-resource';\n}\n\nfunction extractCbetaHtml(data) {\n const first = Array.isArray(data?.results) ? data.results[0] : null;\n if (typeof first === 'string') return first;\n if (first && typeof first === 'object' && typeof first.html === 'string') return first.html;\n if (typeof data?.html === 'string') return data.html;\n return '';\n}\n\nasync function downloadCbetaResource(resource) {\n const url = buildUrl(cbetaApiRoot, 'juans', {\n work: resource.work,\n juan: resource.juan,\n work_info: 1,\n toc: 1,\n });\n const data = await fetchJson(url);\n const workInfo = data.work_info || {};\n const title = safeUserText(workInfo.title) || safeUserText(resource.title) || resource.work;\n const html = extractCbetaHtml(data);\n const text = cleanExtractedText(stripHtml(html));\n if (!text) throw new Error('CBETA resource returned empty content');\n return {\n title,\n sourceName: 'CBETA 佛典',\n url: `cbeta:${resource.work}:${resource.juan}`,\n contentText: text.slice(0, maxResourceTextChars),\n fileName: `${resource.work}_${resource.juan}_${safeFileName(title)}.txt`,\n };\n}\n\nasync function downloadInternetArchiveAudio(resource) {\n const identifier = safeUserText(resource.identifier);\n if (!identifier) throw new Error('Internet Archive identifier is required');\n const metadata = await fetchJson(`https://archive.org/metadata/${encodeURIComponent(identifier)}`);\n const title = safeUserText(metadata?.metadata?.title) || safeUserText(resource.title) || identifier;\n const files = Array.isArray(metadata?.files) ? metadata.files : [];\n const audioFile = files.find((file) => {\n const name = safeUserText(file?.name);\n const format = safeUserText(file?.format).toLowerCase();\n return /\\.(mp3|m4a|ogg)$/i.test(name) || format.includes('mp3');\n });\n if (!audioFile?.name) {\n throw new Error('Internet Archive audio item has no downloadable audio file');\n }\n const sourceFileName = audioFile.name;\n const fileUrl = `https://archive.org/download/${encodeURIComponent(identifier)}/${sourceFileName\n .split('/')\n .map((part) => encodeURIComponent(part))\n .join('/')}`;\n const response = await fetch(fileUrl, {\n headers: {\n Accept: 'audio/mpeg, audio/*;q=0.9, */*;q=0.5',\n 'User-Agent': 'DachengResourceDownloader/1.0 (+https://ombhrum.com)',\n },\n signal: AbortSignal.timeout(60_000),\n });\n if (!response.ok) {\n throw new Error(`Internet Archive audio download failed: ${response.status}`);\n }\n const binaryContent = Buffer.from(await response.arrayBuffer());\n if (!binaryContent.length) throw new Error('Internet Archive audio returned empty content');\n const sizeMb = (binaryContent.length / 1024 / 1024).toFixed(1);\n return {\n title,\n sourceName: 'Internet Archive 音频',\n url: fileUrl,\n contentText: `已下载音频资源: ${title}\\n文件名: ${sourceFileName}\\n大小: ${sizeMb} MB\\n来源: ${fileUrl}`,\n binaryContent,\n fileName: safeFileName(sourceFileName),\n };\n}\n\nasync function downloadWebResource(input) {\n const url = safeUserText(input.url);\n const parsed = new URL(url);\n if (!['http:', 'https:'].includes(parsed.protocol)) {\n throw new Error('Only http/https resources can be downloaded');\n }\n\n const response = await fetch(parsed, {\n headers: {\n Accept: 'text/html, text/plain, application/xhtml+xml, application/xml;q=0.8, */*;q=0.5',\n 'User-Agent': 'DachengResourceDownloader/1.0 (+https://ombhrum.com)',\n },\n signal: AbortSignal.timeout(25_000),\n });\n if (!response.ok) throw new Error(`Resource download failed: ${response.status}`);\n const contentType = response.headers.get('content-type') || '';\n const body = await response.text();\n const title = safeUserText(input.title) || parsed.hostname;\n const contentText = contentType.includes('html') ? cleanExtractedText(stripHtml(body)) : body.trim();\n if (!contentText) throw new Error('Resource returned empty content');\n return {\n title,\n sourceName: safeUserText(input.sourceName) || parsed.hostname,\n url,\n contentText: contentText.slice(0, maxResourceTextChars),\n fileName: `${safeFileName(title)}.txt`,\n };\n}\n\nfunction persistDownloadedResource(user, content) {\n const id = crypto.randomUUID();\n const fileName = `${id}-${safeFileName(content.fileName)}`;\n const filePath = path.join(resourcesDir, fileName);\n if (content.binaryContent) {\n fs.writeFileSync(filePath, content.binaryContent);\n } else {\n fs.writeFileSync(filePath, content.contentText, 'utf8');\n }\n statements.insertResourceDownload.run({\n id,\n userId: user.userId,\n title: content.title,\n sourceName: content.sourceName,\n sourceUrl: content.url,\n fileName: content.fileName,\n filePath,\n createdAt: nowIso(),\n });\n return { id, fileName, filePath };\n}\n\nfunction createLibreChatMcpServer() {\n const server = new McpServer({\n name: 'dacheng-ai-tools',\n version: '0.1.0',\n });\n\n server.registerTool(\n 'search_dharma_resources',\n {\n title: 'Search dharma resources',\n description:\n 'Search for legally shareable Buddhist scripture, text, web, or audio resources that can be prepared for Fabushi sharing.',\n inputSchema: {\n query: z.string().min(2).max(200),\n limit: z.number().int().min(1).max(12).optional(),\n },\n outputSchema: {\n query: z.string(),\n items: z.array(\n z.object({\n id: z.string(),\n title: z.string(),\n sourceName: z.string(),\n url: z.string(),\n snippet: z.string(),\n resourceType: z.string(),\n work: z.string().optional(),\n juan: z.number().optional(),\n }),\n ),\n },\n annotations: {\n readOnlyHint: true,\n destructiveHint: false,\n openWorldHint: true,\n },\n },\n async ({ query, limit }) => {\n const items = await findResourceCandidates(query, limit ?? 8);\n return textToolResult(`Found ${items.length} candidate resources for \"${query}\".`, {\n query,\n items,\n });\n },\n );\n\n server.registerTool(\n 'download_dharma_resource',\n {\n title: 'Download dharma resource',\n description:\n 'Download and extract a selected resource. Use the url/work/juan returned by search_dharma_resources whenever possible.',\n inputSchema: {\n url: z.string().min(1).max(1000),\n title: z.string().max(300).optional(),\n sourceName: z.string().max(120).optional(),\n work: z.string().max(40).optional(),\n juan: z.number().int().min(1).optional(),\n identifier: z.string().max(300).optional(),\n },\n outputSchema: {\n title: z.string(),\n sourceName: z.string(),\n url: z.string(),\n fileName: z.string(),\n downloadId: z.string(),\n contentText: z.string(),\n },\n annotations: {\n readOnlyHint: false,\n destructiveHint: false,\n openWorldHint: true,\n },\n },\n async (input) => {\n const user = { userId: 'librechat:mcp' };\n const cbeta = parseCbetaResource(input);\n const internetArchive = parseInternetArchiveResource(input);\n const content = cbeta\n ? await downloadCbetaResource({ ...input, ...cbeta })\n : internetArchive\n ? await downloadInternetArchiveAudio({ ...input, ...internetArchive })\n : await downloadWebResource(input);\n const persisted = persistDownloadedResource(user, content);\n const { binaryContent: _binaryContent, ...publicContent } = content;\n return textToolResult(`Downloaded ${publicContent.title} as ${publicContent.fileName}.`, {\n ...publicContent,\n downloadId: persisted.id,\n });\n },\n );\n\n server.registerTool(\n 'prepare_dharma_share_text',\n {\n title: 'Prepare dharma sharing text',\n description:\n 'Prepare a text payload for the Fabushi app to confirm and add to global dharma sharing. This returns a clientAction; it does not start sending.',\n inputSchema: {\n title: z.string().min(1).max(120),\n text: z.string().min(1).max(20000),\n },\n outputSchema: {\n clientAction: z.object({\n type: z.literal('prepare_dharma_share_text'),\n title: z.string(),\n text: z.string(),\n }),\n },\n annotations: {\n readOnlyHint: false,\n destructiveHint: false,\n openWorldHint: false,\n },\n },\n async ({ title, text }) =>\n textToolResult(`Prepared \"${title}\" for user confirmation in the Fabushi app.`, {\n clientAction: {\n type: 'prepare_dharma_share_text',\n title: safeUserText(title).slice(0, 120),\n text: String(text || '').trim().slice(0, 20000),\n },\n }),\n );\n\n server.registerTool(\n 'prepare_practice_book_item',\n {\n title: 'Prepare practice book item',\n description:\n 'Prepare a scripture, chant, or practice item for the Fabushi app practice book. This returns a clientAction; it does not mutate the app by itself.',\n inputSchema: {\n title: z.string().min(1).max(120),\n sourceName: z.string().max(120).optional(),\n text: z.string().min(1).max(20000),\n },\n outputSchema: {\n clientAction: z.object({\n type: z.literal('prepare_practice_book_item'),\n title: z.string(),\n sourceName: z.string(),\n text: z.string(),\n }),\n },\n annotations: {\n readOnlyHint: false,\n destructiveHint: false,\n openWorldHint: false,\n },\n },\n async ({ title, sourceName, text }) =>\n textToolResult(`Prepared \"${title}\" for the Fabushi practice book confirmation flow.`, {\n clientAction: {\n type: 'prepare_practice_book_item',\n title: safeUserText(title).slice(0, 120),\n sourceName: safeUserText(sourceName || '大乘 AI').slice(0, 120),\n text: String(text || '').trim().slice(0, 20000),\n },\n }),\n );\n\n return server;\n}\n\nasync function handleMcpRequest(req, res) {\n const server = createLibreChatMcpServer();\n const transport = new StreamableHTTPServerTransport({\n sessionIdGenerator: undefined,\n enableJsonResponse: true,\n });\n\n res.on('close', () => {\n transport.close();\n server.close();\n });\n\n await server.connect(transport);\n await transport.handleRequest(req, res, req.body);\n}\n\napp.all('/mcp', async (req, res) => {\n try {\n await handleMcpRequest(req, res);\n } catch (error) {\n logger.error({ error: error.stack || String(error) }, 'MCP request failed');\n if (!res.headersSent) {\n res.status(500).json({\n error: 'MCP request failed',\n message: publicAiErrorMessage(error),\n });\n }\n }\n});\n\napp.post(\n '/api/resources/download',\n asyncHandler(async (req, res) => {\n const user = await resolveUser(req, req.body || {});\n const cbeta = parseCbetaResource(req.body || {});\n const internetArchive = parseInternetArchiveResource(req.body || {});\n const content = cbeta\n ? await downloadCbetaResource({ ...req.body, ...cbeta })\n : internetArchive\n ? await downloadInternetArchiveAudio({ ...req.body, ...internetArchive })\n : await downloadWebResource(req.body || {});\n const persisted = persistDownloadedResource(user, content);\n const { binaryContent: _binaryContent, ...publicContent } = content;\n jsonResponse(res, 200, {\n success: true,\n downloadId: persisted.id,\n ...publicContent,\n });\n }),\n);\n\napp.post(\n '/api/codex/resource-task',\n asyncHandler(async (req, res) => {\n if (process.env.ENABLE_CODEX_SDK !== 'true') {\n return jsonResponse(res, 403, {\n success: false,\n message: 'Codex SDK task runner is disabled. Set ENABLE_CODEX_SDK=true on the server to enable it.',\n });\n }\n const prompt = safeUserText(req.body?.prompt);\n if (!prompt) {\n return jsonResponse(res, 400, { success: false, message: 'prompt is required' });\n }\n const { Codex } = await import('@openai/codex-sdk');\n const codexRuntime = createCodexDeepSeekRuntime();\n const codex = new Codex(codexRuntime.enabled ? codexRuntime.options : {});\n const thread = codex.startThread(\n codexRuntime.enabled\n ? codexRuntime.threadOptions\n : { skipGitRepoCheck: true, networkAccessEnabled: true },\n );\n const result = await thread.run(prompt);\n jsonResponse(res, 200, {\n success: true,\n threadId: thread.id,\n provider: codexRuntime.provider,\n model: deepseekModel,\n result,\n });\n }),\n);\n\napp.use((error, _req, res, _next) => {\n const status = Number(error.statusCode || error.status || 500);\n const safeStatus = status >= 400 && status < 600 ? status : 500;\n logger.error({ error: error.stack || String(error) }, 'request failed');\n jsonResponse(res, safeStatus, {\n success: false,\n message: error.message || 'Internal Server Error',\n details: error.details,\n });\n});\n\nconst port = Number(process.env.PORT || 8788);\napp.listen(port, '0.0.0.0', () => {\n logger.info(\n {\n port,\n dbPath,\n dataDir,\n deepseekModel,\n cbetaApiRoot,\n },\n 'Dacheng AI backend started',\n );\n});\n"}, "2029d40b14d05d3b2991bdbb82e9751f6fe0a100": {"t": 1785757983.550837, "n": 115742, "h": "e7cfa2d149be5761", "c": "import crypto from 'node:crypto';\nimport fs from 'node:fs';\nimport path from 'node:path';\nimport process from 'node:process';\nimport { fileURLToPath } from 'node:url';\n\nimport { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';\nimport { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';\nimport Database from 'better-sqlite3';\nimport cors from 'cors';\nimport dotenv from 'dotenv';\nimport express from 'express';\nimport rateLimit from 'express-rate-limit';\nimport helmet from 'helmet';\nimport pino from 'pino';\nimport { z } from 'zod';\n\ndotenv.config();\n\nconst __dirname = path.dirname(fileURLToPath(import.meta.url));\nconst rootDir = path.resolve(__dirname, '..');\nconst dataDir = process.env.DATA_DIR || path.join(rootDir, 'data');\nconst resourcesDir = path.join(dataDir, 'resources');\nconst openClawRuntimeUpdatesDir =\n process.env.OPENCLAW_RUNTIME_UPDATES_DIR || path.join(dataDir, 'openclaw-runtime');\nconst codexHomeDir = process.env.CODEX_HOME || path.join(dataDir, 'codex-home');\nconst codexTempDir = process.env.CODEX_TMPDIR || path.join(dataDir, 'codex-tmp');\nconst codexRuntimeDir = process.env.XDG_RUNTIME_DIR || path.join(dataDir, 'codex-runtime');\nconst dbPath = process.env.SQLITE_PATH || path.join(dataDir, 'dacheng-ai.sqlite');\n\nfs.mkdirSync(resourcesDir, { recursive: true });\nfs.mkdirSync(openClawRuntimeUpdatesDir, { recursive: true });\nfs.mkdirSync(codexHomeDir, { recursive: true });\nfs.mkdirSync(codexTempDir, { recursive: true });\nfs.mkdirSync(codexRuntimeDir, { recursive: true });\n\nconst logger = pino({\n level: process.env.LOG_LEVEL || 'info',\n});\n\nconst app = express();\napp.disable('x-powered-by');\napp.set('trust proxy', Number(process.env.TRUST_PROXY_HOPS || 1));\napp.use(helmet());\nconst corsOrigin =\n !process.env.CORS_ORIGIN || process.env.CORS_ORIGIN.trim() === '*'\n ? true\n : process.env.CORS_ORIGIN.split(',').map((item) => item.trim()).filter(Boolean);\napp.use(cors({ origin: corsOrigin }));\napp.use(express.json({ limit: '1mb' }));\napp.use(\n rateLimit({\n windowMs: 60_000,\n limit: Number(process.env.RATE_LIMIT_PER_MINUTE || 90),\n standardHeaders: true,\n legacyHeaders: false,\n }),\n);\n\nconst db = new Database(dbPath);\ndb.pragma('journal_mode = WAL');\ndb.pragma('foreign_keys = ON');\n\ndb.exec(`\n CREATE TABLE IF NOT EXISTS conversations (\n id TEXT PRIMARY KEY,\n user_id TEXT NOT NULL,\n title TEXT NOT NULL,\n provider TEXT NOT NULL,\n model TEXT NOT NULL,\n created_at TEXT NOT NULL,\n updated_at TEXT NOT NULL\n );\n\n CREATE INDEX IF NOT EXISTS idx_conversations_user_updated\n ON conversations (user_id, updated_at DESC);\n\n CREATE TABLE IF NOT EXISTS messages (\n id TEXT PRIMARY KEY,\n conversation_id TEXT NOT NULL,\n role TEXT NOT NULL,\n content TEXT NOT NULL,\n prompt_tokens INTEGER NOT NULL DEFAULT 0,\n completion_tokens INTEGER NOT NULL DEFAULT 0,\n total_tokens INTEGER NOT NULL DEFAULT 0,\n created_at TEXT NOT NULL,\n FOREIGN KEY (conversation_id) REFERENCES conversations(id) ON DELETE CASCADE\n );\n\n CREATE INDEX IF NOT EXISTS idx_messages_conversation_created\n ON messages (conversation_id, created_at ASC);\n\n CREATE TABLE IF NOT EXISTS usage_months (\n user_id TEXT NOT NULL,\n month TEXT NOT NULL,\n total_tokens INTEGER NOT NULL DEFAULT 0,\n updated_at TEXT NOT NULL,\n PRIMARY KEY (user_id, month)\n );\n\n CREATE TABLE IF NOT EXISTS resource_downloads (\n id TEXT PRIMARY KEY,\n user_id TEXT NOT NULL,\n title TEXT NOT NULL,\n source_name TEXT NOT NULL,\n source_url TEXT NOT NULL,\n file_name TEXT NOT NULL,\n file_path TEXT NOT NULL,\n created_at TEXT NOT NULL\n );\n\n CREATE TABLE IF NOT EXISTS agent_runs (\n id TEXT PRIMARY KEY,\n user_id TEXT NOT NULL,\n conversation_id TEXT NOT NULL,\n message_id TEXT NOT NULL,\n openclaw_run_id TEXT,\n openclaw_session_key TEXT NOT NULL,\n status TEXT NOT NULL,\n mode TEXT NOT NULL,\n provider TEXT NOT NULL,\n model TEXT,\n input_tokens INTEGER NOT NULL DEFAULT 0,\n output_tokens INTEGER NOT NULL DEFAULT 0,\n tool_call_count INTEGER NOT NULL DEFAULT 0,\n cost_microusd INTEGER NOT NULL DEFAULT 0,\n started_at TEXT NOT NULL,\n completed_at TEXT,\n failed_at TEXT,\n error_code TEXT,\n error_message TEXT\n );\n\n CREATE INDEX IF NOT EXISTS idx_agent_runs_user_started\n ON agent_runs (user_id, started_at DESC);\n\n CREATE INDEX IF NOT EXISTS idx_agent_runs_status_started\n ON agent_runs (status, started_at DESC);\n\n CREATE TABLE IF NOT EXISTS agent_message_feedback (\n id TEXT PRIMARY KEY,\n message_id TEXT NOT NULL,\n user_id TEXT NOT NULL,\n rating TEXT NOT NULL,\n reason TEXT,\n comment TEXT,\n created_at TEXT NOT NULL\n );\n`);\n\nconst statements = {\n insertConversation: db.prepare(`\n INSERT INTO conversations (id, user_id, title, provider, model, created_at, updated_at)\n VALUES (@id, @userId, @title, @provider, @model, @createdAt, @updatedAt)\n `),\n updateConversation: db.prepare(`\n UPDATE conversations\n SET title = @title, updated_at = @updatedAt\n WHERE id = @id AND user_id = @userId\n `),\n getConversation: db.prepare(`\n SELECT * FROM conversations WHERE id = ? AND user_id = ? LIMIT 1\n `),\n listConversations: db.prepare(`\n SELECT id, title, updated_at AS updatedAt\n FROM conversations\n WHERE user_id = ?\n ORDER BY updated_at DESC\n LIMIT 30\n `),\n insertMessage: db.prepare(`\n INSERT INTO messages (\n id, conversation_id, role, content,\n prompt_tokens, completion_tokens, total_tokens, created_at\n )\n VALUES (\n @id, @conversationId, @role, @content,\n @promptTokens, @completionTokens, @totalTokens, @createdAt\n )\n `),\n listMessages: db.prepare(`\n SELECT role, content, prompt_tokens AS promptTokens,\n completion_tokens AS completionTokens, total_tokens AS totalTokens,\n created_at AS createdAt\n FROM messages\n WHERE conversation_id = ?\n ORDER BY created_at ASC\n `),\n getUsage: db.prepare(`\n SELECT total_tokens AS totalTokens FROM usage_months WHERE user_id = ? AND month = ?\n `),\n upsertUsage: db.prepare(`\n INSERT INTO usage_months (user_id, month, total_tokens, updated_at)\n VALUES (@userId, @month, @totalTokens, @updatedAt)\n ON CONFLICT(user_id, month)\n DO UPDATE SET total_tokens = usage_months.total_tokens + excluded.total_tokens,\n updated_at = excluded.updated_at\n `),\n insertResourceDownload: db.prepare(`\n INSERT INTO resource_downloads (\n id, user_id, title, source_name, source_url, file_name, file_path, created_at\n )\n VALUES (\n @id, @userId, @title, @sourceName, @sourceUrl, @fileName, @filePath, @createdAt\n )\n `),\n insertAgentRun: db.prepare(`\n INSERT INTO agent_runs (\n id, user_id, conversation_id, message_id, openclaw_run_id, openclaw_session_key,\n status, mode, provider, model, started_at\n )\n VALUES (\n @id, @userId, @conversationId, @messageId, @openClawRunId, @openClawSessionKey,\n @status, @mode, @provider, @model, @startedAt\n )\n `),\n getAgentRun: db.prepare(`\n SELECT * FROM agent_runs WHERE id = ? AND user_id = ? LIMIT 1\n `),\n updateAgentRunStatus: db.prepare(`\n UPDATE agent_runs\n SET status = @status,\n openclaw_run_id = COALESCE(@openClawRunId, openclaw_run_id),\n input_tokens = COALESCE(@inputTokens, input_tokens),\n output_tokens = COALESCE(@outputTokens, output_tokens),\n tool_call_count = COALESCE(@toolCallCount, tool_call_count),\n completed_at = @completedAt,\n failed_at = @failedAt,\n error_code = @errorCode,\n error_message = @errorMessage\n WHERE id = @id AND user_id = @userId\n `),\n insertAgentFeedback: db.prepare(`\n INSERT INTO agent_message_feedback (id, message_id, user_id, rating, reason, comment, created_at)\n VALUES (@id, @messageId, @userId, @rating, @reason, @comment, @createdAt)\n `),\n};\n\nconst deepseekApiKey = process.env.DEEPSEEK_API_KEY || '';\nconst deepseekBaseUrl = (process.env.DEEPSEEK_BASE_URL || 'https://api.deepseek.com').replace(/\\/+$/, '');\nconst deepseekModel = process.env.DEEPSEEK_MODEL || 'deepseek-chat';\nconst cbetaApiRoot = (process.env.CBETA_API_ROOT || 'https://api.ombhrum.com/api/cbeta').replace(/\\/+$/, '');\nconst fabushiApiBaseUrl = (process.env.FABUSHI_API_BASE_URL || 'https://api.ombhrum.com').replace(/\\/+$/, '');\nconst memberMonthlyLimit = Number(process.env.MEMBER_MONTHLY_TOKEN_LIMIT || 1_000_000);\nconst freeMonthlyLimit = Number(process.env.FREE_MONTHLY_TOKEN_LIMIT || 1_000);\nconst requireMemberForAi = process.env.REQUIRE_MEMBER_FOR_AI === 'true';\nconst deepseekMaxCompletionTokens = Math.max(1, Number(process.env.DEEPSEEK_MAX_COMPLETION_TOKENS || 1400));\nconst maxResourceTextChars = Number(process.env.MAX_RESOURCE_TEXT_CHARS || 80_000);\nconst openClawRuntimeManifestPath =\n process.env.OPENCLAW_RUNTIME_MANIFEST_PATH || path.join(openClawRuntimeUpdatesDir, 'manifest.json');\nconst openClawRuntimePublicBaseUrl = (process.env.OPENCLAW_RUNTIME_PUBLIC_BASE_URL || '').replace(/\\/+$/, '');\nconst enableLibreChatAgentChat = process.env.ENABLE_LIBRECHAT_AGENT_CHAT === 'true';\nconst libreChatAgentsBaseUrl = (\n process.env.LIBRECHAT_AGENTS_BASE_URL || 'http://127.0.0.1:3080/api/agents/v1'\n).replace(/\\/+$/, '');\nconst libreChatAgentApiKey = process.env.LIBRECHAT_AGENT_API_KEY || '';\nconst libreChatAgentId = process.env.LIBRECHAT_AGENT_ID || '';\nconst enableCodexSdkChat = process.env.ENABLE_CODEX_SDK_CHAT === 'true';\nconst enableOpenClawAgentChat = process.env.ENABLE_OPENCLAW_AGENT_CHAT === 'true';\nconst openClawGatewayUrl = (process.env.OPENCLAW_GATEWAY_URL || 'http://127.0.0.1:18789').replace(/\\/+$/, '');\nconst openClawGatewayToken = process.env.OPENCLAW_GATEWAY_TOKEN || '';\nconst openClawAgentId = process.env.OPENCLAW_AGENT_ID || 'fabushi-public-agent';\nconst openClawRunsEndpoint = process.env.OPENCLAW_RUNS_ENDPOINT || '';\nconst codexDeepSeekProviderId = 'deepseek-chat-completions';\nconst codexResponsesBaseUrl = (\n process.env.CODEX_DEEPSEEK_RESPONSES_BASE_URL ||\n `http://127.0.0.1:${process.env.PORT || 8788}/codex-deepseek/v1`\n).replace(/\\/+$/, '');\n\nconst systemPrompt = [\n '你是“大乘”App 的 AI 助手。',\n '你的核心任务是帮助用户查找、整理、理解并全球法布施合法可分享的佛法资源。',\n '回答要庄重、简洁、可执行;涉及经典、仪轨或资源时,提醒用户尊重版权、来源和当地法规。',\n '如果用户想找资源、下载经文、寻找音频或准备可分享资料,你必须先在后端自动执行搜索、验证和下载步骤;不要要求用户去前端 + 菜单手动查找。',\n '你可以把执行进度总结给用户,但不要输出隐藏推理原文。',\n].join('\\n');\n\nfunction nowIso() {\n return new Date().toISOString();\n}\n\nfunction monthKey(date = new Date()) {\n return `${date.getUTCFullYear()}-${String(date.getUTCMonth() + 1).padStart(2, '0')}`;\n}\n\nfunction jsonResponse(res, status, payload) {\n res.status(status).json(payload);\n}\n\nasync function readJsonFile(filePath) {\n const raw = await fs.promises.readFile(filePath, 'utf8');\n return JSON.parse(raw);\n}\n\nfunction requestOrigin(req) {\n if (openClawRuntimePublicBaseUrl) return openClawRuntimePublicBaseUrl;\n const proto = req.get('X-Forwarded-Proto') || req.protocol || 'https';\n const host = req.get('X-Forwarded-Host') || req.get('Host');\n return host ? `${proto}://${host}` : '';\n}\n\nfunction absolutizeOpenClawRuntimeUrl(req, value) {\n const text = readText(value);\n if (!text) return '';\n if (/^https?:\\/\\//i.test(text)) return text;\n const normalized = text.startsWith('/')\n ? text\n : `/api/openclaw/runtime/files/${text.replace(/^\\/+/, '')}`;\n const origin = requestOrigin(req);\n return origin ? new URL(normalized, `${origin}/`).toString() : normalized;\n}\n\nfunction textToolResult(text, structuredContent = {}) {\n return {\n structuredContent,\n content: [{ type: 'text', text }],\n };\n}\n\nfunction sseHeaders(res) {\n res.status(200);\n res.setHeader('Content-Type', 'text/event-stream; charset=utf-8');\n res.setHeader('Cache-Control', 'no-cache, no-transform');\n res.setHeader('Connection', 'keep-alive');\n res.setHeader('X-Accel-Buffering', 'no');\n if (typeof res.flushHeaders === 'function') res.flushHeaders();\n}\n\nfunction writeSse(res, event, payload) {\n res.write(`event: ${event}\\n`);\n res.write(`data: ${JSON.stringify(payload)}\\n\\n`);\n}\n\nfunction asyncHandler(fn) {\n return (req, res, next) => {\n Promise.resolve(fn(req, res, next)).catch(next);\n };\n}\n\nfunction bearerToken(req) {\n const delegated = readText(req.get('x-dacheng-auth-token'));\n if (\n delegated &&\n delegated !== 'DACHENG_AUTH_TOKEN' &&\n !delegated.startsWith('secretref-env:') &&\n delegated !== 'dacheng-openclaw-proxy'\n ) {\n return delegated;\n }\n const header = req.get('Authorization') || '';\n if (!header.startsWith('Bearer ')) return '';\n const token = header.slice('Bearer '.length).trim();\n return token === 'dacheng-openclaw-proxy' ? '' : token;\n}\n\nfunction sha256(value) {\n return crypto.createHash('sha256').update(value).digest('hex');\n}\n\nfunction safeUserText(value, fallback = '') {\n if (typeof value !== 'string') return fallback;\n return value.replace(/\\u0000/g, '').trim();\n}\n\nfunction estimateTokens(text) {\n const normalized = String(text || '').trim();\n if (!normalized) return 0;\n return Math.max(1, Math.ceil(normalized.length / 3));\n}\n\nfunction readText(value) {\n if (value === undefined || value === null) return '';\n return String(value).replace(/\\u0000/g, '').trim();\n}\n\nfunction unwrapPayload(payload) {\n if (!payload || typeof payload !== 'object') return {};\n return payload.data && typeof payload.data === 'object' ? payload.data : payload;\n}\n\nfunction readFirstText(values) {\n for (const value of values) {\n const text = readText(value);\n if (text) return text;\n }\n return '';\n}\n\nfunction activeFlag(value) {\n if (value === true) return true;\n if (value === 1) return true;\n if (typeof value !== 'string') return false;\n return ['true', '1', 'yes', 'active'].includes(value.trim().toLowerCase());\n}\n\nfunction futureDate(value) {\n const text = readText(value);\n if (!text) return false;\n const timestamp = Date.parse(text);\n return Number.isFinite(timestamp) && timestamp > Date.now();\n}\n\nfunction normalizeMembershipPayload(payload) {\n if (!payload || typeof payload !== 'object') return false;\n const data = unwrapPayload(payload);\n const membership = data.membership && typeof data.membership === 'object' ? data.membership : data;\n const type = readText(membership.type || membership.membershipType || data.membershipType).toLowerCase();\n const status = readText(membership.status || data.status).toLowerCase();\n const expiry = membership.expiresAt ||\n membership.expires_at ||\n membership.membershipExpiry ||\n membership.membership_expires_at ||\n data.membershipExpiry ||\n data.membership_expires_at;\n const hasExplicitActiveFlag =\n membership.isActive !== undefined ||\n membership.active !== undefined ||\n membership.hasPremiumAccess !== undefined ||\n membership.hasPremiumMembership !== undefined ||\n membership.isPremium !== undefined;\n\n if (hasExplicitActiveFlag) {\n return Boolean(\n activeFlag(membership.isActive) ||\n activeFlag(membership.active) ||\n activeFlag(membership.hasPremiumAccess) ||\n activeFlag(membership.hasPremiumMembership) ||\n activeFlag(membership.isPremium),\n );\n }\n\n return Boolean(\n status === 'active' ||\n type === 'paid' ||\n type === 'premium' ||\n type === 'trial' ||\n futureDate(expiry),\n );\n}\n\nfunction extractRemoteAccount(payload) {\n const data = unwrapPayload(payload);\n const nestedUser = data.user && typeof data.user === 'object' ? data.user : {};\n const id = readFirstText([\n data.userId,\n data.user_id,\n data.userNo,\n data.user_no,\n data.id,\n nestedUser.userId,\n nestedUser.user_id,\n nestedUser.userNo,\n nestedUser.user_no,\n nestedUser.id,\n ]);\n const username = readFirstText([data.username, data.name, nestedUser.username, nestedUser.name]);\n const email = readFirstText([data.email, nestedUser.email]);\n const membership = data.membership && typeof data.membership === 'object' ? data.membership : null;\n return {\n authenticated: Boolean(id || username || email || membership || data.success === true),\n id,\n username,\n email,\n isMember: normalizeMembershipPayload(payload),\n membership,\n };\n}\n\nasync function resolveRemoteAccount(token) {\n const fallback = {\n authenticated: false,\n id: '',\n username: '',\n email: '',\n isMember: false,\n membership: null,\n };\n if (!token || !fabushiApiBaseUrl) return fallback;\n\n const endpoints = [\n '/api/stripe/membership-status',\n '/api/alipay/check-membership',\n '/api/auth/user-info',\n ];\n\n for (const endpoint of endpoints) {\n try {\n const response = await fetch(`${fabushiApiBaseUrl}${endpoint}`, {\n headers: {\n Accept: 'application/json',\n Authorization: `Bearer ${token}`,\n },\n signal: AbortSignal.timeout(8_000),\n });\n if (response.status === 401 || response.status === 403) return fallback;\n if (!response.ok) continue;\n const payload = await response.json();\n const account = extractRemoteAccount(payload);\n if (account.authenticated || account.isMember) {\n return {\n ...fallback,\n ...account,\n authenticated: true,\n };\n }\n } catch (error) {\n logger.debug({ endpoint, error: String(error) }, 'membership lookup skipped');\n }\n }\n\n return fallback;\n}\n\nasync function resolveUser(req, body = {}) {\n const token = bearerToken(req);\n const usernameHint = safeUserText(body.username || req.query.username);\n const tokenHash = token ? sha256(token).slice(0, 24) : '';\n const anonymousHash = sha256(`${req.ip}|${req.get('User-Agent') || ''}`).slice(0, 24);\n const remoteAccount = await resolveRemoteAccount(token);\n const remoteIdentity = remoteAccount.id ||\n remoteAccount.username ||\n remoteAccount.email ||\n (remoteAccount.authenticated ? tokenHash : '');\n const userId = remoteAccount.authenticated\n ? `user:${remoteIdentity}`\n : tokenHash\n ? `token:${tokenHash}`\n : `anon:${anonymousHash}`;\n return {\n userId,\n username: remoteAccount.username || usernameHint,\n tokenHash,\n isAuthenticated: remoteAccount.authenticated,\n isMember: remoteAccount.isMember,\n membership: remoteAccount.membership,\n };\n}\n\nfunction usageFor(userId) {\n const month = monthKey();\n const row = statements.getUsage.get(userId, month);\n return {\n month,\n used: Number(row?.totalTokens || 0),\n };\n}\n\nfunction enforceTokenBudget(user, estimatedTokensForRequest) {\n if (requireMemberForAi && !user.isMember) {\n const error = new Error('大乘 AI 需要开通会员后使用');\n error.statusCode = 403;\n error.code = 'AI_MEMBERSHIP_REQUIRED';\n error.details = {\n requireMemberForAi,\n monthlyLimit: memberMonthlyLimit,\n remainingTokens: 0,\n };\n throw error;\n }\n\n const limit = user.isMember ? memberMonthlyLimit : freeMonthlyLimit;\n const usage = usageFor(user.userId);\n if (usage.used + estimatedTokensForRequest > limit) {\n const error = new Error('本月 AI token 额度已不足');\n error.statusCode = 429;\n error.code = 'USER_TOKEN_QUOTA_EXCEEDED';\n error.details = {\n monthlyLimit: limit,\n usedTokens: usage.used,\n remainingTokens: Math.max(0, limit - usage.used),\n };\n throw error;\n }\n return { limit, usage };\n}\n\nfunction completionBudgetFor(budget, estimatedPromptTokens) {\n const remainingAfterPrompt = budget.limit - budget.usage.used - estimatedPromptTokens;\n return Math.max(1, Math.min(deepseekMaxCompletionTokens, Math.floor(remainingAfterPrompt)));\n}\n\nfunction recordUsage(userId, totalTokens) {\n if (!totalTokens || totalTokens < 1) return;\n statements.upsertUsage.run({\n userId,\n month: monthKey(),\n totalTokens,\n updatedAt: nowIso(),\n });\n}\n\nfunction titleFromMessage(message) {\n const compact = message.replace(/\\s+/g, ' ').trim();\n if (!compact) return '新对话';\n return compact.length > 22 ? `${compact.slice(0, 22)}...` : compact;\n}\n\nfunction normalizeMessages(rows) {\n return rows.map((row) => ({\n role: row.role,\n content: row.content,\n }));\n}\n\nfunction normalizeMaxCompletionTokens(value) {\n const numeric = Number(value || deepseekMaxCompletionTokens);\n if (!Number.isFinite(numeric) || numeric < 1) return deepseekMaxCompletionTokens;\n return Math.max(1, Math.min(deepseekMaxCompletionTokens, Math.floor(numeric)));\n}\n\nfunction normalizeDeepSeekModelName(value) {\n const raw = readText(value || deepseekModel);\n if (!raw) return deepseekModel;\n const modelId = raw.includes('/') ? raw.split('/').pop() : raw;\n return modelId || deepseekModel;\n}\n\nasync function callDeepSeek(messages, options = {}) {\n if (!deepseekApiKey) {\n const error = new Error('DeepSeek API key is not configured');\n error.statusCode = 503;\n throw error;\n }\n\n const model = normalizeDeepSeekModelName(options.model);\n const response = await fetch(`${deepseekBaseUrl}/chat/completions`, {\n method: 'POST',\n headers: {\n Accept: 'application/json',\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${deepseekApiKey}`,\n },\n body: JSON.stringify({\n model,\n messages,\n temperature: 0.4,\n max_tokens: normalizeMaxCompletionTokens(options.maxCompletionTokens),\n stream: false,\n }),\n signal: AbortSignal.timeout(60_000),\n });\n\n const bodyText = await response.text();\n let payload;\n try {\n payload = bodyText ? JSON.parse(bodyText) : {};\n } catch {\n payload = { error: bodyText };\n }\n\n if (!response.ok) {\n const upstreamMessage = payload?.error?.message || payload?.message || '';\n const friendlyMessage =\n response.status === 402 || /insufficient\\s+balance/i.test(upstreamMessage)\n ? '大乘 AI 服务额度暂不可用,请联系管理员充值 DeepSeek 账户后重试。'\n : upstreamMessage || `DeepSeek request failed: ${response.status}`;\n const error = new Error(friendlyMessage);\n error.statusCode = response.status;\n if (response.status === 402 || /insufficient\\s+balance/i.test(upstreamMessage)) {\n error.code = 'DEEPSEEK_PROVIDER_QUOTA';\n }\n throw error;\n }\n\n const message = payload?.choices?.[0]?.message?.content?.trim();\n if (!message) {\n const error = new Error('DeepSeek returned an empty response');\n error.statusCode = 502;\n throw error;\n }\n\n return {\n message,\n model,\n usage: {\n promptTokens: Number(payload?.usage?.prompt_tokens || 0),\n completionTokens: Number(payload?.usage?.completion_tokens || 0),\n totalTokens: Number(payload?.usage?.total_tokens || 0),\n },\n };\n}\n\nasync function callDeepSeekStream(messages, callbacks = {}) {\n if (!deepseekApiKey) {\n const error = new Error('DeepSeek API key is not configured');\n error.statusCode = 503;\n throw error;\n }\n\n const model = normalizeDeepSeekModelName(callbacks.model);\n const response = await fetch(`${deepseekBaseUrl}/chat/completions`, {\n method: 'POST',\n headers: {\n Accept: 'text/event-stream',\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${deepseekApiKey}`,\n },\n body: JSON.stringify({\n model,\n messages,\n temperature: 0.4,\n max_tokens: normalizeMaxCompletionTokens(callbacks.maxCompletionTokens),\n stream: true,\n stream_options: { include_usage: true },\n }),\n signal: callbacks.signal || AbortSignal.timeout(90_000),\n });\n\n if (!response.ok) {\n const bodyText = await response.text();\n let payload;\n try {\n payload = bodyText ? JSON.parse(bodyText) : {};\n } catch {\n payload = { error: bodyText };\n }\n const upstreamMessage = payload?.error?.message || payload?.message || '';\n const friendlyMessage =\n response.status === 402 || /insufficient\\s+balance/i.test(upstreamMessage)\n ? '大乘 AI 服务额度暂不可用,请联系管理员充值 DeepSeek 账户后重试。'\n : upstreamMessage || `DeepSeek request failed: ${response.status}`;\n const error = new Error(friendlyMessage);\n error.statusCode = response.status;\n if (response.status === 402 || /insufficient\\s+balance/i.test(upstreamMessage)) {\n error.code = 'DEEPSEEK_PROVIDER_QUOTA';\n }\n throw error;\n }\n\n let buffer = '';\n let message = '';\n let usage = {\n promptTokens: 0,\n completionTokens: 0,\n totalTokens: 0,\n };\n const decoder = new TextDecoder();\n\n for await (const chunk of response.body) {\n buffer += decoder.decode(chunk, { stream: true });\n const lines = buffer.split(/\\r?\\n/);\n buffer = lines.pop() || '';\n\n for (const rawLine of lines) {\n const line = rawLine.trim();\n if (!line.startsWith('data:')) continue;\n const data = line.slice('data:'.length).trim();\n if (!data || data === '[DONE]') continue;\n\n let payload;\n try {\n payload = JSON.parse(data);\n } catch {\n continue;\n }\n\n const delta = payload?.choices?.[0]?.delta?.content || '';\n if (delta) {\n message += delta;\n callbacks.onToken?.(delta);\n }\n\n if (payload?.usage) {\n usage = {\n promptTokens: Number(payload.usage.prompt_tokens || usage.promptTokens || 0),\n completionTokens: Number(payload.usage.completion_tokens || usage.completionTokens || 0),\n totalTokens: Number(payload.usage.total_tokens || usage.totalTokens || 0),\n };\n }\n }\n }\n\n message = message.trim();\n if (!message) {\n const error = new Error('DeepSeek returned an empty response');\n error.statusCode = 502;\n throw error;\n }\n\n return { message, model, usage };\n}\n\nfunction createCodexDeepSeekRuntime() {\n if (!enableCodexSdkChat || !deepseekApiKey) {\n return {\n enabled: false,\n provider: 'deepseek-direct',\n reason: enableCodexSdkChat ? 'DeepSeek API key is not configured' : 'ENABLE_CODEX_SDK_CHAT is not true',\n };\n }\n\n return {\n enabled: true,\n provider: 'codex-sdk-deepseek',\n options: {\n apiKey: deepseekApiKey,\n baseUrl: deepseekBaseUrl,\n env: {\n ...process.env,\n DEEPSEEK_API_KEY: deepseekApiKey,\n HOME: codexHomeDir,\n CODEX_HOME: codexHomeDir,\n TMPDIR: codexTempDir,\n XDG_RUNTIME_DIR: codexRuntimeDir,\n XDG_CACHE_HOME: path.join(codexHomeDir, '.cache'),\n XDG_CONFIG_HOME: path.join(codexHomeDir, '.config'),\n XDG_DATA_HOME: path.join(codexHomeDir, '.local', 'share'),\n PATH: process.env.PATH || '/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin',\n },\n config: {\n model_provider: codexDeepSeekProviderId,\n model_providers: {\n [codexDeepSeekProviderId]: {\n name: 'DeepSeek Chat Completions',\n base_url: codexResponsesBaseUrl,\n env_key: 'DEEPSEEK_API_KEY',\n wire_api: 'responses',\n query_params: {},\n },\n },\n },\n },\n threadOptions: {\n model: deepseekModel,\n approvalPolicy: 'never',\n sandboxMode: 'read-only',\n skipGitRepoCheck: true,\n networkAccessEnabled: true,\n },\n };\n}\n\nfunction codexPromptFromMessages(messages) {\n return messages\n .map((message) => {\n const role = String(message.role || 'user').toUpperCase();\n return `${role}:\\n${message.content}`;\n })\n .join('\\n\\n');\n}\n\nfunction responseContentText(content) {\n if (typeof content === 'string') return content;\n if (!Array.isArray(content)) return '';\n return content\n .map((part) => {\n if (typeof part === 'string') return part;\n if (!part || typeof part !== 'object') return '';\n return String(part.text || part.input_text || part.output_text || '');\n })\n .filter(Boolean)\n .join('\\n');\n}\n\nfunction codexResponsesPrompt(body) {\n const input = Array.isArray(body?.input) ? body.input : [];\n const userItems = input.filter((item) => item?.role === 'user');\n const source = userItems[userItems.length - 1] || input[input.length - 1];\n const prompt = responseContentText(source?.content).trim();\n if (prompt) return prompt;\n if (typeof body?.input === 'string') return body.input.trim();\n return '';\n}\n\nfunction writeResponsesEvent(res, event, data) {\n res.write(`event: ${event}\\n`);\n res.write(`data: ${JSON.stringify(data)}\\n\\n`);\n}\n\nfunction responseUsage(usage = {}) {\n return {\n input_tokens: Number(usage.promptTokens || usage.input_tokens || 0),\n cached_input_tokens: 0,\n output_tokens: Number(usage.completionTokens || usage.output_tokens || 0),\n reasoning_output_tokens: 0,\n total_tokens: Number(usage.totalTokens || usage.total_tokens || 0),\n };\n}\n\nfunction createLibreChatAgentRuntime() {\n if (!enableLibreChatAgentChat || !libreChatAgentApiKey || !libreChatAgentId) {\n return {\n enabled: false,\n provider: 'librechat-agent',\n reason: !enableLibreChatAgentChat\n ? 'ENABLE_LIBRECHAT_AGENT_CHAT is not true'\n : !libreChatAgentApiKey\n ? 'LIBRECHAT_AGENT_API_KEY is not configured'\n : 'LIBRECHAT_AGENT_ID is not configured',\n };\n }\n\n return {\n enabled: true,\n provider: 'librechat-agent',\n model: libreChatAgentId,\n chatCompletionsUrl: `${libreChatAgentsBaseUrl}/chat/completions`,\n };\n}\n\nfunction libreChatUsage(usage = {}) {\n return {\n promptTokens: Number(usage.prompt_tokens || usage.input_tokens || usage.promptTokens || 0),\n completionTokens: Number(usage.completion_tokens || usage.output_tokens || usage.completionTokens || 0),\n totalTokens: Number(usage.total_tokens || usage.totalTokens || 0),\n };\n}\n\nasync function callLibreChatAgent(messages) {\n const runtime = createLibreChatAgentRuntime();\n if (!runtime.enabled) {\n const error = new Error(runtime.reason || 'LibreChat Agent chat is not enabled');\n error.statusCode = 503;\n throw error;\n }\n\n const response = await fetch(runtime.chatCompletionsUrl, {\n method: 'POST',\n headers: {\n Accept: 'application/json',\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${libreChatAgentApiKey}`,\n },\n body: JSON.stringify({\n model: runtime.model,\n messages,\n temperature: 0.4,\n stream: false,\n }),\n signal: AbortSignal.timeout(120_000),\n });\n\n const bodyText = await response.text();\n let payload;\n try {\n payload = bodyText ? JSON.parse(bodyText) : {};\n } catch {\n payload = { error: bodyText };\n }\n\n if (!response.ok) {\n const upstreamMessage = payload?.error?.message || payload?.message || bodyText || '';\n const error = new Error(upstreamMessage || `LibreChat Agent request failed: ${response.status}`);\n error.statusCode = response.status;\n throw error;\n }\n\n const message = payload?.choices?.[0]?.message?.content?.trim();\n if (!message) {\n const error = new Error('LibreChat Agent returned an empty response');\n error.statusCode = 502;\n throw error;\n }\n\n return {\n message,\n usage: libreChatUsage(payload?.usage),\n };\n}\n\nasync function callLibreChatAgentStream(messages, callbacks = {}) {\n const runtime = createLibreChatAgentRuntime();\n if (!runtime.enabled) {\n const error = new Error(runtime.reason || 'LibreChat Agent chat is not enabled');\n error.statusCode = 503;\n throw error;\n }\n\n const response = await fetch(runtime.chatCompletionsUrl, {\n method: 'POST',\n headers: {\n Accept: 'text/event-stream',\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${libreChatAgentApiKey}`,\n },\n body: JSON.stringify({\n model: runtime.model,\n messages,\n temperature: 0.4,\n stream: true,\n }),\n signal: callbacks.signal || AbortSignal.timeout(180_000),\n });\n\n if (!response.ok) {\n const bodyText = await response.text();\n let payload;\n try {\n payload = bodyText ? JSON.parse(bodyText) : {};\n } catch {\n payload = { error: bodyText };\n }\n const upstreamMessage = payload?.error?.message || payload?.message || bodyText || '';\n const error = new Error(upstreamMessage || `LibreChat Agent request failed: ${response.status}`);\n error.statusCode = response.status;\n throw error;\n }\n\n let buffer = '';\n let message = '';\n let usage = { promptTokens: 0, completionTokens: 0, totalTokens: 0 };\n const decoder = new TextDecoder();\n\n for await (const chunk of response.body) {\n buffer += decoder.decode(chunk, { stream: true });\n const lines = buffer.split(/\\r?\\n/);\n buffer = lines.pop() || '';\n\n for (const rawLine of lines) {\n const line = rawLine.trim();\n if (!line.startsWith('data:')) continue;\n const data = line.slice('data:'.length).trim();\n if (!data || data === '[DONE]') continue;\n\n let payload;\n try {\n payload = JSON.parse(data);\n } catch {\n continue;\n }\n\n const delta = payload?.choices?.[0]?.delta?.content || '';\n if (delta) {\n message += delta;\n callbacks.onToken?.(delta);\n }\n\n if (payload?.usage) {\n usage = libreChatUsage(payload.usage);\n }\n }\n }\n\n message = message.trim();\n if (!message) {\n const error = new Error('LibreChat Agent returned an empty response');\n error.statusCode = 502;\n throw error;\n }\n\n return { message, usage };\n}\n\nfunction isDeepSeekQuotaError(error) {\n if (error?.code === 'DEEPSEEK_PROVIDER_QUOTA') return true;\n if (error?.code === 'USER_TOKEN_QUOTA_EXCEEDED' || error?.code === 'AI_MEMBERSHIP_REQUIRED') return false;\n const message = String(error?.message || error || '');\n return /insufficient\\s+balance|balance/i.test(message);\n}\n\nfunction publicAiErrorMessage(error) {\n if (isDeepSeekQuotaError(error)) {\n return '大乘 AI 服务额度暂不可用,请联系管理员充值 DeepSeek 账户后重试。';\n }\n return error?.message || 'Internal Server Error';\n}\n\nfunction responsesPayload({\n responseId,\n itemId,\n status,\n model,\n text = '',\n usage = null,\n error = null,\n}) {\n return {\n id: responseId,\n object: 'response',\n status,\n model,\n output:\n status === 'completed'\n ? [\n {\n id: itemId,\n type: 'message',\n role: 'assistant',\n status: 'completed',\n content: [{ type: 'output_text', text }],\n },\n ]\n : [],\n ...(usage ? { usage: responseUsage(usage) } : {}),\n ...(error\n ? {\n error: {\n message: error.message || String(error),\n type: 'server_error',\n },\n }\n : {}),\n };\n}\n\nasync function callCodexSdkDeepSeek(messages, callbacks = {}) {\n const codexRuntime = createCodexDeepSeekRuntime();\n if (!codexRuntime.enabled) {\n const error = new Error(codexRuntime.reason || 'Codex SDK chat is not enabled');\n error.statusCode = 503;\n throw error;\n }\n\n const { Codex } = await import('@openai/codex-sdk');\n const codex = new Codex(codexRuntime.options);\n const thread = codex.startThread(codexRuntime.threadOptions);\n const prompt = codexPromptFromMessages(messages);\n\n if (callbacks.onToken || callbacks.onStep) {\n const { events } = await thread.runStreamed(prompt, { signal: callbacks.signal });\n let finalResponse = '';\n let usage = null;\n\n for await (const event of events) {\n if (event.type === 'item.completed') {\n const item = event.item;\n if (item.type === 'agent_message') {\n finalResponse = item.text || finalResponse;\n callbacks.onToken?.(item.text || '');\n } else if (item.type === 'todo_list') {\n callbacks.onStep?.({\n title: 'Codex SDK 执行计划',\n message: item.items.map((todo) => `${todo.completed ? '✓' : '•'} ${todo.text}`).join('\\n'),\n });\n } else if (item.type === 'command_execution') {\n callbacks.onStep?.({\n title: 'Codex SDK 执行命令',\n message: item.command,\n });\n } else if (item.type === 'mcp_tool_call') {\n callbacks.onStep?.({\n title: 'Codex SDK 调用工具',\n message: `${item.server}.${item.tool}`,\n });\n }\n } else if (event.type === 'turn.completed') {\n usage = event.usage;\n } else if (event.type === 'turn.failed') {\n throw new Error(event.error?.message || 'Codex SDK turn failed');\n } else if (event.type === 'error') {\n throw new Error(event.message || 'Codex SDK stream failed');\n }\n }\n\n if (!finalResponse.trim()) {\n const error = new Error('Codex SDK returned an empty response');\n error.statusCode = 502;\n throw error;\n }\n\n return {\n message: finalResponse.trim(),\n usage: {\n promptTokens: Number(usage?.input_tokens || 0),\n completionTokens: Number(usage?.output_tokens || 0),\n totalTokens: Number((usage?.input_tokens || 0) + (usage?.output_tokens || 0)),\n },\n };\n }\n\n const turn = await thread.run(prompt);\n if (!turn.finalResponse?.trim()) {\n const error = new Error('Codex SDK returned an empty response');\n error.statusCode = 502;\n throw error;\n }\n\n return {\n message: turn.finalResponse.trim(),\n usage: {\n promptTokens: Number(turn.usage?.input_tokens || 0),\n completionTokens: Number(turn.usage?.output_tokens || 0),\n totalTokens: Number((turn.usage?.input_tokens || 0) + (turn.usage?.output_tokens || 0)),\n },\n };\n}\n\nconst activeAgentRuns = new Map();\n\nfunction normalizeAgentMode(value) {\n const mode = safeUserText(value || 'dharma_guide');\n return /^[a-z][a-z0-9_:-]{0,48}$/i.test(mode) ? mode : 'dharma_guide';\n}\n\nfunction openClawSessionKey(user, conversationId) {\n const env = process.env.NODE_ENV === 'production' ? 'prod' : 'dev';\n return `fabushi:env:${env}:user:${user.userId}:conversation:${conversationId}`;\n}\n\nfunction createOpenClawRuntime() {\n if (!enableOpenClawAgentChat || !openClawGatewayToken || !openClawGatewayUrl) {\n return {\n enabled: false,\n provider: 'openclaw-gateway',\n reason: !enableOpenClawAgentChat\n ? 'ENABLE_OPENCLAW_AGENT_CHAT is not true'\n : !openClawGatewayToken\n ? 'OPENCLAW_GATEWAY_TOKEN is not configured'\n : 'OPENCLAW_GATEWAY_URL is not configured',\n };\n }\n\n return {\n enabled: true,\n provider: 'openclaw-gateway',\n model: openClawAgentId,\n runsUrl: openClawRunsEndpoint || `${openClawGatewayUrl}/v1/agents/${encodeURIComponent(openClawAgentId)}/runs`,\n };\n}\n\nfunction extractOpenClawText(payload) {\n if (!payload || typeof payload !== 'object') return '';\n const candidates = [\n payload.message,\n payload.content,\n payload.output_text,\n payload.finalResponse,\n payload.final_response,\n payload.result?.message,\n payload.result?.content,\n payload.output?.message,\n payload.output?.content,\n payload.choices?.[0]?.message?.content,\n ];\n for (const candidate of candidates) {\n if (typeof candidate === 'string' && candidate.trim()) return candidate.trim();\n }\n return '';\n}\n\nfunction extractOpenClawUsage(payload) {\n const usage = payload?.usage || payload?.result?.usage || {};\n return {\n promptTokens: Number(usage.input_tokens || usage.prompt_tokens || usage.promptTokens || 0),\n completionTokens: Number(usage.output_tokens || usage.completion_tokens || usage.completionTokens || 0),\n totalTokens: Number(usage.total_tokens || usage.totalTokens || 0),\n };\n}\n\nasync function callOpenClawAgent({ messages, user, conversationId, mode, signal }) {\n const runtime = createOpenClawRuntime();\n if (!runtime.enabled) {\n const error = new Error(runtime.reason || 'OpenClaw Agent chat is not enabled');\n error.statusCode = 503;\n throw error;\n }\n\n const sessionKey = openClawSessionKey(user, conversationId);\n const response = await fetch(runtime.runsUrl, {\n method: 'POST',\n headers: {\n Accept: 'application/json',\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${openClawGatewayToken}`,\n },\n body: JSON.stringify({\n agentId: openClawAgentId,\n sessionKey,\n mode,\n stream: false,\n input: { messages },\n messages,\n metadata: {\n app: 'fabushi',\n userId: user.userId,\n conversationId,\n toolPolicy: 'public-user-read-mostly',\n },\n }),\n signal: signal || AbortSignal.timeout(180_000),\n });\n\n const bodyText = await response.text();\n let payload;\n try {\n payload = bodyText ? JSON.parse(bodyText) : {};\n } catch {\n payload = { message: bodyText };\n }\n\n if (!response.ok) {\n const error = new Error(payload?.error?.message || payload?.message || `OpenClaw request failed: ${response.status}`);\n error.statusCode = response.status;\n throw error;\n }\n\n const message = extractOpenClawText(payload);\n if (!message) {\n const error = new Error('OpenClaw returned an empty response');\n error.statusCode = 502;\n throw error;\n }\n\n return {\n message,\n runId: payload.runId || payload.id || payload.result?.runId || null,\n provider: runtime.provider,\n model: runtime.model,\n usage: extractOpenClawUsage(payload),\n };\n}\n\nasync function runAgentModel({ messages, user, conversationId, mode, callbacks = {}, signal, maxCompletionTokens }) {\n const openClawRuntime = createOpenClawRuntime();\n if (openClawRuntime.enabled && !callbacks.onToken) {\n return await callOpenClawAgent({ messages, user, conversationId, mode, signal });\n }\n\n const codexRuntime = createCodexDeepSeekRuntime();\n const libreChatRuntime = createLibreChatAgentRuntime();\n let provider = libreChatRuntime.enabled\n ? libreChatRuntime.provider\n : codexRuntime.enabled\n ? codexRuntime.provider\n : 'deepseek';\n let model = libreChatRuntime.enabled ? libreChatRuntime.model : deepseekModel;\n\n try {\n if (libreChatRuntime.enabled) {\n const result = callbacks.onToken\n ? await callLibreChatAgentStream(messages, { ...callbacks, signal })\n : await callLibreChatAgent(messages);\n return { ...result, provider, model };\n }\n if (codexRuntime.enabled) {\n const result = await callCodexSdkDeepSeek(messages, { ...callbacks, signal });\n return { ...result, provider, model };\n }\n const result = callbacks.onToken\n ? await callDeepSeekStream(messages, { ...callbacks, signal, maxCompletionTokens })\n : await callDeepSeek(messages, { maxCompletionTokens });\n return { ...result, provider, model };\n } catch (error) {\n if (!codexRuntime.enabled || isDeepSeekQuotaError(error)) throw error;\n logger.warn({ error: String(error) }, 'Agent model failed, falling back to DeepSeek direct');\n const result = callbacks.onToken\n ? await callDeepSeekStream(messages, { ...callbacks, signal, maxCompletionTokens })\n : await callDeepSeek(messages, { maxCompletionTokens });\n return { ...result, provider: 'deepseek', model: deepseekModel };\n }\n}\n\nfunction updateAgentRunStatus(run, status, fields = {}) {\n statements.updateAgentRunStatus.run({\n id: run.id,\n userId: run.user_id || run.userId,\n status,\n openClawRunId: fields.openClawRunId ?? null,\n inputTokens: fields.inputTokens ?? null,\n outputTokens: fields.outputTokens ?? null,\n toolCallCount: fields.toolCallCount ?? null,\n completedAt: status === 'completed' || status === 'cancelled' ? nowIso() : null,\n failedAt: status === 'failed' ? nowIso() : null,\n errorCode: fields.errorCode ?? null,\n errorMessage: fields.errorMessage ?? null,\n });\n}\n\nfunction buildAgentMessages(conversationId) {\n const historyRows = statements.listMessages.all(conversationId).slice(-14);\n return [{ role: 'system', content: systemPrompt }, ...normalizeMessages(historyRows)];\n}\n\nfunction agentUsagePayload(usage, user, budget) {\n const promptTokens = usage.promptTokens || usage.inputTokens || 0;\n const completionTokens = usage.completionTokens || usage.outputTokens || 0;\n const totalTokens = usage.totalTokens || promptTokens + completionTokens;\n const latestUsage = usageFor(user.userId);\n return {\n inputTokens: promptTokens,\n outputTokens: completionTokens,\n totalTokens,\n monthlyLimit: budget.limit,\n remainingTokens: Math.max(0, budget.limit - latestUsage.used),\n };\n}\n\nfunction openAiContentToText(content) {\n if (typeof content === 'string') return content;\n if (Array.isArray(content)) {\n return content\n .map((part) => {\n if (typeof part === 'string') return part;\n if (!part || typeof part !== 'object') return '';\n return readText(part.text || part.content || part.value);\n })\n .filter(Boolean)\n .join('\\n');\n }\n return readText(content);\n}\n\nfunction normalizeOpenAiMessages(value) {\n if (!Array.isArray(value)) return [];\n const allowedRoles = new Set(['system', 'user', 'assistant', 'tool']);\n return value\n .map((item) => {\n if (!item || typeof item !== 'object') return null;\n const role = allowedRoles.has(item.role) ? item.role : 'user';\n const content = openAiContentToText(item.content);\n if (!content) return null;\n return { role, content };\n })\n .filter(Boolean);\n}\n\nfunction truncateForDeepSeekMessage(text, maxChars = 12_000) {\n const value = readText(text);\n if (value.length <= maxChars) return value;\n return `${value.slice(0, maxChars)}\\n\\n[内容已截断]`;\n}\n\nfunction truncateTailForDeepSeekMessage(text, maxChars = 2400) {\n const value = readText(text);\n if (value.length <= maxChars) return value;\n return `[前文已省略]\\n\\n${value.slice(-maxChars)}`;\n}\n\nfunction stripOpenClawPromptNoise(text) {\n const value = readText(text);\n if (!value) return '';\n\n const finalPromptMatch = value.match(/finalPromptText[\"'\\s::]+([\\s\\S]{1,4000})$/i);\n if (finalPromptMatch?.[1]) {\n return readText(finalPromptMatch[1].replace(/^[\"'`]+|[\"'`,}\\]]+$/g, ''));\n }\n\n const markers = [\n 'final prompt:',\n 'final user prompt:',\n 'user prompt:',\n '用户问题:',\n '用户消息:',\n '用户输入:',\n ];\n const lower = value.toLowerCase();\n for (const marker of markers) {\n const index = lower.lastIndexOf(marker.toLowerCase());\n if (index >= 0) {\n const tail = value.slice(index + marker.length).trim();\n if (tail) return tail;\n }\n }\n\n return value;\n}\n\nfunction compactOpenClawMessagesForDeepSeek(messages) {\n const conversationalMessages = messages\n .filter((message) => message.role === 'user' || message.role === 'assistant')\n .map((message) => ({\n role: message.role,\n content: truncateTailForDeepSeekMessage(stripOpenClawPromptNoise(message.content)),\n }))\n .filter((message) => message.content);\n\n const recent = conversationalMessages.slice(-6);\n while (recent.length > 0 && recent[0].role !== 'user') {\n recent.shift();\n }\n\n if (recent.length === 0) {\n const fallback = messages\n .slice()\n .reverse()\n .map((message) => truncateTailForDeepSeekMessage(stripOpenClawPromptNoise(message.content), 2400))\n .find(Boolean);\n if (!fallback) return messages;\n recent.push({ role: 'user', content: fallback });\n }\n\n return [\n {\n role: 'system',\n content: '你是大乘 App 的桌面 AI 助手。请直接回答用户问题,默认使用简洁、准确的中文。',\n },\n ...recent,\n ];\n}\n\nfunction isDeepSeekBlockedError(error) {\n return (\n Number(error?.statusCode) === 403 &&\n /blocked|被拦截|forbidden/i.test(String(error?.message || error || ''))\n );\n}\n\nfunction shouldRetryWithCompactedOpenClawMessages(error, messages, compactedMessages) {\n if (!isDeepSeekBlockedError(error)) return false;\n return JSON.stringify(messages) !== JSON.stringify(compactedMessages);\n}\n\nfunction usageWithFallback(usage, messages, text) {\n const promptTokens = Number(usage?.promptTokens || usage?.inputTokens || 0);\n const completionTokens = Number(usage?.completionTokens || usage?.outputTokens || 0);\n const totalTokens = Number(usage?.totalTokens || 0);\n if (totalTokens > 0) {\n return {\n promptTokens,\n completionTokens,\n totalTokens,\n };\n }\n const estimatedPrompt = promptTokens || estimateTokens(JSON.stringify(messages));\n const estimatedCompletion = completionTokens || estimateTokens(text);\n return {\n promptTokens: estimatedPrompt,\n completionTokens: estimatedCompletion,\n totalTokens: estimatedPrompt + estimatedCompletion,\n };\n}\n\nfunction openAiUsagePayload(usage, user, budget) {\n const promptTokens = Number(usage.promptTokens || 0);\n const completionTokens = Number(usage.completionTokens || 0);\n const totalTokens = Number(usage.totalTokens || promptTokens + completionTokens);\n const latestUsage = usageFor(user.userId);\n const remainingTokens = Math.max(0, budget.limit - latestUsage.used);\n return {\n prompt_tokens: promptTokens,\n completion_tokens: completionTokens,\n total_tokens: totalTokens,\n promptTokens,\n completionTokens,\n totalTokens,\n monthlyLimit: budget.limit,\n remainingTokens,\n };\n}\n\nfunction writeOpenAiSse(res, payload) {\n res.write(`data: ${JSON.stringify(payload)}\\n\\n`);\n}\n\napp.post(\n '/api/openclaw/deepseek/v1/chat/completions',\n asyncHandler(async (req, res) => {\n const user = await resolveUser(req, req.body || {});\n const incomingMessages = normalizeOpenAiMessages(req.body?.messages);\n if (incomingMessages.length === 0) {\n res.status(400).json({ error: { message: 'messages is required' } });\n return;\n }\n\n const messages = compactOpenClawMessagesForDeepSeek(incomingMessages);\n const promptEstimate = estimateTokens(JSON.stringify(messages));\n const estimated = promptEstimate + 600;\n const budget = enforceTokenBudget(user, estimated);\n const maxCompletionTokens = Math.min(\n completionBudgetFor(budget, promptEstimate),\n normalizeMaxCompletionTokens(req.body?.max_tokens),\n );\n const model = normalizeDeepSeekModelName(req.body?.model);\n const wantsStream =\n req.body?.stream === true ||\n String(req.get('accept') || '').includes('text/event-stream');\n const id = `chatcmpl_${crypto.randomUUID().replaceAll('-', '')}`;\n const created = Math.floor(Date.now() / 1000);\n const compactedMessages = messages;\n\n if (!wantsStream) {\n let result;\n let usageMessages = messages;\n try {\n result = await callDeepSeek(messages, { model, maxCompletionTokens });\n } catch (error) {\n if (!shouldRetryWithCompactedOpenClawMessages(error, messages, compactedMessages)) {\n throw error;\n }\n logger.warn(\n {\n statusCode: error?.statusCode,\n messageCount: messages.length,\n compactedMessageCount: compactedMessages.length,\n },\n 'OpenClaw DeepSeek proxy retrying with compacted messages',\n );\n usageMessages = compactedMessages;\n result = await callDeepSeek(compactedMessages, { model, maxCompletionTokens });\n }\n const usage = usageWithFallback(result.usage, usageMessages, result.message);\n recordUsage(user.userId, usage.totalTokens);\n res.json({\n id,\n object: 'chat.completion',\n created,\n model: result.model || model,\n choices: [\n {\n index: 0,\n message: { role: 'assistant', content: result.message },\n finish_reason: 'stop',\n },\n ],\n usage: openAiUsagePayload(usage, user, budget),\n });\n return;\n }\n\n const abortController = new AbortController();\n req.on('close', () => abortController.abort());\n\n res.status(200);\n res.set({\n 'Content-Type': 'text/event-stream; charset=utf-8',\n 'Cache-Control': 'no-cache, no-transform',\n Connection: 'keep-alive',\n 'X-Accel-Buffering': 'no',\n });\n res.flushHeaders?.();\n\n const baseChunk = {\n id,\n object: 'chat.completion.chunk',\n created,\n model,\n system_fingerprint: 'fabushi-openclaw-deepseek-proxy',\n };\n writeOpenAiSse(res, {\n ...baseChunk,\n choices: [{ index: 0, delta: { role: 'assistant' }, finish_reason: null }],\n });\n\n let text = '';\n let usageMessages = messages;\n try {\n let result;\n try {\n result = await callDeepSeekStream(messages, {\n model,\n maxCompletionTokens,\n signal: abortController.signal,\n onToken: (delta) => {\n text += delta;\n writeOpenAiSse(res, {\n ...baseChunk,\n choices: [{ index: 0, delta: { content: delta }, finish_reason: null }],\n });\n },\n });\n } catch (error) {\n if (!shouldRetryWithCompactedOpenClawMessages(error, messages, compactedMessages)) {\n throw error;\n }\n logger.warn(\n {\n statusCode: error?.statusCode,\n messageCount: messages.length,\n compactedMessageCount: compactedMessages.length,\n },\n 'OpenClaw DeepSeek proxy stream retrying with compacted messages',\n );\n text = '';\n usageMessages = compactedMessages;\n result = await callDeepSeekStream(compactedMessages, {\n model,\n maxCompletionTokens,\n signal: abortController.signal,\n onToken: (delta) => {\n text += delta;\n writeOpenAiSse(res, {\n ...baseChunk,\n choices: [{ index: 0, delta: { content: delta }, finish_reason: null }],\n });\n },\n });\n }\n text = result.message || text;\n const usage = usageWithFallback(result.usage, usageMessages, text);\n recordUsage(user.userId, usage.totalTokens);\n writeOpenAiSse(res, {\n ...baseChunk,\n model: result.model || model,\n choices: [{ index: 0, delta: {}, finish_reason: 'stop' }],\n });\n writeOpenAiSse(res, {\n ...baseChunk,\n model: result.model || model,\n choices: [],\n usage: openAiUsagePayload(usage, user, budget),\n });\n res.write('data: [DONE]\\n\\n');\n } catch (error) {\n logger.error(\n { error: String(error), code: error?.code, statusCode: error?.statusCode },\n 'OpenClaw DeepSeek proxy stream failed',\n );\n writeOpenAiSse(res, {\n ...baseChunk,\n choices: [],\n error: {\n message: publicAiErrorMessage(error),\n code: error?.code || 'OPENCLAW_DEEPSEEK_PROXY_ERROR',\n },\n });\n res.write('data: [DONE]\\n\\n');\n } finally {\n res.end();\n }\n }),\n);\n\napp.post(\n '/codex-deepseek/v1/responses',\n asyncHandler(async (req, res) => {\n const bearer = bearerToken(req);\n if (!deepseekApiKey || bearer !== deepseekApiKey) {\n res.status(401).json({\n error: { message: 'Unauthorized Codex DeepSeek adapter request' },\n });\n return;\n }\n\n const prompt = codexResponsesPrompt(req.body);\n if (!prompt) {\n res.status(400).json({ error: { message: 'Responses input is required' } });\n return;\n }\n\n const model = String(req.body?.model || deepseekModel);\n const responseId = `resp_${crypto.randomUUID().replaceAll('-', '')}`;\n const itemId = `msg_${crypto.randomUUID().replaceAll('-', '')}`;\n const wantsStream =\n req.body?.stream !== false ||\n String(req.get('accept') || '').includes('text/event-stream');\n const messages = [{ role: 'user', content: prompt }];\n\n if (!wantsStream) {\n const result = await callDeepSeek(messages);\n res.json(\n responsesPayload({\n responseId,\n itemId,\n status: 'completed',\n model,\n text: result.message,\n usage: result.usage,\n }),\n );\n return;\n }\n\n res.status(200);\n res.set({\n 'Content-Type': 'text/event-stream; charset=utf-8',\n 'Cache-Control': 'no-cache, no-transform',\n Connection: 'keep-alive',\n 'X-Accel-Buffering': 'no',\n });\n res.flushHeaders?.();\n\n writeResponsesEvent(res, 'response.created', {\n type: 'response.created',\n response: responsesPayload({\n responseId,\n itemId,\n status: 'in_progress',\n model,\n }),\n });\n writeResponsesEvent(res, 'response.output_item.added', {\n type: 'response.output_item.added',\n output_index: 0,\n item: {\n id: itemId,\n type: 'message',\n role: 'assistant',\n status: 'in_progress',\n content: [],\n },\n });\n writeResponsesEvent(res, 'response.content_part.added', {\n type: 'response.content_part.added',\n item_id: itemId,\n output_index: 0,\n content_index: 0,\n part: { type: 'output_text', text: '' },\n });\n\n let text = '';\n try {\n const result = await callDeepSeekStream(messages, {\n onToken: (delta) => {\n text += delta;\n writeResponsesEvent(res, 'response.output_text.delta', {\n type: 'response.output_text.delta',\n item_id: itemId,\n output_index: 0,\n content_index: 0,\n delta,\n });\n },\n });\n text = result.message || text;\n writeResponsesEvent(res, 'response.output_text.done', {\n type: 'response.output_text.done',\n item_id: itemId,\n output_index: 0,\n content_index: 0,\n text,\n });\n writeResponsesEvent(res, 'response.content_part.done', {\n type: 'response.content_part.done',\n item_id: itemId,\n output_index: 0,\n content_index: 0,\n part: { type: 'output_text', text },\n });\n writeResponsesEvent(res, 'response.output_item.done', {\n type: 'response.output_item.done',\n output_index: 0,\n item: {\n id: itemId,\n type: 'message',\n role: 'assistant',\n status: 'completed',\n content: [{ type: 'output_text', text }],\n },\n });\n writeResponsesEvent(res, 'response.completed', {\n type: 'response.completed',\n response: responsesPayload({\n responseId,\n itemId,\n status: 'completed',\n model,\n text,\n usage: result.usage,\n }),\n });\n res.end();\n } catch (error) {\n writeResponsesEvent(res, 'response.failed', {\n type: 'response.failed',\n response: responsesPayload({\n responseId,\n itemId,\n status: 'failed',\n model,\n text,\n error,\n }),\n });\n res.end();\n }\n }),\n);\n\napp.use(\n '/api/openclaw/runtime/files',\n express.static(openClawRuntimeUpdatesDir, {\n fallthrough: false,\n immutable: true,\n maxAge: '7d',\n setHeaders(res) {\n res.setHeader('X-Content-Type-Options', 'nosniff');\n },\n }),\n);\n\napp.get(\n '/api/openclaw/runtime/manifest',\n asyncHandler(async (req, res) => {\n const platform = readText(req.query.platform);\n const currentVersion = readText(req.query.currentVersion);\n const appVersion = readText(req.query.appVersion);\n\n let manifest;\n try {\n manifest = await readJsonFile(openClawRuntimeManifestPath);\n } catch (error) {\n logger.info(\n { manifestPath: openClawRuntimeManifestPath, error: String(error) },\n 'OpenClaw runtime manifest unavailable',\n );\n return jsonResponse(res, 200, {\n success: true,\n schema: 1,\n updateAvailable: false,\n reason: 'manifest-unavailable',\n platform,\n currentVersion,\n appVersion,\n });\n }\n\n const latest = manifest.latest && typeof manifest.latest === 'object'\n ? manifest.latest\n : manifest;\n const platforms = latest.platforms && typeof latest.platforms === 'object'\n ? latest.platforms\n : {};\n const platformRelease = platform ? platforms[platform] : null;\n const version = readText(latest.version || manifest.version);\n\n if (!platform || !platformRelease || typeof platformRelease !== 'object') {\n return jsonResponse(res, 200, {\n success: true,\n schema: Number(manifest.schema || 1),\n channel: readText(manifest.channel || latest.channel || 'stable'),\n updateAvailable: false,\n reason: platform ? 'platform-unavailable' : 'platform-required',\n platform,\n currentVersion,\n appVersion,\n latestVersion: version,\n });\n }\n\n const downloadUrl = absolutizeOpenClawRuntimeUrl(\n req,\n platformRelease.archiveUrl || platformRelease.url || platformRelease.fileName,\n );\n const release = {\n version,\n channel: readText(manifest.channel || latest.channel || 'stable'),\n minAppVersion: readText(latest.minAppVersion || platformRelease.minAppVersion),\n notes: readText(latest.notes || platformRelease.notes),\n nodeExecutable: readText(\n platformRelease.nodeExecutable || latest.nodeExecutable || 'node/bin/node',\n ),\n cliEntrypoint: readText(\n platformRelease.cliEntrypoint || latest.cliEntrypoint || 'openclaw/openclaw.mjs',\n ),\n defaultPort: Number(latest.defaultPort || manifest.defaultPort || 18789),\n defaultModel: readText(latest.defaultModel || manifest.defaultModel || 'openclaw/default'),\n defaultModelOverride: readText(latest.defaultModelOverride || manifest.defaultModelOverride),\n gatewayArgs: Array.isArray(latest.gatewayArgs)\n ? latest.gatewayArgs.map((item) => String(item))\n : ['gateway', '--port', '{port}', '--force'],\n archiveUrl: downloadUrl,\n sha256: readText(platformRelease.sha256),\n size: Number(platformRelease.size || 0),\n };\n\n jsonResponse(res, 200, {\n success: true,\n schema: Number(manifest.schema || 1),\n updateAvailable: Boolean(version && downloadUrl && version !== currentVersion),\n platform,\n currentVersion,\n appVersion,\n latest: release,\n generatedAt: readText(manifest.generatedAt || latest.generatedAt),\n });\n }),\n);\n\napp.get('/health', (_req, res) => {\n const codexRuntime = createCodexDeepSeekRuntime();\n const libreChatRuntime = createLibreChatAgentRuntime();\n const openClawRuntime = createOpenClawRuntime();\n jsonResponse(res, 200, {\n status: 'ok',\n service: 'dacheng-ai-backend',\n provider: libreChatRuntime.enabled ? libreChatRuntime.provider : 'deepseek',\n model: libreChatRuntime.enabled ? libreChatRuntime.model : deepseekModel,\n libreChatAgentChatEnabled: libreChatRuntime.enabled,\n libreChatAgentProvider: libreChatRuntime.provider,\n libreChatAgentReason: libreChatRuntime.enabled ? undefined : libreChatRuntime.reason,\n codexSdkAvailable: true,\n codexSdkChatEnabled: codexRuntime.enabled,\n codexSdkProvider: codexRuntime.provider,\n openClawAgentChatEnabled: openClawRuntime.enabled,\n openClawAgentProvider: openClawRuntime.provider,\n openClawAgentReason: openClawRuntime.enabled ? undefined : openClawRuntime.reason,\n requireMemberForAi,\n memberMonthlyLimit,\n freeMonthlyLimit,\n deepseekMaxCompletionTokens,\n openClawRuntimeUpdatesDir,\n openClawRuntimeManifestConfigured: fs.existsSync(openClawRuntimeManifestPath),\n cbetaApiRoot,\n timestamp: nowIso(),\n });\n});\n\napp.get(\n '/api/ai/quota',\n asyncHandler(async (req, res) => {\n const user = await resolveUser(req, { username: req.query.username });\n const budget = enforceTokenBudget(user, 0);\n const usage = usageFor(user.userId);\n jsonResponse(res, 200, {\n success: true,\n isMember: user.isMember,\n monthlyLimit: budget.limit,\n usedTokens: usage.used,\n remainingTokens: Math.max(0, budget.limit - usage.used),\n month: usage.month,\n });\n }),\n);\n\napp.post(\n '/api/agent/chat',\n asyncHandler(async (req, res) => {\n const message = safeUserText(req.body?.message);\n if (!message) {\n return jsonResponse(res, 400, { success: false, message: 'message is required' });\n }\n\n const user = await resolveUser(req, req.body || {});\n const promptEstimate = estimateTokens(message);\n const estimated = promptEstimate + 600;\n const budget = enforceTokenBudget(user, estimated);\n const maxCompletionTokens = completionBudgetFor(budget, promptEstimate);\n const createdAt = nowIso();\n const conversationId = safeUserText(req.body?.conversationId) || crypto.randomUUID();\n const mode = normalizeAgentMode(req.body?.mode);\n const stream = req.body?.stream === true;\n let conversation = statements.getConversation.get(conversationId, user.userId);\n const isNewConversation = !conversation;\n\n if (isNewConversation) {\n conversation = { id: conversationId, user_id: user.userId, title: titleFromMessage(message) };\n statements.insertConversation.run({\n id: conversationId,\n userId: user.userId,\n title: conversation.title,\n provider: 'agent',\n model: openClawAgentId,\n createdAt,\n updatedAt: createdAt,\n });\n }\n\n const messageId = safeUserText(req.body?.messageId) || crypto.randomUUID();\n statements.insertMessage.run({\n id: messageId,\n conversationId,\n role: 'user',\n content: message,\n promptTokens: 0,\n completionTokens: 0,\n totalTokens: 0,\n createdAt,\n });\n\n const runId = `run_${crypto.randomUUID().replaceAll('-', '')}`;\n const sessionKey = openClawSessionKey(user, conversationId);\n const openClawRuntime = createOpenClawRuntime();\n statements.insertAgentRun.run({\n id: runId,\n userId: user.userId,\n conversationId,\n messageId,\n openClawRunId: null,\n openClawSessionKey: sessionKey,\n status: stream ? 'queued' : 'running',\n mode,\n provider: openClawRuntime.enabled ? openClawRuntime.provider : 'dacheng-ai-fallback',\n model: openClawRuntime.enabled ? openClawAgentId : deepseekModel,\n startedAt: createdAt,\n });\n\n if (stream) {\n return jsonResponse(res, 200, {\n success: true,\n runId,\n conversationId,\n streamUrl: `/api/agent/runs/${runId}/events`,\n });\n }\n\n const controller = new AbortController();\n activeAgentRuns.set(runId, controller);\n try {\n const modelMessages = buildAgentMessages(conversationId);\n const aiResult = await runAgentModel({\n messages: modelMessages,\n user,\n conversationId,\n mode,\n signal: controller.signal,\n maxCompletionTokens,\n });\n const usage = {\n promptTokens: aiResult.usage?.promptTokens || estimateTokens(JSON.stringify(modelMessages)),\n completionTokens: aiResult.usage?.completionTokens || estimateTokens(aiResult.message),\n totalTokens:\n aiResult.usage?.totalTokens ||\n estimateTokens(JSON.stringify(modelMessages)) + estimateTokens(aiResult.message),\n };\n recordUsage(user.userId, usage.totalTokens);\n const answeredAt = nowIso();\n const assistantMessageId = crypto.randomUUID();\n statements.insertMessage.run({\n id: assistantMessageId,\n conversationId,\n role: 'assistant',\n content: aiResult.message,\n promptTokens: usage.promptTokens,\n completionTokens: usage.completionTokens,\n totalTokens: usage.totalTokens,\n createdAt: answeredAt,\n });\n statements.updateConversation.run({\n id: conversationId,\n userId: user.userId,\n title: isNewConversation ? titleFromMessage(message) : conversation.title,\n updatedAt: answeredAt,\n });\n updateAgentRunStatus({ id: runId, user_id: user.userId }, 'completed', {\n openClawRunId: aiResult.runId,\n inputTokens: usage.promptTokens,\n outputTokens: usage.completionTokens,\n });\n return jsonResponse(res, 200, {\n success: true,\n runId,\n conversationId,\n messageId: assistantMessageId,\n provider: aiResult.provider,\n model: aiResult.model,\n message: aiResult.message,\n usage: agentUsagePayload(usage, user, budget),\n });\n } catch (error) {\n const status = error.name === 'AbortError' ? 'cancelled' : 'failed';\n updateAgentRunStatus({ id: runId, user_id: user.userId }, status, {\n errorCode: error.name || 'AGENT_RUN_FAILED',\n errorMessage: publicAiErrorMessage(error),\n });\n if (status === 'cancelled') {\n return jsonResponse(res, 499, { success: false, runId, status: 'cancelled' });\n }\n throw error;\n } finally {\n activeAgentRuns.delete(runId);\n }\n }),\n);\n\napp.get(\n '/api/agent/runs/:runId/events',\n asyncHandler(async (req, res) => {\n const user = await resolveUser(req, {});\n const run = statements.getAgentRun.get(req.params.runId, user.userId);\n if (!run) {\n return jsonResponse(res, 404, { success: false, message: 'run not found' });\n }\n if (run.status === 'completed' || run.status === 'failed' || run.status === 'cancelled') {\n return jsonResponse(res, 409, { success: false, message: `run is already ${run.status}` });\n }\n\n sseHeaders(res);\n const controller = new AbortController();\n activeAgentRuns.set(run.id, controller);\n updateAgentRunStatus(run, 'running');\n writeSse(res, 'run.started', { runId: run.id, conversationId: run.conversation_id });\n\n try {\n let streamedText = '';\n const modelMessages = buildAgentMessages(run.conversation_id);\n const promptEstimate = estimateTokens(JSON.stringify(modelMessages));\n const estimated = promptEstimate + 600;\n const budget = enforceTokenBudget(user, estimated);\n const maxCompletionTokens = completionBudgetFor(budget, promptEstimate);\n const aiResult = await runAgentModel({\n messages: modelMessages,\n user,\n conversationId: run.conversation_id,\n mode: run.mode,\n signal: controller.signal,\n maxCompletionTokens,\n callbacks: {\n onStep: (step) => writeSse(res, 'tool.call.completed', {\n tool: 'agent.step',\n displayName: step.title || 'Agent step',\n summary: step.message || '',\n }),\n onToken: (delta) => {\n streamedText += delta;\n writeSse(res, 'assistant.delta', { text: delta });\n },\n },\n });\n const message = aiResult.message || streamedText.trim();\n const usage = {\n promptTokens: aiResult.usage?.promptTokens || estimateTokens(JSON.stringify(modelMessages)),\n completionTokens: aiResult.usage?.completionTokens || estimateTokens(message),\n totalTokens:\n aiResult.usage?.totalTokens || estimateTokens(JSON.stringify(modelMessages)) + estimateTokens(message),\n };\n recordUsage(user.userId, usage.totalTokens);\n const assistantMessageId = crypto.randomUUID();\n const answeredAt = nowIso();\n statements.insertMessage.run({\n id: assistantMessageId,\n conversationId: run.conversation_id,\n role: 'assistant',\n content: message,\n promptTokens: usage.promptTokens,\n completionTokens: usage.completionTokens,\n totalTokens: usage.totalTokens,\n createdAt: answeredAt,\n });\n statements.updateConversation.run({\n id: run.conversation_id,\n userId: user.userId,\n title: titleFromMessage(message),\n updatedAt: answeredAt,\n });\n updateAgentRunStatus(run, 'completed', {\n openClawRunId: aiResult.runId,\n inputTokens: usage.promptTokens,\n outputTokens: usage.completionTokens,\n });\n writeSse(res, 'assistant.message', { messageId: assistantMessageId, content: message });\n writeSse(res, 'run.completed', { usage: agentUsagePayload(usage, user, budget) });\n res.end();\n } catch (error) {\n const status = error.name === 'AbortError' ? 'cancelled' : 'failed';\n updateAgentRunStatus(run, status, {\n errorCode: error.name || 'AGENT_RUN_FAILED',\n errorMessage: publicAiErrorMessage(error),\n });\n writeSse(res, status === 'cancelled' ? 'run.cancelled' : 'run.failed', {\n runId: run.id,\n message: publicAiErrorMessage(error),\n });\n res.end();\n } finally {\n activeAgentRuns.delete(run.id);\n }\n }),\n);\n\napp.post(\n '/api/agent/runs/:runId/cancel',\n asyncHandler(async (req, res) => {\n const user = await resolveUser(req, req.body || {});\n const run = statements.getAgentRun.get(req.params.runId, user.userId);\n if (!run) {\n return jsonResponse(res, 404, { success: false, message: 'run not found' });\n }\n const controller = activeAgentRuns.get(run.id);\n if (controller) controller.abort();\n updateAgentRunStatus(run, 'cancelled');\n return jsonResponse(res, 200, { success: true, runId: run.id, status: 'cancelled' });\n }),\n);\n\napp.post(\n '/api/agent/messages/:messageId/feedback',\n asyncHandler(async (req, res) => {\n const user = await resolveUser(req, req.body || {});\n const rating = safeUserText(req.body?.rating);\n if (!['up', 'down'].includes(rating)) {\n return jsonResponse(res, 400, { success: false, message: 'rating must be up or down' });\n }\n statements.insertAgentFeedback.run({\n id: crypto.randomUUID(),\n messageId: req.params.messageId,\n userId: user.userId,\n rating,\n reason: safeUserText(req.body?.reason),\n comment: safeUserText(req.body?.comment),\n createdAt: nowIso(),\n });\n return jsonResponse(res, 200, { success: true });\n }),\n);\n\napp.post(\n '/api/ai/chat',\n asyncHandler(async (req, res) => {\n const message = safeUserText(req.body?.message);\n if (!message) {\n return jsonResponse(res, 400, { success: false, message: 'message is required' });\n }\n\n const user = await resolveUser(req, req.body || {});\n const promptEstimate = estimateTokens(message);\n const estimated = promptEstimate + 600;\n const budget = enforceTokenBudget(user, estimated);\n const maxCompletionTokens = completionBudgetFor(budget, promptEstimate);\n const createdAt = nowIso();\n const conversationId = safeUserText(req.body?.conversationId) || crypto.randomUUID();\n let conversation = statements.getConversation.get(conversationId, user.userId);\n const isNewConversation = !conversation;\n\n if (isNewConversation) {\n conversation = {\n id: conversationId,\n user_id: user.userId,\n title: titleFromMessage(message),\n };\n statements.insertConversation.run({\n id: conversationId,\n userId: user.userId,\n title: conversation.title,\n provider: 'deepseek',\n model: deepseekModel,\n createdAt,\n updatedAt: createdAt,\n });\n }\n\n statements.insertMessage.run({\n id: crypto.randomUUID(),\n conversationId,\n role: 'user',\n content: message,\n promptTokens: 0,\n completionTokens: 0,\n totalTokens: 0,\n createdAt,\n });\n\n const libreChatRuntime = createLibreChatAgentRuntime();\n const skillResult = await runResourceFinderSkill({ message, user });\n const skillContext = resourceContextMessage(skillResult);\n const historyRows = statements.listMessages.all(conversationId).slice(-14);\n const modelMessages = [\n { role: 'system', content: systemPrompt },\n ...(skillContext ? [{ role: 'system', content: skillContext }] : []),\n ...normalizeMessages(historyRows),\n ];\n\n const openClawRuntime = createOpenClawRuntime();\n const codexRuntime = createCodexDeepSeekRuntime();\n let provider = openClawRuntime.enabled\n ? openClawRuntime.provider\n : libreChatRuntime.enabled\n ? libreChatRuntime.provider\n : 'deepseek';\n let responseModel = openClawRuntime.enabled\n ? openClawRuntime.model\n : libreChatRuntime.enabled\n ? libreChatRuntime.model\n : deepseekModel;\n let aiResult;\n try {\n if (openClawRuntime.enabled) {\n aiResult = await callOpenClawAgent({\n messages: modelMessages,\n user,\n conversationId,\n mode: normalizeAgentMode(req.body?.mode),\n });\n provider = aiResult.provider || provider;\n responseModel = aiResult.model || responseModel;\n } else if (libreChatRuntime.enabled) {\n aiResult = await callLibreChatAgent(modelMessages);\n } else if (codexRuntime.enabled) {\n provider = codexRuntime.provider;\n aiResult = await callCodexSdkDeepSeek(modelMessages);\n } else {\n aiResult = await callDeepSeek(modelMessages, { maxCompletionTokens });\n }\n } catch (error) {\n if (libreChatRuntime.enabled) {\n logger.warn({ error: String(error) }, 'LibreChat Agent chat failed, falling back to Codex SDK or DeepSeek direct');\n provider = codexRuntime.enabled ? codexRuntime.provider : 'deepseek';\n responseModel = deepseekModel;\n if (codexRuntime.enabled) {\n try {\n aiResult = await callCodexSdkDeepSeek(modelMessages);\n } catch (codexError) {\n if (isDeepSeekQuotaError(codexError)) throw codexError;\n logger.warn({ error: String(codexError) }, 'Codex SDK chat failed, falling back to DeepSeek direct');\n provider = 'deepseek';\n aiResult = await callDeepSeek(modelMessages, { maxCompletionTokens });\n }\n } else {\n aiResult = await callDeepSeek(modelMessages, { maxCompletionTokens });\n }\n } else {\n if (!codexRuntime.enabled || isDeepSeekQuotaError(error)) throw error;\n logger.warn({ error: String(error) }, 'Codex SDK chat failed, falling back to DeepSeek direct');\n provider = 'deepseek';\n aiResult = await callDeepSeek(modelMessages, { maxCompletionTokens });\n }\n }\n const usage = {\n promptTokens: aiResult.usage.promptTokens || estimateTokens(JSON.stringify(modelMessages)),\n completionTokens: aiResult.usage.completionTokens || estimateTokens(aiResult.message),\n totalTokens:\n aiResult.usage.totalTokens ||\n estimateTokens(JSON.stringify(modelMessages)) + estimateTokens(aiResult.message),\n };\n recordUsage(user.userId, usage.totalTokens);\n\n const answeredAt = nowIso();\n statements.insertMessage.run({\n id: crypto.randomUUID(),\n conversationId,\n role: 'assistant',\n content: aiResult.message,\n promptTokens: usage.promptTokens,\n completionTokens: usage.completionTokens,\n totalTokens: usage.totalTokens,\n createdAt: answeredAt,\n });\n statements.updateConversation.run({\n id: conversationId,\n userId: user.userId,\n title: isNewConversation ? titleFromMessage(message) : conversation.title,\n updatedAt: answeredAt,\n });\n\n const latestUsage = usageFor(user.userId);\n const monthlyLimit = budget.limit;\n jsonResponse(res, 200, {\n success: true,\n conversationId,\n provider,\n model: responseModel,\n message: aiResult.message,\n usage: {\n ...usage,\n monthlyLimit,\n remainingTokens: Math.max(0, monthlyLimit - latestUsage.used),\n },\n });\n }),\n);\n\napp.post('/api/ai/chat/stream', async (req, res) => {\n sseHeaders(res);\n\n try {\n const message = safeUserText(req.body?.message);\n if (!message) {\n writeSse(res, 'error', { message: 'message is required' });\n res.end();\n return;\n }\n\n const user = await resolveUser(req, req.body || {});\n const promptEstimate = estimateTokens(message);\n const estimated = promptEstimate + 600;\n const budget = enforceTokenBudget(user, estimated);\n const maxCompletionTokens = completionBudgetFor(budget, promptEstimate);\n const createdAt = nowIso();\n const conversationId = safeUserText(req.body?.conversationId) || crypto.randomUUID();\n let conversation = statements.getConversation.get(conversationId, user.userId);\n const isNewConversation = !conversation;\n\n if (isNewConversation) {\n conversation = {\n id: conversationId,\n user_id: user.userId,\n title: titleFromMessage(message),\n };\n statements.insertConversation.run({\n id: conversationId,\n userId: user.userId,\n title: conversation.title,\n provider: 'deepseek',\n model: deepseekModel,\n createdAt,\n updatedAt: createdAt,\n });\n }\n\n statements.insertMessage.run({\n id: crypto.randomUUID(),\n conversationId,\n role: 'user',\n content: message,\n promptTokens: 0,\n completionTokens: 0,\n totalTokens: 0,\n createdAt,\n });\n\n const codexRuntime = createCodexDeepSeekRuntime();\n const libreChatRuntime = createLibreChatAgentRuntime();\n const initialOpenClawRuntime = createOpenClawRuntime();\n let provider = initialOpenClawRuntime.enabled\n ? initialOpenClawRuntime.provider\n : libreChatRuntime.enabled\n ? libreChatRuntime.provider\n : codexRuntime.enabled\n ? codexRuntime.provider\n : 'deepseek';\n let responseModel = initialOpenClawRuntime.enabled\n ? initialOpenClawRuntime.model\n : libreChatRuntime.enabled\n ? libreChatRuntime.model\n : deepseekModel;\n writeSse(res, 'meta', {\n conversationId,\n provider,\n model: responseModel,\n });\n const skillResult = await runResourceFinderSkill({\n message,\n user,\n onStep: (step) => writeSse(res, 'step', step),\n });\n const skillContext = resourceContextMessage(skillResult);\n const historyRows = statements.listMessages.all(conversationId).slice(-14);\n const modelMessages = [\n { role: 'system', content: systemPrompt },\n ...(skillContext ? [{ role: 'system', content: skillContext }] : []),\n ...normalizeMessages(historyRows),\n ];\n\n let aiResult;\n const openClawRuntime = initialOpenClawRuntime;\n try {\n if (openClawRuntime.enabled) {\n aiResult = await callOpenClawAgent({\n messages: modelMessages,\n user,\n conversationId,\n mode: normalizeAgentMode(req.body?.mode),\n });\n provider = aiResult.provider || provider;\n responseModel = aiResult.model || responseModel;\n writeSse(res, 'delta', { text: aiResult.message });\n } else if (libreChatRuntime.enabled) {\n aiResult = await callLibreChatAgentStream(modelMessages, {\n onToken: (token) => writeSse(res, 'delta', { text: token }),\n });\n } else if (codexRuntime.enabled) {\n aiResult = await callCodexSdkDeepSeek(modelMessages, {\n onToken: (token) => writeSse(res, 'delta', { text: token }),\n onStep: (step) => writeSse(res, 'step', step),\n });\n } else {\n aiResult = await callDeepSeekStream(modelMessages, {\n maxCompletionTokens,\n onToken: (token) => writeSse(res, 'delta', { text: token }),\n });\n }\n } catch (error) {\n if (libreChatRuntime.enabled) {\n logger.warn({ error: String(error) }, 'LibreChat Agent streaming chat failed, falling back to Codex SDK or DeepSeek direct');\n provider = codexRuntime.enabled ? codexRuntime.provider : 'deepseek';\n responseModel = deepseekModel;\n writeSse(res, 'meta', {\n conversationId,\n provider,\n model: responseModel,\n });\n if (codexRuntime.enabled) {\n try {\n aiResult = await callCodexSdkDeepSeek(modelMessages, {\n onToken: (token) => writeSse(res, 'delta', { text: token }),\n onStep: (step) => writeSse(res, 'step', step),\n });\n } catch (codexError) {\n if (isDeepSeekQuotaError(codexError)) throw codexError;\n logger.warn({ error: String(codexError) }, 'Codex SDK streaming chat failed, falling back to DeepSeek direct');\n provider = 'deepseek';\n responseModel = deepseekModel;\n writeSse(res, 'meta', {\n conversationId,\n provider,\n model: responseModel,\n });\n aiResult = await callDeepSeekStream(modelMessages, {\n maxCompletionTokens,\n onToken: (token) => writeSse(res, 'delta', { text: token }),\n });\n }\n } else {\n aiResult = await callDeepSeekStream(modelMessages, {\n maxCompletionTokens,\n onToken: (token) => writeSse(res, 'delta', { text: token }),\n });\n }\n } else {\n if (!codexRuntime.enabled || isDeepSeekQuotaError(error)) throw error;\n logger.warn({ error: String(error) }, 'Codex SDK streaming chat failed, falling back to DeepSeek direct');\n provider = 'deepseek';\n responseModel = deepseekModel;\n writeSse(res, 'meta', {\n conversationId,\n provider,\n model: responseModel,\n });\n aiResult = await callDeepSeekStream(modelMessages, {\n maxCompletionTokens,\n onToken: (token) => writeSse(res, 'delta', { text: token }),\n });\n }\n }\n\n const usage = {\n promptTokens: aiResult.usage.promptTokens || estimateTokens(JSON.stringify(modelMessages)),\n completionTokens: aiResult.usage.completionTokens || estimateTokens(aiResult.message),\n totalTokens:\n aiResult.usage.totalTokens ||\n estimateTokens(JSON.stringify(modelMessages)) + estimateTokens(aiResult.message),\n };\n recordUsage(user.userId, usage.totalTokens);\n\n const answeredAt = nowIso();\n statements.insertMessage.run({\n id: crypto.randomUUID(),\n conversationId,\n role: 'assistant',\n content: aiResult.message,\n promptTokens: usage.promptTokens,\n completionTokens: usage.completionTokens,\n totalTokens: usage.totalTokens,\n createdAt: answeredAt,\n });\n statements.updateConversation.run({\n id: conversationId,\n userId: user.userId,\n title: isNewConversation ? titleFromMessage(message) : conversation.title,\n updatedAt: answeredAt,\n });\n\n const latestUsage = usageFor(user.userId);\n const monthlyLimit = budget.limit;\n writeSse(res, 'done', {\n conversationId,\n provider,\n model: responseModel,\n message: aiResult.message,\n usage: {\n ...usage,\n monthlyLimit,\n remainingTokens: Math.max(0, monthlyLimit - latestUsage.used),\n },\n });\n res.end();\n } catch (error) {\n logger.error({ error: error.stack || String(error) }, 'streaming chat failed');\n writeSse(res, 'error', {\n message: publicAiErrorMessage(error),\n details: error.details,\n });\n res.end();\n }\n});\n\napp.get(\n '/api/ai/conversations',\n asyncHandler(async (req, res) => {\n const user = await resolveUser(req, { username: req.query.username });\n const items = statements.listConversations.all(user.userId).map((item) => ({\n id: item.id,\n title: item.title,\n updatedAt: item.updatedAt,\n }));\n jsonResponse(res, 200, { success: true, items });\n }),\n);\n\napp.get(\n '/api/ai/conversations/:id',\n asyncHandler(async (req, res) => {\n const user = await resolveUser(req);\n const conversation = statements.getConversation.get(req.params.id, user.userId);\n if (!conversation) {\n return jsonResponse(res, 404, { success: false, message: 'conversation not found' });\n }\n const messages = statements.listMessages.all(req.params.id).map((item) => ({\n role: item.role,\n content: item.content,\n createdAt: item.createdAt,\n }));\n jsonResponse(res, 200, {\n success: true,\n conversation: {\n id: conversation.id,\n title: conversation.title,\n updatedAt: conversation.updated_at,\n },\n messages,\n });\n }),\n);\n\nfunction buildUrl(base, endpoint, params = {}) {\n const url = new URL(endpoint, `${base}/`);\n for (const [key, value] of Object.entries(params)) {\n if (Array.isArray(value)) {\n for (const item of value) {\n if (item !== undefined && item !== null && item !== '') {\n url.searchParams.append(key, String(item));\n }\n }\n } else if (value !== undefined && value !== null && value !== '') {\n url.searchParams.set(key, String(value));\n }\n }\n return url;\n}\n\nasync function fetchJson(url) {\n const response = await fetch(url, {\n headers: { Accept: 'application/json' },\n signal: AbortSignal.timeout(15_000),\n });\n if (!response.ok) {\n throw new Error(`Request failed: ${response.status}`);\n }\n return response.json();\n}\n\nfunction stripHtml(value) {\n return String(value || '')\n .replace(/<script[\\s\\S]*?<\\/script>/gi, ' ')\n .replace(/<style[\\s\\S]*?<\\/style>/gi, ' ')\n .replace(/<[^>]+>/g, ' ')\n .replace(/ /g, ' ')\n .replace(/</g, '<')\n .replace(/>/g, '>')\n .replace(/&/g, '&')\n .replace(/&#(\\d+);/g, (_match, code) => String.fromCharCode(Number(code)))\n .replace(/\\s+/g, ' ')\n .trim();\n}\n\nfunction cleanExtractedText(value) {\n let text = String(value || '')\n .replace(/\\b[A-Z]{1,3}\\d{2}n\\d{4,5}[a-z]?_p?\\d{4}[a-z]\\d{2}\\b/g, ' ')\n .replace(/\\uFFFD/g, '')\n .replace(/\\s+([,。;:!?、」』)》】])/g, '$1')\n .replace(/([「『《(【])\\s+/g, '$1');\n for (let i = 0; i < 3; i += 1) {\n text = text.replace(/([\\u3400-\\u9fff])\\s+([\\u3400-\\u9fff])/g, '$1$2');\n }\n return text.replace(/[ \\t]{2,}/g, ' ').trim();\n}\n\nfunction compactText(value, limit = 140) {\n const text = stripHtml(value);\n return text.length > limit ? `${text.slice(0, limit)}...` : text;\n}\n\nfunction collectSearchResults(data) {\n if (!data || typeof data !== 'object') return [];\n if (Array.isArray(data.results)) return data.results;\n if (Array.isArray(data?.results?.docs)) return data.results.docs;\n if (Array.isArray(data?.response?.docs)) return data.response.docs;\n if (Array.isArray(data.docs)) return data.docs;\n return [];\n}\n\nfunction firstText(item, keys) {\n for (const key of keys) {\n const value = item?.[key];\n if (Array.isArray(value)) {\n const first = value.map((entry) => safeUserText(String(entry))).find(Boolean);\n if (first) return first;\n }\n if (typeof value === 'string' && value.trim()) return value.trim();\n if (typeof value === 'number') return String(value);\n }\n return '';\n}\n\nfunction firstNumber(item, keys, fallback = undefined) {\n for (const key of keys) {\n const value = item?.[key];\n if (typeof value === 'number') return value;\n if (typeof value === 'string' && value.trim() && Number.isFinite(Number(value))) {\n return Number(value);\n }\n }\n return fallback;\n}\n\nasync function searchCbetaTitle(query, limit) {\n const url = buildUrl(cbetaApiRoot, 'search/title', { q: query, start: 0, rows: limit });\n const data = await fetchJson(url);\n return collectSearchResults(data).flatMap((item) => {\n const work = firstText(item, ['work', 'work_id', 'workId']);\n const title = firstText(item, ['content', 'title', 'work_title', 'workTitle']);\n if (!work || !title) return [];\n const juan = firstNumber(item, ['juan', 'juan_num', 'juanNum'], 1);\n return [\n {\n id: `cbeta-title-${work}-${juan}`,\n title,\n sourceName: 'CBETA 佛典',\n url: `cbeta:${work}:${juan}`,\n snippet: firstText(item, ['byline', 'creators', 'time_dynasty']) || '可下载经文正文并用于全球法布施。',\n resourceType: 'scripture',\n work,\n juan,\n },\n ];\n });\n}\n\nasync function searchCbetaContent(query, limit) {\n const endpoints = ['search/fulltext', 'search/content', 'search/all_in_one'];\n const settled = await Promise.allSettled(\n endpoints.map((endpoint) =>\n fetchJson(buildUrl(cbetaApiRoot, endpoint, { q: query, start: 0, rows: limit, field: 'content' })),\n ),\n );\n const results = [];\n for (const response of settled) {\n if (response.status !== 'fulfilled') continue;\n for (const item of collectSearchResults(response.value)) {\n const work = firstText(item, ['work', 'work_id', 'workId', 'sutra', 'id']);\n const snippet = compactText(firstText(item, ['kwic', 'highlight', 'snippet', 'content', 'text', 'body', 'p']));\n if (!work || !snippet) continue;\n const juan = firstNumber(item, ['juan', 'juan_num', 'juanNum', '卷'], 1);\n const title = firstText(item, ['title', 'work_title', 'workTitle', 'sutra_name', 'book', 'name']) || work;\n results.push({\n id: `cbeta-content-${work}-${juan}-${sha256(snippet).slice(0, 8)}`,\n title,\n sourceName: 'CBETA 佛典',\n url: `cbeta:${work}:${juan}`,\n snippet,\n resourceType: 'scripture',\n work,\n juan,\n });\n if (results.length >= limit) return results;\n }\n }\n return results;\n}\n\nfunction directUrlResource(query) {\n const url = safeUserText(query);\n let parsed;\n try {\n parsed = new URL(url);\n } catch {\n return null;\n }\n if (!['http:', 'https:'].includes(parsed.protocol)) return null;\n return {\n id: `url-${sha256(url).slice(0, 12)}`,\n title: parsed.hostname,\n sourceName: '网页资源',\n url,\n snippet: '直接下载并提取这个链接的可读正文。',\n resourceType: 'web',\n };\n}\n\nfunction looksLikeAudioQuery(query) {\n return /音乐|音樂|音频|音訊|mp3|audio|念诵|念誦|诵经|誦經|唱诵|唱誦|读诵|讀誦/.test(\n safeUserText(query).toLowerCase(),\n );\n}\n\nfunction archiveSearchTerm(query) {\n return (\n safeUserText(query)\n .replace(/下载|查找|寻找|找一?个|资源|音频|音訊|音乐|音樂|mp3|audio|念诵|念誦|诵经|誦經|唱诵|唱誦|读诵|讀誦/gi, ' ')\n .replace(/\\s+/g, ' ')\n .trim() || safeUserText(query)\n );\n}\n\nfunction solrPhrase(value) {\n return String(value).replace(/\\\\/g, '\\\\\\\\').replace(/\"/g, '\\\\\"');\n}\n\nasync function searchInternetArchiveAudio(query, limit) {\n if (!looksLikeAudioQuery(query)) return [];\n const term = archiveSearchTerm(query);\n if (!term) return [];\n const variants = Array.from(\n new Set([\n term,\n term.replaceAll('金刚经', '金剛經'),\n term.replaceAll('金剛經', '金刚经'),\n ].filter(Boolean)),\n );\n const phraseQuery = variants\n .flatMap((item) => [`title:\"${solrPhrase(item)}\"`, `description:\"${solrPhrase(item)}\"`])\n .join(' OR ');\n const url = buildUrl('https://archive.org', 'advancedsearch.php', {\n q: `(${phraseQuery}) AND mediatype:audio`,\n rows: Math.min(Math.max(limit, 1), 10),\n output: 'json',\n 'fl[]': ['identifier', 'title', 'description'],\n });\n const data = await fetchJson(url);\n const docs = Array.isArray(data?.response?.docs) ? data.response.docs : [];\n return docs\n .map((item) => {\n const identifier = firstText(item, ['identifier']);\n const title = firstText(item, ['title']) || identifier;\n if (!identifier || !title) return null;\n return {\n id: `ia-audio-${identifier}`,\n title,\n sourceName: 'Internet Archive 音频',\n url: `ia:${identifier}`,\n snippet: compactText(firstText(item, ['description'])) || '公开可访问的音频资源,将下载第一个可用 MP3 文件。',\n resourceType: 'audio',\n identifier,\n };\n })\n .filter(Boolean);\n}\n\nconst curatedCbetaWorks = [\n {\n work: 'T0235',\n juan: 1,\n title: '金剛般若波羅蜜經',\n keywords: ['金剛般若波羅蜜經', '金刚般若波罗蜜经', '金剛經', '金刚经'],\n snippet: '姚秦 鳩摩羅什譯。常用于读诵、共修与法布施的般若经典。',\n },\n {\n work: 'T0251',\n juan: 1,\n title: '般若波羅蜜多心經',\n keywords: ['般若波羅蜜多心經', '般若波罗蜜多心经', '心經', '心经'],\n snippet: '唐 玄奘譯。篇幅短,适合加入禅室功课本与日常持诵。',\n },\n {\n work: 'T0262',\n juan: 1,\n title: '妙法蓮華經',\n keywords: ['妙法蓮華經', '妙法莲华经', '法華經', '法华经'],\n snippet: '姚秦 鳩摩羅什譯。可按卷下载正文,用于长期功课与分享。',\n },\n {\n work: 'T0279',\n juan: 1,\n title: '大方廣佛華嚴經',\n keywords: ['大方廣佛華嚴經', '大方广佛华严经', '華嚴經', '华严经'],\n snippet: '唐 實叉難陀譯。可按卷下载正文,用于系统研读与法布施。',\n },\n {\n work: 'T0360',\n juan: 1,\n title: '佛說無量壽經',\n keywords: ['佛說無量壽經', '佛说无量寿经', '無量壽經', '无量寿经'],\n snippet: '净土经典。可下载正文,用于净土功课本与全球法布施。',\n },\n {\n work: 'T0365',\n juan: 1,\n title: '佛說觀無量壽佛經',\n keywords: ['佛說觀無量壽佛經', '佛说观无量寿佛经', '觀無量壽經', '观无量寿经'],\n snippet: '净土三经之一。适合查找下载后加入净土功课。',\n },\n {\n work: 'T0366',\n juan: 1,\n title: '佛說阿彌陀經',\n keywords: ['佛說阿彌陀經', '佛说阿弥陀经', '阿彌陀經', '阿弥陀经'],\n snippet: '姚秦 鳩摩羅什譯。篇幅适中,适合共修、持诵与分享。',\n },\n {\n work: 'T0475',\n juan: 1,\n title: '維摩詰所說經',\n keywords: ['維摩詰所說經', '维摩诘所说经', '維摩詰經', '维摩诘经'],\n snippet: '姚秦 鳩摩羅什譯。大乘经典,可按卷下载用于研读。',\n },\n {\n work: 'T0676',\n juan: 1,\n title: '解深密經',\n keywords: ['解深密經', '解深密经'],\n snippet: '唯识相关大乘经典。适合研读、整理笔记与法布施。',\n },\n {\n work: 'T1666',\n juan: 1,\n title: '大乘起信論',\n keywords: ['大乘起信論', '大乘起信论', '起信論', '起信论'],\n snippet: '大乘论典。适合下载后作为研习功课资料。',\n },\n];\n\nfunction localCbetaResults(query, limit) {\n const normalizedQuery = safeUserText(query).toLowerCase();\n if (!normalizedQuery) return [];\n return curatedCbetaWorks\n .filter((item) =>\n item.keywords.some((keyword) => {\n const normalizedKeyword = keyword.toLowerCase();\n return normalizedKeyword.includes(normalizedQuery) || normalizedQuery.includes(normalizedKeyword);\n }),\n )\n .slice(0, limit)\n .map((item) => ({\n id: `cbeta-curated-${item.work}-${item.juan}`,\n title: item.title,\n sourceName: 'CBETA 佛典',\n url: `cbeta:${item.work}:${item.juan}`,\n snippet: item.snippet,\n resourceType: 'scripture',\n work: item.work,\n juan: item.juan,\n }));\n}\n\nfunction uniqueResources(items, limit) {\n const seen = new Set();\n const results = [];\n for (const item of items) {\n const key = `${item.url}:${item.title}`;\n if (seen.has(key)) continue;\n seen.add(key);\n results.push(item);\n if (results.length >= limit) break;\n }\n return results;\n}\n\nasync function findResourceCandidates(query, limit) {\n const direct = directUrlResource(query);\n const results = [];\n if (direct) results.push(direct);\n try {\n results.push(...(await searchInternetArchiveAudio(query, limit)));\n } catch (error) {\n logger.warn({ error: String(error), query }, 'Internet Archive audio search skipped');\n }\n results.push(...localCbetaResults(query, limit));\n\n const [titleResults, contentResults] = await Promise.allSettled([\n searchCbetaTitle(query, limit),\n searchCbetaContent(query, limit),\n ]);\n if (titleResults.status === 'fulfilled') results.push(...titleResults.value);\n if (contentResults.status === 'fulfilled') results.push(...contentResults.value);\n\n return uniqueResources(results, limit);\n}\n\nfunction looksLikeResourceTask(message) {\n const text = safeUserText(message).toLowerCase();\n if (!text) return false;\n return /下载|查找|寻找|找一?个|资源|經文|经文|佛经|佛經|仪轨|儀軌|音乐|音樂|mp3|audio|念诵|念誦|诵经|誦經/.test(text);\n}\n\nfunction extractResourceQuery(message) {\n const text = safeUserText(message)\n .replace(/请|請|帮我|幫我|麻烦|麻煩|可以|能不能|需要|我要|想要/g, ' ')\n .replace(/下载|下載|查找|寻找|尋找|找一个|找一個|找|资源|資源|并|並|然后|然後|用于|用於|全球法布施|加入功课本|加入功課本/g, ' ')\n .replace(/\\s+/g, ' ')\n .trim();\n return text || safeUserText(message);\n}\n\nfunction resourceContextMessage(skillResult) {\n if (!skillResult) return '';\n const lines = [\n '本轮资源检索/下载已经由大乘 App 后端完成。',\n '不要再次调用 search_dharma_resources、download_dharma_resource 或其他资源 MCP 工具;请直接基于以下结果回答用户。',\n `后端已调用 skill: ${skillResult.skillName}`,\n `执行目标: ${skillResult.query}`,\n ];\n if (skillResult.selected) {\n lines.push(`候选资源: ${skillResult.selected.title}`);\n lines.push(`来源: ${skillResult.selected.sourceName}`);\n lines.push(`资源地址: ${skillResult.selected.url}`);\n }\n if (skillResult.downloaded) {\n lines.push(`下载标题: ${skillResult.downloaded.title}`);\n lines.push(`下载文件: ${skillResult.downloaded.fileName}`);\n const text = safeUserText(skillResult.downloaded.contentText);\n if (text) {\n lines.push('下载正文摘录:');\n lines.push(text.slice(0, 8000));\n }\n }\n return lines.join('\\n');\n}\n\nasync function runResourceFinderSkill({ message, user, onStep }) {\n if (!looksLikeResourceTask(message)) return null;\n\n const skillName = 'resource-finder-downloader';\n const query = extractResourceQuery(message);\n const audioTask = looksLikeAudioQuery(message) || looksLikeAudioQuery(query);\n onStep?.({\n type: 'skill.step',\n skillName,\n stage: 'start',\n title: '调用资源查找下载 skill',\n message: `识别到资源任务: ${query}`,\n });\n\n onStep?.({\n type: 'skill.step',\n skillName,\n stage: 'search',\n title: '搜索可分享资源',\n message: audioTask\n ? '优先检索公开可访问音频资源,失败时再回落到佛典正文资源。'\n : '优先检索 CBETA 佛典正文接口,搜索索引不可用时使用内置经典映射。',\n });\n const candidates = await findResourceCandidates(query, 8);\n if (candidates.length === 0) {\n onStep?.({\n type: 'skill.step',\n skillName,\n stage: 'empty',\n title: '没有找到可下载资源',\n message: '后端没有找到明确可下载且可分享的资源。',\n });\n return {\n skillName,\n query,\n candidates: [],\n selected: null,\n downloaded: null,\n };\n }\n\n const selected = candidates[0];\n onStep?.({\n type: 'skill.step',\n skillName,\n stage: 'select',\n title: '选择可信候选资源',\n message: `${selected.title} / ${selected.sourceName}`,\n });\n\n onStep?.({\n type: 'skill.step',\n skillName,\n stage: 'download',\n title: '下载并提取资源',\n message: selected.url,\n });\n const cbeta = parseCbetaResource(selected);\n const internetArchive = parseInternetArchiveResource(selected);\n const downloaded = cbeta\n ? await downloadCbetaResource({ ...selected, ...cbeta })\n : internetArchive\n ? await downloadInternetArchiveAudio({ ...selected, ...internetArchive })\n : await downloadWebResource(selected);\n const persisted = persistDownloadedResource(user, downloaded);\n\n onStep?.({\n type: 'skill.step',\n skillName,\n stage: 'done',\n title: '资源已准备完成',\n message: downloaded.fileName,\n });\n\n return {\n skillName,\n query,\n candidates,\n selected,\n downloaded: {\n ...downloaded,\n downloadId: persisted.id,\n },\n };\n}\n\napp.post(\n '/api/resources/search',\n asyncHandler(async (req, res) => {\n const query = safeUserText(req.body?.query);\n const limit = Math.min(Math.max(Number(req.body?.limit || 12), 1), 20);\n if (query.length < 2) {\n return jsonResponse(res, 400, { success: false, message: 'query is required' });\n }\n\n const items = await findResourceCandidates(query, limit);\n\n jsonResponse(res, 200, {\n success: true,\n source: 'resource-index',\n items,\n });\n }),\n);\n\nfunction parseCbetaResource(input) {\n const work = safeUserText(input.work);\n const juan = Number(input.juan || 1);\n if (work) return { work, juan: Number.isFinite(juan) ? juan : 1 };\n\n const url = safeUserText(input.url);\n const match = /^cbeta:([^:]+):(\\d+)$/i.exec(url);\n if (!match) return null;\n return {\n work: match[1].toUpperCase(),\n juan: Number(match[2] || 1),\n };\n}\n\nfunction parseInternetArchiveResource(input) {\n const url = safeUserText(input.url);\n const match = /^ia:([^:]+)$/i.exec(url);\n const identifier = match?.[1] || safeUserText(input.identifier);\n if (!identifier) return null;\n return { identifier };\n}\n\nfunction safeFileName(value) {\n const normalized = String(value || 'dharma-resource')\n .replace(/[\\\\/:*?\"<>|]+/g, '-')\n .replace(/\\s+/g, '-')\n .replace(/-+/g, '-')\n .replace(/^-|-$/g, '')\n .slice(0, 80);\n return normalized || 'dharma-resource';\n}\n\nfunction extractCbetaHtml(data) {\n const first = Array.isArray(data?.results) ? data.results[0] : null;\n if (typeof first === 'string') return first;\n if (first && typeof first === 'object' && typeof first.html === 'string') return first.html;\n if (typeof data?.html === 'string') return data.html;\n return '';\n}\n\nasync function downloadCbetaResource(resource) {\n const url = buildUrl(cbetaApiRoot, 'juans', {\n work: resource.work,\n juan: resource.juan,\n work_info: 1,\n toc: 1,\n });\n const data = await fetchJson(url);\n const workInfo = data.work_info || {};\n const title = safeUserText(workInfo.title) || safeUserText(resource.title) || resource.work;\n const html = extractCbetaHtml(data);\n const text = cleanExtractedText(stripHtml(html));\n if (!text) throw new Error('CBETA resource returned empty content');\n return {\n title,\n sourceName: 'CBETA 佛典',\n url: `cbeta:${resource.work}:${resource.juan}`,\n contentText: text.slice(0, maxResourceTextChars),\n fileName: `${resource.work}_${resource.juan}_${safeFileName(title)}.txt`,\n };\n}\n\nasync function downloadInternetArchiveAudio(resource) {\n const identifier = safeUserText(resource.identifier);\n if (!identifier) throw new Error('Internet Archive identifier is required');\n const metadata = await fetchJson(`https://archive.org/metadata/${encodeURIComponent(identifier)}`);\n const title = safeUserText(metadata?.metadata?.title) || safeUserText(resource.title) || identifier;\n const files = Array.isArray(metadata?.files) ? metadata.files : [];\n const audioFile = files.find((file) => {\n const name = safeUserText(file?.name);\n const format = safeUserText(file?.format).toLowerCase();\n return /\\.(mp3|m4a|ogg)$/i.test(name) || format.includes('mp3');\n });\n if (!audioFile?.name) {\n throw new Error('Internet Archive audio item has no downloadable audio file');\n }\n const sourceFileName = audioFile.name;\n const fileUrl = `https://archive.org/download/${encodeURIComponent(identifier)}/${sourceFileName\n .split('/')\n .map((part) => encodeURIComponent(part))\n .join('/')}`;\n const response = await fetch(fileUrl, {\n headers: {\n Accept: 'audio/mpeg, audio/*;q=0.9, */*;q=0.5',\n 'User-Agent': 'DachengResourceDownloader/1.0 (+https://ombhrum.com)',\n },\n signal: AbortSignal.timeout(60_000),\n });\n if (!response.ok) {\n throw new Error(`Internet Archive audio download failed: ${response.status}`);\n }\n const binaryContent = Buffer.from(await response.arrayBuffer());\n if (!binaryContent.length) throw new Error('Internet Archive audio returned empty content');\n const sizeMb = (binaryContent.length / 1024 / 1024).toFixed(1);\n return {\n title,\n sourceName: 'Internet Archive 音频',\n url: fileUrl,\n contentText: `已下载音频资源: ${title}\\n文件名: ${sourceFileName}\\n大小: ${sizeMb} MB\\n来源: ${fileUrl}`,\n binaryContent,\n fileName: safeFileName(sourceFileName),\n };\n}\n\nasync function downloadWebResource(input) {\n const url = safeUserText(input.url);\n const parsed = new URL(url);\n if (!['http:', 'https:'].includes(parsed.protocol)) {\n throw new Error('Only http/https resources can be downloaded');\n }\n\n const response = await fetch(parsed, {\n headers: {\n Accept: 'text/html, text/plain, application/xhtml+xml, application/xml;q=0.8, */*;q=0.5',\n 'User-Agent': 'DachengResourceDownloader/1.0 (+https://ombhrum.com)',\n },\n signal: AbortSignal.timeout(25_000),\n });\n if (!response.ok) throw new Error(`Resource download failed: ${response.status}`);\n const contentType = response.headers.get('content-type') || '';\n const body = await response.text();\n const title = safeUserText(input.title) || parsed.hostname;\n const contentText = contentType.includes('html') ? cleanExtractedText(stripHtml(body)) : body.trim();\n if (!contentText) throw new Error('Resource returned empty content');\n return {\n title,\n sourceName: safeUserText(input.sourceName) || parsed.hostname,\n url,\n contentText: contentText.slice(0, maxResourceTextChars),\n fileName: `${safeFileName(title)}.txt`,\n };\n}\n\nfunction persistDownloadedResource(user, content) {\n const id = crypto.randomUUID();\n const fileName = `${id}-${safeFileName(content.fileName)}`;\n const filePath = path.join(resourcesDir, fileName);\n if (content.binaryContent) {\n fs.writeFileSync(filePath, content.binaryContent);\n } else {\n fs.writeFileSync(filePath, content.contentText, 'utf8');\n }\n statements.insertResourceDownload.run({\n id,\n userId: user.userId,\n title: content.title,\n sourceName: content.sourceName,\n sourceUrl: content.url,\n fileName: content.fileName,\n filePath,\n createdAt: nowIso(),\n });\n return { id, fileName, filePath };\n}\n\nfunction createLibreChatMcpServer() {\n const server = new McpServer({\n name: 'dacheng-ai-tools',\n version: '0.1.0',\n });\n\n server.registerTool(\n 'search_dharma_resources',\n {\n title: 'Search dharma resources',\n description:\n 'Search for legally shareable Buddhist scripture, text, web, or audio resources that can be prepared for Fabushi sharing.',\n inputSchema: {\n query: z.string().min(2).max(200),\n limit: z.number().int().min(1).max(12).optional(),\n },\n outputSchema: {\n query: z.string(),\n items: z.array(\n z.object({\n id: z.string(),\n title: z.string(),\n sourceName: z.string(),\n url: z.string(),\n snippet: z.string(),\n resourceType: z.string(),\n work: z.string().optional(),\n juan: z.number().optional(),\n }),\n ),\n },\n annotations: {\n readOnlyHint: true,\n destructiveHint: false,\n openWorldHint: true,\n },\n },\n async ({ query, limit }) => {\n const items = await findResourceCandidates(query, limit ?? 8);\n return textToolResult(`Found ${items.length} candidate resources for \"${query}\".`, {\n query,\n items,\n });\n },\n );\n\n server.registerTool(\n 'download_dharma_resource',\n {\n title: 'Download dharma resource',\n description:\n 'Download and extract a selected resource. Use the url/work/juan returned by search_dharma_resources whenever possible.',\n inputSchema: {\n url: z.string().min(1).max(1000),\n title: z.string().max(300).optional(),\n sourceName: z.string().max(120).optional(),\n work: z.string().max(40).optional(),\n juan: z.number().int().min(1).optional(),\n identifier: z.string().max(300).optional(),\n },\n outputSchema: {\n title: z.string(),\n sourceName: z.string(),\n url: z.string(),\n fileName: z.string(),\n downloadId: z.string(),\n contentText: z.string(),\n },\n annotations: {\n readOnlyHint: false,\n destructiveHint: false,\n openWorldHint: true,\n },\n },\n async (input) => {\n const user = { userId: 'librechat:mcp' };\n const cbeta = parseCbetaResource(input);\n const internetArchive = parseInternetArchiveResource(input);\n const content = cbeta\n ? await downloadCbetaResource({ ...input, ...cbeta })\n : internetArchive\n ? await downloadInternetArchiveAudio({ ...input, ...internetArchive })\n : await downloadWebResource(input);\n const persisted = persistDownloadedResource(user, content);\n const { binaryContent: _binaryContent, ...publicContent } = content;\n return textToolResult(`Downloaded ${publicContent.title} as ${publicContent.fileName}.`, {\n ...publicContent,\n downloadId: persisted.id,\n });\n },\n );\n\n server.registerTool(\n 'prepare_dharma_share_text',\n {\n title: 'Prepare dharma sharing text',\n description:\n 'Prepare a text payload for the Fabushi app to confirm and add to global dharma sharing. This returns a clientAction; it does not start sending.',\n inputSchema: {\n title: z.string().min(1).max(120),\n text: z.string().min(1).max(20000),\n },\n outputSchema: {\n clientAction: z.object({\n type: z.literal('prepare_dharma_share_text'),\n title: z.string(),\n text: z.string(),\n }),\n },\n annotations: {\n readOnlyHint: false,\n destructiveHint: false,\n openWorldHint: false,\n },\n },\n async ({ title, text }) =>\n textToolResult(`Prepared \"${title}\" for user confirmation in the Fabushi app.`, {\n clientAction: {\n type: 'prepare_dharma_share_text',\n title: safeUserText(title).slice(0, 120),\n text: String(text || '').trim().slice(0, 20000),\n },\n }),\n );\n\n server.registerTool(\n 'prepare_practice_book_item',\n {\n title: 'Prepare practice book item',\n description:\n 'Prepare a scripture, chant, or practice item for the Fabushi app practice book. This returns a clientAction; it does not mutate the app by itself.',\n inputSchema: {\n title: z.string().min(1).max(120),\n sourceName: z.string().max(120).optional(),\n text: z.string().min(1).max(20000),\n },\n outputSchema: {\n clientAction: z.object({\n type: z.literal('prepare_practice_book_item'),\n title: z.string(),\n sourceName: z.string(),\n text: z.string(),\n }),\n },\n annotations: {\n readOnlyHint: false,\n destructiveHint: false,\n openWorldHint: false,\n },\n },\n async ({ title, sourceName, text }) =>\n textToolResult(`Prepared \"${title}\" for the Fabushi practice book confirmation flow.`, {\n clientAction: {\n type: 'prepare_practice_book_item',\n title: safeUserText(title).slice(0, 120),\n sourceName: safeUserText(sourceName || '大乘 AI').slice(0, 120),\n text: String(text || '').trim().slice(0, 20000),\n },\n }),\n );\n\n return server;\n}\n\nasync function handleMcpRequest(req, res) {\n const server = createLibreChatMcpServer();\n const transport = new StreamableHTTPServerTransport({\n sessionIdGenerator: undefined,\n enableJsonResponse: true,\n });\n\n res.on('close', () => {\n transport.close();\n server.close();\n });\n\n await server.connect(transport);\n await transport.handleRequest(req, res, req.body);\n}\n\napp.all('/mcp', async (req, res) => {\n try {\n await handleMcpRequest(req, res);\n } catch (error) {\n logger.error({ error: error.stack || String(error) }, 'MCP request failed');\n if (!res.headersSent) {\n res.status(500).json({\n error: 'MCP request failed',\n message: publicAiErrorMessage(error),\n });\n }\n }\n});\n\napp.post(\n '/api/resources/download',\n asyncHandler(async (req, res) => {\n const user = await resolveUser(req, req.body || {});\n const cbeta = parseCbetaResource(req.body || {});\n const internetArchive = parseInternetArchiveResource(req.body || {});\n const content = cbeta\n ? await downloadCbetaResource({ ...req.body, ...cbeta })\n : internetArchive\n ? await downloadInternetArchiveAudio({ ...req.body, ...internetArchive })\n : await downloadWebResource(req.body || {});\n const persisted = persistDownloadedResource(user, content);\n const { binaryContent: _binaryContent, ...publicContent } = content;\n jsonResponse(res, 200, {\n success: true,\n downloadId: persisted.id,\n ...publicContent,\n });\n }),\n);\n\napp.post(\n '/api/codex/resource-task',\n asyncHandler(async (req, res) => {\n if (process.env.ENABLE_CODEX_SDK !== 'true') {\n return jsonResponse(res, 403, {\n success: false,\n message: 'Codex SDK task runner is disabled. Set ENABLE_CODEX_SDK=true on the server to enable it.',\n });\n }\n const prompt = safeUserText(req.body?.prompt);\n if (!prompt) {\n return jsonResponse(res, 400, { success: false, message: 'prompt is required' });\n }\n const { Codex } = await import('@openai/codex-sdk');\n const codexRuntime = createCodexDeepSeekRuntime();\n const codex = new Codex(codexRuntime.enabled ? codexRuntime.options : {});\n const thread = codex.startThread(\n codexRuntime.enabled\n ? codexRuntime.threadOptions\n : { skipGitRepoCheck: true, networkAccessEnabled: true },\n );\n const result = await thread.run(prompt);\n jsonResponse(res, 200, {\n success: true,\n threadId: thread.id,\n provider: codexRuntime.provider,\n model: deepseekModel,\n result,\n });\n }),\n);\n\napp.use((error, _req, res, _next) => {\n const status = Number(error.statusCode || error.status || 500);\n const safeStatus = status >= 400 && status < 600 ? status : 500;\n logger.error({ error: error.stack || String(error) }, 'request failed');\n jsonResponse(res, safeStatus, {\n success: false,\n message: error.message || 'Internal Server Error',\n details: error.details,\n });\n});\n\nconst port = Number(process.env.PORT || 8788);\napp.listen(port, '0.0.0.0', () => {\n logger.info(\n {\n port,\n dbPath,\n dataDir,\n deepseekModel,\n cbetaApiRoot,\n },\n 'Dacheng AI backend started',\n );\n});\n"}, "a88efabfc033300cce8c65bb2fb0de754e1ed744": {"t": 1785757986.1723993, "n": 93692, "h": "0d4fe272f1eaa87c", "c": "import crypto from 'node:crypto';\nimport fs from 'node:fs';\nimport path from 'node:path';\nimport process from 'node:process';\nimport { fileURLToPath } from 'node:url';\n\nimport { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';\nimport { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';\nimport Database from 'better-sqlite3';\nimport cors from 'cors';\nimport dotenv from 'dotenv';\nimport express from 'express';\nimport rateLimit from 'express-rate-limit';\nimport helmet from 'helmet';\nimport pino from 'pino';\nimport { z } from 'zod';\n\ndotenv.config();\n\nconst __dirname = path.dirname(fileURLToPath(import.meta.url));\nconst rootDir = path.resolve(__dirname, '..');\nconst dataDir = process.env.DATA_DIR || path.join(rootDir, 'data');\nconst resourcesDir = path.join(dataDir, 'resources');\nconst codexHomeDir = process.env.CODEX_HOME || path.join(dataDir, 'codex-home');\nconst codexTempDir = process.env.CODEX_TMPDIR || path.join(dataDir, 'codex-tmp');\nconst codexRuntimeDir = process.env.XDG_RUNTIME_DIR || path.join(dataDir, 'codex-runtime');\nconst dbPath = process.env.SQLITE_PATH || path.join(dataDir, 'dacheng-ai.sqlite');\n\nfs.mkdirSync(resourcesDir, { recursive: true });\nfs.mkdirSync(codexHomeDir, { recursive: true });\nfs.mkdirSync(codexTempDir, { recursive: true });\nfs.mkdirSync(codexRuntimeDir, { recursive: true });\n\nconst logger = pino({\n level: process.env.LOG_LEVEL || 'info',\n});\n\nconst app = express();\napp.disable('x-powered-by');\napp.set('trust proxy', Number(process.env.TRUST_PROXY_HOPS || 1));\napp.use(helmet());\nconst corsOrigin =\n !process.env.CORS_ORIGIN || process.env.CORS_ORIGIN.trim() === '*'\n ? true\n : process.env.CORS_ORIGIN.split(',').map((item) => item.trim()).filter(Boolean);\napp.use(cors({ origin: corsOrigin }));\napp.use(express.json({ limit: '1mb' }));\napp.use(\n rateLimit({\n windowMs: 60_000,\n limit: Number(process.env.RATE_LIMIT_PER_MINUTE || 90),\n standardHeaders: true,\n legacyHeaders: false,\n }),\n);\n\nconst db = new Database(dbPath);\ndb.pragma('journal_mode = WAL');\ndb.pragma('foreign_keys = ON');\n\ndb.exec(`\n CREATE TABLE IF NOT EXISTS conversations (\n id TEXT PRIMARY KEY,\n user_id TEXT NOT NULL,\n title TEXT NOT NULL,\n provider TEXT NOT NULL,\n model TEXT NOT NULL,\n created_at TEXT NOT NULL,\n updated_at TEXT NOT NULL\n );\n\n CREATE INDEX IF NOT EXISTS idx_conversations_user_updated\n ON conversations (user_id, updated_at DESC);\n\n CREATE TABLE IF NOT EXISTS messages (\n id TEXT PRIMARY KEY,\n conversation_id TEXT NOT NULL,\n role TEXT NOT NULL,\n content TEXT NOT NULL,\n prompt_tokens INTEGER NOT NULL DEFAULT 0,\n completion_tokens INTEGER NOT NULL DEFAULT 0,\n total_tokens INTEGER NOT NULL DEFAULT 0,\n created_at TEXT NOT NULL,\n FOREIGN KEY (conversation_id) REFERENCES conversations(id) ON DELETE CASCADE\n );\n\n CREATE INDEX IF NOT EXISTS idx_messages_conversation_created\n ON messages (conversation_id, created_at ASC);\n\n CREATE TABLE IF NOT EXISTS usage_months (\n user_id TEXT NOT NULL,\n month TEXT NOT NULL,\n total_tokens INTEGER NOT NULL DEFAULT 0,\n updated_at TEXT NOT NULL,\n PRIMARY KEY (user_id, month)\n );\n\n CREATE TABLE IF NOT EXISTS resource_downloads (\n id TEXT PRIMARY KEY,\n user_id TEXT NOT NULL,\n title TEXT NOT NULL,\n source_name TEXT NOT NULL,\n source_url TEXT NOT NULL,\n file_name TEXT NOT NULL,\n file_path TEXT NOT NULL,\n created_at TEXT NOT NULL\n );\n\n CREATE TABLE IF NOT EXISTS agent_runs (\n id TEXT PRIMARY KEY,\n user_id TEXT NOT NULL,\n conversation_id TEXT NOT NULL,\n message_id TEXT NOT NULL,\n openclaw_run_id TEXT,\n openclaw_session_key TEXT NOT NULL,\n status TEXT NOT NULL,\n mode TEXT NOT NULL,\n provider TEXT NOT NULL,\n model TEXT,\n input_tokens INTEGER NOT NULL DEFAULT 0,\n output_tokens INTEGER NOT NULL DEFAULT 0,\n tool_call_count INTEGER NOT NULL DEFAULT 0,\n cost_microusd INTEGER NOT NULL DEFAULT 0,\n started_at TEXT NOT NULL,\n completed_at TEXT,\n failed_at TEXT,\n error_code TEXT,\n error_message TEXT\n );\n\n CREATE INDEX IF NOT EXISTS idx_agent_runs_user_started\n ON agent_runs (user_id, started_at DESC);\n\n CREATE INDEX IF NOT EXISTS idx_agent_runs_status_started\n ON agent_runs (status, started_at DESC);\n\n CREATE TABLE IF NOT EXISTS agent_message_feedback (\n id TEXT PRIMARY KEY,\n message_id TEXT NOT NULL,\n user_id TEXT NOT NULL,\n rating TEXT NOT NULL,\n reason TEXT,\n comment TEXT,\n created_at TEXT NOT NULL\n );\n`);\n\nconst statements = {\n insertConversation: db.prepare(`\n INSERT INTO conversations (id, user_id, title, provider, model, created_at, updated_at)\n VALUES (@id, @userId, @title, @provider, @model, @createdAt, @updatedAt)\n `),\n updateConversation: db.prepare(`\n UPDATE conversations\n SET title = @title, updated_at = @updatedAt\n WHERE id = @id AND user_id = @userId\n `),\n getConversation: db.prepare(`\n SELECT * FROM conversations WHERE id = ? AND user_id = ? LIMIT 1\n `),\n listConversations: db.prepare(`\n SELECT id, title, updated_at AS updatedAt\n FROM conversations\n WHERE user_id = ?\n ORDER BY updated_at DESC\n LIMIT 30\n `),\n insertMessage: db.prepare(`\n INSERT INTO messages (\n id, conversation_id, role, content,\n prompt_tokens, completion_tokens, total_tokens, created_at\n )\n VALUES (\n @id, @conversationId, @role, @content,\n @promptTokens, @completionTokens, @totalTokens, @createdAt\n )\n `),\n listMessages: db.prepare(`\n SELECT role, content, prompt_tokens AS promptTokens,\n completion_tokens AS completionTokens, total_tokens AS totalTokens,\n created_at AS createdAt\n FROM messages\n WHERE conversation_id = ?\n ORDER BY created_at ASC\n `),\n getUsage: db.prepare(`\n SELECT total_tokens AS totalTokens FROM usage_months WHERE user_id = ? AND month = ?\n `),\n upsertUsage: db.prepare(`\n INSERT INTO usage_months (user_id, month, total_tokens, updated_at)\n VALUES (@userId, @month, @totalTokens, @updatedAt)\n ON CONFLICT(user_id, month)\n DO UPDATE SET total_tokens = usage_months.total_tokens + excluded.total_tokens,\n updated_at = excluded.updated_at\n `),\n insertResourceDownload: db.prepare(`\n INSERT INTO resource_downloads (\n id, user_id, title, source_name, source_url, file_name, file_path, created_at\n )\n VALUES (\n @id, @userId, @title, @sourceName, @sourceUrl, @fileName, @filePath, @createdAt\n )\n `),\n insertAgentRun: db.prepare(`\n INSERT INTO agent_runs (\n id, user_id, conversation_id, message_id, openclaw_run_id, openclaw_session_key,\n status, mode, provider, model, started_at\n )\n VALUES (\n @id, @userId, @conversationId, @messageId, @openClawRunId, @openClawSessionKey,\n @status, @mode, @provider, @model, @startedAt\n )\n `),\n getAgentRun: db.prepare(`\n SELECT * FROM agent_runs WHERE id = ? AND user_id = ? LIMIT 1\n `),\n updateAgentRunStatus: db.prepare(`\n UPDATE agent_runs\n SET status = @status,\n openclaw_run_id = COALESCE(@openClawRunId, openclaw_run_id),\n input_tokens = COALESCE(@inputTokens, input_tokens),\n output_tokens = COALESCE(@outputTokens, output_tokens),\n tool_call_count = COALESCE(@toolCallCount, tool_call_count),\n completed_at = @completedAt,\n failed_at = @failedAt,\n error_code = @errorCode,\n error_message = @errorMessage\n WHERE id = @id AND user_id = @userId\n `),\n insertAgentFeedback: db.prepare(`\n INSERT INTO agent_message_feedback (id, message_id, user_id, rating, reason, comment, created_at)\n VALUES (@id, @messageId, @userId, @rating, @reason, @comment, @createdAt)\n `),\n};\n\nconst deepseekApiKey = process.env.DEEPSEEK_API_KEY || '';\nconst deepseekBaseUrl = (process.env.DEEPSEEK_BASE_URL || 'https://api.deepseek.com').replace(/\\/+$/, '');\nconst deepseekModel = process.env.DEEPSEEK_MODEL || 'deepseek-chat';\nconst cbetaApiRoot = (process.env.CBETA_API_ROOT || 'https://144.24.17.21.sslip.io').replace(/\\/+$/, '');\nconst fabushiApiBaseUrl = (process.env.FABUSHI_API_BASE_URL || 'http://144.24.17.21').replace(/\\/+$/, '');\nconst memberMonthlyLimit = Number(process.env.MEMBER_MONTHLY_TOKEN_LIMIT || 1_000_000);\nconst freeMonthlyLimit = Number(process.env.FREE_MONTHLY_TOKEN_LIMIT || 50_000);\nconst maxResourceTextChars = Number(process.env.MAX_RESOURCE_TEXT_CHARS || 80_000);\nconst enableLibreChatAgentChat = process.env.ENABLE_LIBRECHAT_AGENT_CHAT === 'true';\nconst libreChatAgentsBaseUrl = (\n process.env.LIBRECHAT_AGENTS_BASE_URL || 'http://127.0.0.1:3080/api/agents/v1'\n).replace(/\\/+$/, '');\nconst libreChatAgentApiKey = process.env.LIBRECHAT_AGENT_API_KEY || '';\nconst libreChatAgentId = process.env.LIBRECHAT_AGENT_ID || '';\nconst enableCodexSdkChat = process.env.ENABLE_CODEX_SDK_CHAT === 'true';\nconst enableOpenClawAgentChat = process.env.ENABLE_OPENCLAW_AGENT_CHAT === 'true';\nconst openClawGatewayUrl = (process.env.OPENCLAW_GATEWAY_URL || 'http://127.0.0.1:18789').replace(/\\/+$/, '');\nconst openClawGatewayToken = process.env.OPENCLAW_GATEWAY_TOKEN || '';\nconst openClawAgentId = process.env.OPENCLAW_AGENT_ID || 'fabushi-public-agent';\nconst openClawRunsEndpoint = process.env.OPENCLAW_RUNS_ENDPOINT || '';\nconst codexDeepSeekProviderId = 'deepseek-chat-completions';\nconst codexResponsesBaseUrl = (\n process.env.CODEX_DEEPSEEK_RESPONSES_BASE_URL ||\n `http://127.0.0.1:${process.env.PORT || 8788}/codex-deepseek/v1`\n).replace(/\\/+$/, '');\n\nconst systemPrompt = [\n '你是“大乘”App 的 AI 助手。',\n '你的核心任务是帮助用户查找、整理、理解并全球法布施合法可分享的佛法资源。',\n '回答要庄重、简洁、可执行;涉及经典、仪轨或资源时,提醒用户尊重版权、来源和当地法规。',\n '如果用户想找资源、下载经文、寻找音频或准备可分享资料,你必须先在后端自动执行搜索、验证和下载步骤;不要要求用户去前端 + 菜单手动查找。',\n '你可以把执行进度总结给用户,但不要输出隐藏推理原文。',\n].join('\\n');\n\nfunction nowIso() {\n return new Date().toISOString();\n}\n\nfunction monthKey(date = new Date()) {\n return `${date.getUTCFullYear()}-${String(date.getUTCMonth() + 1).padStart(2, '0')}`;\n}\n\nfunction jsonResponse(res, status, payload) {\n res.status(status).json(payload);\n}\n\nfunction textToolResult(text, structuredContent = {}) {\n return {\n structuredContent,\n content: [{ type: 'text', text }],\n };\n}\n\nfunction sseHeaders(res) {\n res.status(200);\n res.setHeader('Content-Type', 'text/event-stream; charset=utf-8');\n res.setHeader('Cache-Control', 'no-cache, no-transform');\n res.setHeader('Connection', 'keep-alive');\n res.setHeader('X-Accel-Buffering', 'no');\n if (typeof res.flushHeaders === 'function') res.flushHeaders();\n}\n\nfunction writeSse(res, event, payload) {\n res.write(`event: ${event}\\n`);\n res.write(`data: ${JSON.stringify(payload)}\\n\\n`);\n}\n\nfunction asyncHandler(fn) {\n return (req, res, next) => {\n Promise.resolve(fn(req, res, next)).catch(next);\n };\n}\n\nfunction bearerToken(req) {\n const header = req.get('Authorization') || '';\n if (!header.startsWith('Bearer ')) return '';\n return header.slice('Bearer '.length).trim();\n}\n\nfunction sha256(value) {\n return crypto.createHash('sha256').update(value).digest('hex');\n}\n\nfunction safeUserText(value, fallback = '') {\n if (typeof value !== 'string') return fallback;\n return value.replace(/\\u0000/g, '').trim();\n}\n\nfunction estimateTokens(text) {\n const normalized = String(text || '').trim();\n if (!normalized) return 0;\n return Math.max(1, Math.ceil(normalized.length / 3));\n}\n\nfunction normalizeMembershipPayload(payload) {\n if (!payload || typeof payload !== 'object') return false;\n const data = payload.data && typeof payload.data === 'object' ? payload.data : payload;\n return Boolean(\n data.isActive ||\n data.active ||\n data.hasPremiumAccess ||\n data.hasPremiumMembership ||\n data.isPremium ||\n data.status === 'active' ||\n data.type === 'paid' ||\n data.membershipType === 'paid',\n );\n}\n\nasync function checkRemoteMembership(token) {\n if (!token || !fabushiApiBaseUrl) return false;\n const endpoints = [\n '/api/stripe/membership-status',\n '/api/alipay/check-membership',\n '/api/auth/user-info',\n ];\n\n for (const endpoint of endpoints) {\n try {\n const response = await fetch(`${fabushiApiBaseUrl}${endpoint}`, {\n headers: {\n Accept: 'application/json',\n Authorization: `Bearer ${token}`,\n },\n signal: AbortSignal.timeout(8_000),\n });\n if (!response.ok) continue;\n const payload = await response.json();\n if (normalizeMembershipPayload(payload)) return true;\n } catch (error) {\n logger.debug({ endpoint, error: String(error) }, 'membership lookup skipped');\n }\n }\n\n return false;\n}\n\nasync function resolveUser(req, body = {}) {\n const token = bearerToken(req);\n const username = safeUserText(body.username || req.query.username);\n const tokenHash = token ? sha256(token).slice(0, 24) : '';\n const anonymousHash = sha256(`${req.ip}|${req.get('User-Agent') || ''}`).slice(0, 24);\n const userId = username\n ? `user:${username}`\n : tokenHash\n ? `token:${tokenHash}`\n : `anon:${anonymousHash}`;\n const remoteMember = await checkRemoteMembership(token);\n const memberHint = body.clientMembershipHint === true;\n return {\n userId,\n username,\n tokenHash,\n isMember: remoteMember || memberHint,\n };\n}\n\nfunction usageFor(userId) {\n const month = monthKey();\n const row = statements.getUsage.get(userId, month);\n return {\n month,\n used: Number(row?.totalTokens || 0),\n };\n}\n\nfunction enforceTokenBudget(user, estimatedTokensForRequest) {\n const limit = user.isMember ? memberMonthlyLimit : freeMonthlyLimit;\n const usage = usageFor(user.userId);\n if (usage.used + estimatedTokensForRequest > limit) {\n const error = new Error('本月 AI token 额度已不足');\n error.statusCode = 429;\n error.details = {\n monthlyLimit: limit,\n usedTokens: usage.used,\n remainingTokens: Math.max(0, limit - usage.used),\n };\n throw error;\n }\n return { limit, usage };\n}\n\nfunction recordUsage(userId, totalTokens) {\n if (!totalTokens || totalTokens < 1) return;\n statements.upsertUsage.run({\n userId,\n month: monthKey(),\n totalTokens,\n updatedAt: nowIso(),\n });\n}\n\nfunction titleFromMessage(message) {\n const compact = message.replace(/\\s+/g, ' ').trim();\n if (!compact) return '新对话';\n return compact.length > 22 ? `${compact.slice(0, 22)}...` : compact;\n}\n\nfunction normalizeMessages(rows) {\n return rows.map((row) => ({\n role: row.role,\n content: row.content,\n }));\n}\n\nasync function callDeepSeek(messages) {\n if (!deepseekApiKey) {\n const error = new Error('DeepSeek API key is not configured');\n error.statusCode = 503;\n throw error;\n }\n\n const response = await fetch(`${deepseekBaseUrl}/chat/completions`, {\n method: 'POST',\n headers: {\n Accept: 'application/json',\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${deepseekApiKey}`,\n },\n body: JSON.stringify({\n model: deepseekModel,\n messages,\n temperature: 0.4,\n max_tokens: 1400,\n stream: false,\n }),\n signal: AbortSignal.timeout(60_000),\n });\n\n const bodyText = await response.text();\n let payload;\n try {\n payload = bodyText ? JSON.parse(bodyText) : {};\n } catch {\n payload = { error: bodyText };\n }\n\n if (!response.ok) {\n const upstreamMessage = payload?.error?.message || payload?.message || '';\n const friendlyMessage =\n response.status === 402 || /insufficient\\s+balance/i.test(upstreamMessage)\n ? '大乘 AI 服务额度暂不可用,请联系管理员充值 DeepSeek 账户后重试。'\n : upstreamMessage || `DeepSeek request failed: ${response.status}`;\n const error = new Error(friendlyMessage);\n error.statusCode = response.status;\n throw error;\n }\n\n const message = payload?.choices?.[0]?.message?.content?.trim();\n if (!message) {\n const error = new Error('DeepSeek returned an empty response');\n error.statusCode = 502;\n throw error;\n }\n\n return {\n message,\n usage: {\n promptTokens: Number(payload?.usage?.prompt_tokens || 0),\n completionTokens: Number(payload?.usage?.completion_tokens || 0),\n totalTokens: Number(payload?.usage?.total_tokens || 0),\n },\n };\n}\n\nasync function callDeepSeekStream(messages, callbacks = {}) {\n if (!deepseekApiKey) {\n const error = new Error('DeepSeek API key is not configured');\n error.statusCode = 503;\n throw error;\n }\n\n const response = await fetch(`${deepseekBaseUrl}/chat/completions`, {\n method: 'POST',\n headers: {\n Accept: 'text/event-stream',\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${deepseekApiKey}`,\n },\n body: JSON.stringify({\n model: deepseekModel,\n messages,\n temperature: 0.4,\n max_tokens: 1400,\n stream: true,\n stream_options: { include_usage: true },\n }),\n signal: callbacks.signal || AbortSignal.timeout(90_000),\n });\n\n if (!response.ok) {\n const bodyText = await response.text();\n let payload;\n try {\n payload = bodyText ? JSON.parse(bodyText) : {};\n } catch {\n payload = { error: bodyText };\n }\n const upstreamMessage = payload?.error?.message || payload?.message || '';\n const friendlyMessage =\n response.status === 402 || /insufficient\\s+balance/i.test(upstreamMessage)\n ? '大乘 AI 服务额度暂不可用,请联系管理员充值 DeepSeek 账户后重试。'\n : upstreamMessage || `DeepSeek request failed: ${response.status}`;\n const error = new Error(friendlyMessage);\n error.statusCode = response.status;\n throw error;\n }\n\n let buffer = '';\n let message = '';\n let usage = {\n promptTokens: 0,\n completionTokens: 0,\n totalTokens: 0,\n };\n const decoder = new TextDecoder();\n\n for await (const chunk of response.body) {\n buffer += decoder.decode(chunk, { stream: true });\n const lines = buffer.split(/\\r?\\n/);\n buffer = lines.pop() || '';\n\n for (const rawLine of lines) {\n const line = rawLine.trim();\n if (!line.startsWith('data:')) continue;\n const data = line.slice('data:'.length).trim();\n if (!data || data === '[DONE]') continue;\n\n let payload;\n try {\n payload = JSON.parse(data);\n } catch {\n continue;\n }\n\n const delta = payload?.choices?.[0]?.delta?.content || '';\n if (delta) {\n message += delta;\n callbacks.onToken?.(delta);\n }\n\n if (payload?.usage) {\n usage = {\n promptTokens: Number(payload.usage.prompt_tokens || usage.promptTokens || 0),\n completionTokens: Number(payload.usage.completion_tokens || usage.completionTokens || 0),\n totalTokens: Number(payload.usage.total_tokens || usage.totalTokens || 0),\n };\n }\n }\n }\n\n message = message.trim();\n if (!message) {\n const error = new Error('DeepSeek returned an empty response');\n error.statusCode = 502;\n throw error;\n }\n\n return { message, usage };\n}\n\nfunction createCodexDeepSeekRuntime() {\n if (!enableCodexSdkChat || !deepseekApiKey) {\n return {\n enabled: false,\n provider: 'deepseek-direct',\n reason: enableCodexSdkChat ? 'DeepSeek API key is not configured' : 'ENABLE_CODEX_SDK_CHAT is not true',\n };\n }\n\n return {\n enabled: true,\n provider: 'codex-sdk-deepseek',\n options: {\n apiKey: deepseekApiKey,\n baseUrl: deepseekBaseUrl,\n env: {\n ...process.env,\n DEEPSEEK_API_KEY: deepseekApiKey,\n HOME: codexHomeDir,\n CODEX_HOME: codexHomeDir,\n TMPDIR: codexTempDir,\n XDG_RUNTIME_DIR: codexRuntimeDir,\n XDG_CACHE_HOME: path.join(codexHomeDir, '.cache'),\n XDG_CONFIG_HOME: path.join(codexHomeDir, '.config'),\n XDG_DATA_HOME: path.join(codexHomeDir, '.local', 'share'),\n PATH: process.env.PATH || '/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin',\n },\n config: {\n model_provider: codexDeepSeekProviderId,\n model_providers: {\n [codexDeepSeekProviderId]: {\n name: 'DeepSeek Chat Completions',\n base_url: codexResponsesBaseUrl,\n env_key: 'DEEPSEEK_API_KEY',\n wire_api: 'responses',\n query_params: {},\n },\n },\n },\n },\n threadOptions: {\n model: deepseekModel,\n approvalPolicy: 'never',\n sandboxMode: 'read-only',\n skipGitRepoCheck: true,\n networkAccessEnabled: true,\n },\n };\n}\n\nfunction codexPromptFromMessages(messages) {\n return messages\n .map((message) => {\n const role = String(message.role || 'user').toUpperCase();\n return `${role}:\\n${message.content}`;\n })\n .join('\\n\\n');\n}\n\nfunction responseContentText(content) {\n if (typeof content === 'string') return content;\n if (!Array.isArray(content)) return '';\n return content\n .map((part) => {\n if (typeof part === 'string') return part;\n if (!part || typeof part !== 'object') return '';\n return String(part.text || part.input_text || part.output_text || '');\n })\n .filter(Boolean)\n .join('\\n');\n}\n\nfunction codexResponsesPrompt(body) {\n const input = Array.isArray(body?.input) ? body.input : [];\n const userItems = input.filter((item) => item?.role === 'user');\n const source = userItems[userItems.length - 1] || input[input.length - 1];\n const prompt = responseContentText(source?.content).trim();\n if (prompt) return prompt;\n if (typeof body?.input === 'string') return body.input.trim();\n return '';\n}\n\nfunction writeResponsesEvent(res, event, data) {\n res.write(`event: ${event}\\n`);\n res.write(`data: ${JSON.stringify(data)}\\n\\n`);\n}\n\nfunction responseUsage(usage = {}) {\n return {\n input_tokens: Number(usage.promptTokens || usage.input_tokens || 0),\n cached_input_tokens: 0,\n output_tokens: Number(usage.completionTokens || usage.output_tokens || 0),\n reasoning_output_tokens: 0,\n total_tokens: Number(usage.totalTokens || usage.total_tokens || 0),\n };\n}\n\nfunction createLibreChatAgentRuntime() {\n if (!enableLibreChatAgentChat || !libreChatAgentApiKey || !libreChatAgentId) {\n return {\n enabled: false,\n provider: 'librechat-agent',\n reason: !enableLibreChatAgentChat\n ? 'ENABLE_LIBRECHAT_AGENT_CHAT is not true'\n : !libreChatAgentApiKey\n ? 'LIBRECHAT_AGENT_API_KEY is not configured'\n : 'LIBRECHAT_AGENT_ID is not configured',\n };\n }\n\n return {\n enabled: true,\n provider: 'librechat-agent',\n model: libreChatAgentId,\n chatCompletionsUrl: `${libreChatAgentsBaseUrl}/chat/completions`,\n };\n}\n\nfunction libreChatUsage(usage = {}) {\n return {\n promptTokens: Number(usage.prompt_tokens || usage.input_tokens || usage.promptTokens || 0),\n completionTokens: Number(usage.completion_tokens || usage.output_tokens || usage.completionTokens || 0),\n totalTokens: Number(usage.total_tokens || usage.totalTokens || 0),\n };\n}\n\nasync function callLibreChatAgent(messages) {\n const runtime = createLibreChatAgentRuntime();\n if (!runtime.enabled) {\n const error = new Error(runtime.reason || 'LibreChat Agent chat is not enabled');\n error.statusCode = 503;\n throw error;\n }\n\n const response = await fetch(runtime.chatCompletionsUrl, {\n method: 'POST',\n headers: {\n Accept: 'application/json',\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${libreChatAgentApiKey}`,\n },\n body: JSON.stringify({\n model: runtime.model,\n messages,\n temperature: 0.4,\n stream: false,\n }),\n signal: AbortSignal.timeout(120_000),\n });\n\n const bodyText = await response.text();\n let payload;\n try {\n payload = bodyText ? JSON.parse(bodyText) : {};\n } catch {\n payload = { error: bodyText };\n }\n\n if (!response.ok) {\n const upstreamMessage = payload?.error?.message || payload?.message || bodyText || '';\n const error = new Error(upstreamMessage || `LibreChat Agent request failed: ${response.status}`);\n error.statusCode = response.status;\n throw error;\n }\n\n const message = payload?.choices?.[0]?.message?.content?.trim();\n if (!message) {\n const error = new Error('LibreChat Agent returned an empty response');\n error.statusCode = 502;\n throw error;\n }\n\n return {\n message,\n usage: libreChatUsage(payload?.usage),\n };\n}\n\nasync function callLibreChatAgentStream(messages, callbacks = {}) {\n const runtime = createLibreChatAgentRuntime();\n if (!runtime.enabled) {\n const error = new Error(runtime.reason || 'LibreChat Agent chat is not enabled');\n error.statusCode = 503;\n throw error;\n }\n\n const response = await fetch(runtime.chatCompletionsUrl, {\n method: 'POST',\n headers: {\n Accept: 'text/event-stream',\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${libreChatAgentApiKey}`,\n },\n body: JSON.stringify({\n model: runtime.model,\n messages,\n temperature: 0.4,\n stream: true,\n }),\n signal: callbacks.signal || AbortSignal.timeout(180_000),\n });\n\n if (!response.ok) {\n const bodyText = await response.text();\n let payload;\n try {\n payload = bodyText ? JSON.parse(bodyText) : {};\n } catch {\n payload = { error: bodyText };\n }\n const upstreamMessage = payload?.error?.message || payload?.message || bodyText || '';\n const error = new Error(upstreamMessage || `LibreChat Agent request failed: ${response.status}`);\n error.statusCode = response.status;\n throw error;\n }\n\n let buffer = '';\n let message = '';\n let usage = { promptTokens: 0, completionTokens: 0, totalTokens: 0 };\n const decoder = new TextDecoder();\n\n for await (const chunk of response.body) {\n buffer += decoder.decode(chunk, { stream: true });\n const lines = buffer.split(/\\r?\\n/);\n buffer = lines.pop() || '';\n\n for (const rawLine of lines) {\n const line = rawLine.trim();\n if (!line.startsWith('data:')) continue;\n const data = line.slice('data:'.length).trim();\n if (!data || data === '[DONE]') continue;\n\n let payload;\n try {\n payload = JSON.parse(data);\n } catch {\n continue;\n }\n\n const delta = payload?.choices?.[0]?.delta?.content || '';\n if (delta) {\n message += delta;\n callbacks.onToken?.(delta);\n }\n\n if (payload?.usage) {\n usage = libreChatUsage(payload.usage);\n }\n }\n }\n\n message = message.trim();\n if (!message) {\n const error = new Error('LibreChat Agent returned an empty response');\n error.statusCode = 502;\n throw error;\n }\n\n return { message, usage };\n}\n\nfunction isDeepSeekQuotaError(error) {\n const message = String(error?.message || error || '');\n return /额度|insufficient\\s+balance|balance/i.test(message);\n}\n\nfunction publicAiErrorMessage(error) {\n if (isDeepSeekQuotaError(error)) {\n return '大乘 AI 服务额度暂不可用,请联系管理员充值 DeepSeek 账户后重试。';\n }\n return error?.message || 'Internal Server Error';\n}\n\nfunction responsesPayload({\n responseId,\n itemId,\n status,\n model,\n text = '',\n usage = null,\n error = null,\n}) {\n return {\n id: responseId,\n object: 'response',\n status,\n model,\n output:\n status === 'completed'\n ? [\n {\n id: itemId,\n type: 'message',\n role: 'assistant',\n status: 'completed',\n content: [{ type: 'output_text', text }],\n },\n ]\n : [],\n ...(usage ? { usage: responseUsage(usage) } : {}),\n ...(error\n ? {\n error: {\n message: error.message || String(error),\n type: 'server_error',\n },\n }\n : {}),\n };\n}\n\nasync function callCodexSdkDeepSeek(messages, callbacks = {}) {\n const codexRuntime = createCodexDeepSeekRuntime();\n if (!codexRuntime.enabled) {\n const error = new Error(codexRuntime.reason || 'Codex SDK chat is not enabled');\n error.statusCode = 503;\n throw error;\n }\n\n const { Codex } = await import('@openai/codex-sdk');\n const codex = new Codex(codexRuntime.options);\n const thread = codex.startThread(codexRuntime.threadOptions);\n const prompt = codexPromptFromMessages(messages);\n\n if (callbacks.onToken || callbacks.onStep) {\n const { events } = await thread.runStreamed(prompt, { signal: callbacks.signal });\n let finalResponse = '';\n let usage = null;\n\n for await (const event of events) {\n if (event.type === 'item.completed') {\n const item = event.item;\n if (item.type === 'agent_message') {\n finalResponse = item.text || finalResponse;\n callbacks.onToken?.(item.text || '');\n } else if (item.type === 'todo_list') {\n callbacks.onStep?.({\n title: 'Codex SDK 执行计划',\n message: item.items.map((todo) => `${todo.completed ? '✓' : '•'} ${todo.text}`).join('\\n'),\n });\n } else if (item.type === 'command_execution') {\n callbacks.onStep?.({\n title: 'Codex SDK 执行命令',\n message: item.command,\n });\n } else if (item.type === 'mcp_tool_call') {\n callbacks.onStep?.({\n title: 'Codex SDK 调用工具',\n message: `${item.server}.${item.tool}`,\n });\n }\n } else if (event.type === 'turn.completed') {\n usage = event.usage;\n } else if (event.type === 'turn.failed') {\n throw new Error(event.error?.message || 'Codex SDK turn failed');\n } else if (event.type === 'error') {\n throw new Error(event.message || 'Codex SDK stream failed');\n }\n }\n\n if (!finalResponse.trim()) {\n const error = new Error('Codex SDK returned an empty response');\n error.statusCode = 502;\n throw error;\n }\n\n return {\n message: finalResponse.trim(),\n usage: {\n promptTokens: Number(usage?.input_tokens || 0),\n completionTokens: Number(usage?.output_tokens || 0),\n totalTokens: Number((usage?.input_tokens || 0) + (usage?.output_tokens || 0)),\n },\n };\n }\n\n const turn = await thread.run(prompt);\n if (!turn.finalResponse?.trim()) {\n const error = new Error('Codex SDK returned an empty response');\n error.statusCode = 502;\n throw error;\n }\n\n return {\n message: turn.finalResponse.trim(),\n usage: {\n promptTokens: Number(turn.usage?.input_tokens || 0),\n completionTokens: Number(turn.usage?.output_tokens || 0),\n totalTokens: Number((turn.usage?.input_tokens || 0) + (turn.usage?.output_tokens || 0)),\n },\n };\n}\n\nconst activeAgentRuns = new Map();\n\nfunction normalizeAgentMode(value) {\n const mode = safeUserText(value || 'dharma_guide');\n return /^[a-z][a-z0-9_:-]{0,48}$/i.test(mode) ? mode : 'dharma_guide';\n}\n\nfunction openClawSessionKey(user, conversationId) {\n const env = process.env.NODE_ENV === 'production' ? 'prod' : 'dev';\n return `fabushi:env:${env}:user:${user.userId}:conversation:${conversationId}`;\n}\n\nfunction createOpenClawRuntime() {\n if (!enableOpenClawAgentChat || !openClawGatewayToken || !openClawGatewayUrl) {\n return {\n enabled: false,\n provider: 'openclaw-gateway',\n reason: !enableOpenClawAgentChat\n ? 'ENABLE_OPENCLAW_AGENT_CHAT is not true'\n : !openClawGatewayToken\n ? 'OPENCLAW_GATEWAY_TOKEN is not configured'\n : 'OPENCLAW_GATEWAY_URL is not configured',\n };\n }\n\n return {\n enabled: true,\n provider: 'openclaw-gateway',\n model: openClawAgentId,\n runsUrl: openClawRunsEndpoint || `${openClawGatewayUrl}/v1/agents/${encodeURIComponent(openClawAgentId)}/runs`,\n };\n}\n\nfunction extractOpenClawText(payload) {\n if (!payload || typeof payload !== 'object') return '';\n const candidates = [\n payload.message,\n payload.content,\n payload.output_text,\n payload.finalResponse,\n payload.final_response,\n payload.result?.message,\n payload.result?.content,\n payload.output?.message,\n payload.output?.content,\n payload.choices?.[0]?.message?.content,\n ];\n for (const candidate of candidates) {\n if (typeof candidate === 'string' && candidate.trim()) return candidate.trim();\n }\n return '';\n}\n\nfunction extractOpenClawUsage(payload) {\n const usage = payload?.usage || payload?.result?.usage || {};\n return {\n promptTokens: Number(usage.input_tokens || usage.prompt_tokens || usage.promptTokens || 0),\n completionTokens: Number(usage.output_tokens || usage.completion_tokens || usage.completionTokens || 0),\n totalTokens: Number(usage.total_tokens || usage.totalTokens || 0),\n };\n}\n\nasync function callOpenClawAgent({ messages, user, conversationId, mode, signal }) {\n const runtime = createOpenClawRuntime();\n if (!runtime.enabled) {\n const error = new Error(runtime.reason || 'OpenClaw Agent chat is not enabled');\n error.statusCode = 503;\n throw error;\n }\n\n const sessionKey = openClawSessionKey(user, conversationId);\n const response = await fetch(runtime.runsUrl, {\n method: 'POST',\n headers: {\n Accept: 'application/json',\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${openClawGatewayToken}`,\n },\n body: JSON.stringify({\n agentId: openClawAgentId,\n sessionKey,\n mode,\n stream: false,\n input: { messages },\n messages,\n metadata: {\n app: 'fabushi',\n userId: user.userId,\n conversationId,\n toolPolicy: 'public-user-read-mostly',\n },\n }),\n signal: signal || AbortSignal.timeout(180_000),\n });\n\n const bodyText = await response.text();\n let payload;\n try {\n payload = bodyText ? JSON.parse(bodyText) : {};\n } catch {\n payload = { message: bodyText };\n }\n\n if (!response.ok) {\n const error = new Error(payload?.error?.message || payload?.message || `OpenClaw request failed: ${response.status}`);\n error.statusCode = response.status;\n throw error;\n }\n\n const message = extractOpenClawText(payload);\n if (!message) {\n const error = new Error('OpenClaw returned an empty response');\n error.statusCode = 502;\n throw error;\n }\n\n return {\n message,\n runId: payload.runId || payload.id || payload.result?.runId || null,\n provider: runtime.provider,\n model: runtime.model,\n usage: extractOpenClawUsage(payload),\n };\n}\n\nasync function runAgentModel({ messages, user, conversationId, mode, callbacks = {}, signal }) {\n const openClawRuntime = createOpenClawRuntime();\n if (openClawRuntime.enabled && !callbacks.onToken) {\n return await callOpenClawAgent({ messages, user, conversationId, mode, signal });\n }\n\n const codexRuntime = createCodexDeepSeekRuntime();\n const libreChatRuntime = createLibreChatAgentRuntime();\n let provider = libreChatRuntime.enabled\n ? libreChatRuntime.provider\n : codexRuntime.enabled\n ? codexRuntime.provider\n : 'deepseek';\n let model = libreChatRuntime.enabled ? libreChatRuntime.model : deepseekModel;\n\n try {\n if (libreChatRuntime.enabled) {\n const result = callbacks.onToken\n ? await callLibreChatAgentStream(messages, { ...callbacks, signal })\n : await callLibreChatAgent(messages);\n return { ...result, provider, model };\n }\n if (codexRuntime.enabled) {\n const result = await callCodexSdkDeepSeek(messages, { ...callbacks, signal });\n return { ...result, provider, model };\n }\n const result = callbacks.onToken\n ? await callDeepSeekStream(messages, { ...callbacks, signal })\n : await callDeepSeek(messages);\n return { ...result, provider, model };\n } catch (error) {\n if (!codexRuntime.enabled || isDeepSeekQuotaError(error)) throw error;\n logger.warn({ error: String(error) }, 'Agent model failed, falling back to DeepSeek direct');\n const result = callbacks.onToken\n ? await callDeepSeekStream(messages, { ...callbacks, signal })\n : await callDeepSeek(messages);\n return { ...result, provider: 'deepseek', model: deepseekModel };\n }\n}\n\nfunction updateAgentRunStatus(run, status, fields = {}) {\n statements.updateAgentRunStatus.run({\n id: run.id,\n userId: run.user_id || run.userId,\n status,\n openClawRunId: fields.openClawRunId ?? null,\n inputTokens: fields.inputTokens ?? null,\n outputTokens: fields.outputTokens ?? null,\n toolCallCount: fields.toolCallCount ?? null,\n completedAt: status === 'completed' || status === 'cancelled' ? nowIso() : null,\n failedAt: status === 'failed' ? nowIso() : null,\n errorCode: fields.errorCode ?? null,\n errorMessage: fields.errorMessage ?? null,\n });\n}\n\nfunction buildAgentMessages(conversationId) {\n const historyRows = statements.listMessages.all(conversationId).slice(-14);\n return [{ role: 'system', content: systemPrompt }, ...normalizeMessages(historyRows)];\n}\n\nfunction agentUsagePayload(usage, user, budget) {\n const promptTokens = usage.promptTokens || usage.inputTokens || 0;\n const completionTokens = usage.completionTokens || usage.outputTokens || 0;\n const totalTokens = usage.totalTokens || promptTokens + completionTokens;\n const latestUsage = usageFor(user.userId);\n return {\n inputTokens: promptTokens,\n outputTokens: completionTokens,\n totalTokens,\n monthlyLimit: budget.limit,\n remainingTokens: Math.max(0, budget.limit - latestUsage.used),\n };\n}\n\napp.post(\n '/codex-deepseek/v1/responses',\n asyncHandler(async (req, res) => {\n const bearer = bearerToken(req);\n if (!deepseekApiKey || bearer !== deepseekApiKey) {\n res.status(401).json({\n error: { message: 'Unauthorized Codex DeepSeek adapter request' },\n });\n return;\n }\n\n const prompt = codexResponsesPrompt(req.body);\n if (!prompt) {\n res.status(400).json({ error: { message: 'Responses input is required' } });\n return;\n }\n\n const model = String(req.body?.model || deepseekModel);\n const responseId = `resp_${crypto.randomUUID().replaceAll('-', '')}`;\n const itemId = `msg_${crypto.randomUUID().replaceAll('-', '')}`;\n const wantsStream =\n req.body?.stream !== false ||\n String(req.get('accept') || '').includes('text/event-stream');\n const messages = [{ role: 'user', content: prompt }];\n\n if (!wantsStream) {\n const result = await callDeepSeek(messages);\n res.json(\n responsesPayload({\n responseId,\n itemId,\n status: 'completed',\n model,\n text: result.message,\n usage: result.usage,\n }),\n );\n return;\n }\n\n res.status(200);\n res.set({\n 'Content-Type': 'text/event-stream; charset=utf-8',\n 'Cache-Control': 'no-cache, no-transform',\n Connection: 'keep-alive',\n 'X-Accel-Buffering': 'no',\n });\n res.flushHeaders?.();\n\n writeResponsesEvent(res, 'response.created', {\n type: 'response.created',\n response: responsesPayload({\n responseId,\n itemId,\n status: 'in_progress',\n model,\n }),\n });\n writeResponsesEvent(res, 'response.output_item.added', {\n type: 'response.output_item.added',\n output_index: 0,\n item: {\n id: itemId,\n type: 'message',\n role: 'assistant',\n status: 'in_progress',\n content: [],\n },\n });\n writeResponsesEvent(res, 'response.content_part.added', {\n type: 'response.content_part.added',\n item_id: itemId,\n output_index: 0,\n content_index: 0,\n part: { type: 'output_text', text: '' },\n });\n\n let text = '';\n try {\n const result = await callDeepSeekStream(messages, {\n onToken: (delta) => {\n text += delta;\n writeResponsesEvent(res, 'response.output_text.delta', {\n type: 'response.output_text.delta',\n item_id: itemId,\n output_index: 0,\n content_index: 0,\n delta,\n });\n },\n });\n text = result.message || text;\n writeResponsesEvent(res, 'response.output_text.done', {\n type: 'response.output_text.done',\n item_id: itemId,\n output_index: 0,\n content_index: 0,\n text,\n });\n writeResponsesEvent(res, 'response.content_part.done', {\n type: 'response.content_part.done',\n item_id: itemId,\n output_index: 0,\n content_index: 0,\n part: { type: 'output_text', text },\n });\n writeResponsesEvent(res, 'response.output_item.done', {\n type: 'response.output_item.done',\n output_index: 0,\n item: {\n id: itemId,\n type: 'message',\n role: 'assistant',\n status: 'completed',\n content: [{ type: 'output_text', text }],\n },\n });\n writeResponsesEvent(res, 'response.completed', {\n type: 'response.completed',\n response: responsesPayload({\n responseId,\n itemId,\n status: 'completed',\n model,\n text,\n usage: result.usage,\n }),\n });\n res.end();\n } catch (error) {\n writeResponsesEvent(res, 'response.failed', {\n type: 'response.failed',\n response: responsesPayload({\n responseId,\n itemId,\n status: 'failed',\n model,\n text,\n error,\n }),\n });\n res.end();\n }\n }),\n);\n\napp.get('/health', (_req, res) => {\n const codexRuntime = createCodexDeepSeekRuntime();\n const libreChatRuntime = createLibreChatAgentRuntime();\n const openClawRuntime = createOpenClawRuntime();\n jsonResponse(res, 200, {\n status: 'ok',\n service: 'dacheng-ai-backend',\n provider: libreChatRuntime.enabled ? libreChatRuntime.provider : 'deepseek',\n model: libreChatRuntime.enabled ? libreChatRuntime.model : deepseekModel,\n libreChatAgentChatEnabled: libreChatRuntime.enabled,\n libreChatAgentProvider: libreChatRuntime.provider,\n libreChatAgentReason: libreChatRuntime.enabled ? undefined : libreChatRuntime.reason,\n codexSdkAvailable: true,\n codexSdkChatEnabled: codexRuntime.enabled,\n codexSdkProvider: codexRuntime.provider,\n openClawAgentChatEnabled: openClawRuntime.enabled,\n openClawAgentProvider: openClawRuntime.provider,\n openClawAgentReason: openClawRuntime.enabled ? undefined : openClawRuntime.reason,\n cbetaApiRoot,\n timestamp: nowIso(),\n });\n});\n\napp.post(\n '/api/agent/chat',\n asyncHandler(async (req, res) => {\n const message = safeUserText(req.body?.message);\n if (!message) {\n return jsonResponse(res, 400, { success: false, message: 'message is required' });\n }\n\n const user = await resolveUser(req, req.body || {});\n const estimated = estimateTokens(message) + 600;\n const budget = enforceTokenBudget(user, estimated);\n const createdAt = nowIso();\n const conversationId = safeUserText(req.body?.conversationId) || crypto.randomUUID();\n const mode = normalizeAgentMode(req.body?.mode);\n const stream = req.body?.stream === true;\n let conversation = statements.getConversation.get(conversationId, user.userId);\n const isNewConversation = !conversation;\n\n if (isNewConversation) {\n conversation = { id: conversationId, user_id: user.userId, title: titleFromMessage(message) };\n statements.insertConversation.run({\n id: conversationId,\n userId: user.userId,\n title: conversation.title,\n provider: 'agent',\n model: openClawAgentId,\n createdAt,\n updatedAt: createdAt,\n });\n }\n\n const messageId = safeUserText(req.body?.messageId) || crypto.randomUUID();\n statements.insertMessage.run({\n id: messageId,\n conversationId,\n role: 'user',\n content: message,\n promptTokens: 0,\n completionTokens: 0,\n totalTokens: 0,\n createdAt,\n });\n\n const runId = `run_${crypto.randomUUID().replaceAll('-', '')}`;\n const sessionKey = openClawSessionKey(user, conversationId);\n const openClawRuntime = createOpenClawRuntime();\n statements.insertAgentRun.run({\n id: runId,\n userId: user.userId,\n conversationId,\n messageId,\n openClawRunId: null,\n openClawSessionKey: sessionKey,\n status: stream ? 'queued' : 'running',\n mode,\n provider: openClawRuntime.enabled ? openClawRuntime.provider : 'dacheng-ai-fallback',\n model: openClawRuntime.enabled ? openClawAgentId : deepseekModel,\n startedAt: createdAt,\n });\n\n if (stream) {\n return jsonResponse(res, 200, {\n success: true,\n runId,\n conversationId,\n streamUrl: `/api/agent/runs/${runId}/events`,\n });\n }\n\n const controller = new AbortController();\n activeAgentRuns.set(runId, controller);\n try {\n const modelMessages = buildAgentMessages(conversationId);\n const aiResult = await runAgentModel({\n messages: modelMessages,\n user,\n conversationId,\n mode,\n signal: controller.signal,\n });\n const usage = {\n promptTokens: aiResult.usage?.promptTokens || estimateTokens(JSON.stringify(modelMessages)),\n completionTokens: aiResult.usage?.completionTokens || estimateTokens(aiResult.message),\n totalTokens:\n aiResult.usage?.totalTokens ||\n estimateTokens(JSON.stringify(modelMessages)) + estimateTokens(aiResult.message),\n };\n recordUsage(user.userId, usage.totalTokens);\n const answeredAt = nowIso();\n const assistantMessageId = crypto.randomUUID();\n statements.insertMessage.run({\n id: assistantMessageId,\n conversationId,\n role: 'assistant',\n content: aiResult.message,\n promptTokens: usage.promptTokens,\n completionTokens: usage.completionTokens,\n totalTokens: usage.totalTokens,\n createdAt: answeredAt,\n });\n statements.updateConversation.run({\n id: conversationId,\n userId: user.userId,\n title: isNewConversation ? titleFromMessage(message) : conversation.title,\n updatedAt: answeredAt,\n });\n updateAgentRunStatus({ id: runId, user_id: user.userId }, 'completed', {\n openClawRunId: aiResult.runId,\n inputTokens: usage.promptTokens,\n outputTokens: usage.completionTokens,\n });\n return jsonResponse(res, 200, {\n success: true,\n runId,\n conversationId,\n messageId: assistantMessageId,\n provider: aiResult.provider,\n model: aiResult.model,\n message: aiResult.message,\n usage: agentUsagePayload(usage, user, budget),\n });\n } catch (error) {\n const status = error.name === 'AbortError' ? 'cancelled' : 'failed';\n updateAgentRunStatus({ id: runId, user_id: user.userId }, status, {\n errorCode: error.name || 'AGENT_RUN_FAILED',\n errorMessage: publicAiErrorMessage(error),\n });\n if (status === 'cancelled') {\n return jsonResponse(res, 499, { success: false, runId, status: 'cancelled' });\n }\n throw error;\n } finally {\n activeAgentRuns.delete(runId);\n }\n }),\n);\n\napp.get(\n '/api/agent/runs/:runId/events',\n asyncHandler(async (req, res) => {\n const user = await resolveUser(req, {});\n const run = statements.getAgentRun.get(req.params.runId, user.userId);\n if (!run) {\n return jsonResponse(res, 404, { success: false, message: 'run not found' });\n }\n if (run.status === 'completed' || run.status === 'failed' || run.status === 'cancelled') {\n return jsonResponse(res, 409, { success: false, message: `run is already ${run.status}` });\n }\n\n sseHeaders(res);\n const controller = new AbortController();\n activeAgentRuns.set(run.id, controller);\n updateAgentRunStatus(run, 'running');\n writeSse(res, 'run.started', { runId: run.id, conversationId: run.conversation_id });\n\n try {\n const estimated = 600;\n const budget = enforceTokenBudget(user, estimated);\n let streamedText = '';\n const modelMessages = buildAgentMessages(run.conversation_id);\n const aiResult = await runAgentModel({\n messages: modelMessages,\n user,\n conversationId: run.conversation_id,\n mode: run.mode,\n signal: controller.signal,\n callbacks: {\n onStep: (step) => writeSse(res, 'tool.call.completed', {\n tool: 'agent.step',\n displayName: step.title || 'Agent step',\n summary: step.message || '',\n }),\n onToken: (delta) => {\n streamedText += delta;\n writeSse(res, 'assistant.delta', { text: delta });\n },\n },\n });\n const message = aiResult.message || streamedText.trim();\n const usage = {\n promptTokens: aiResult.usage?.promptTokens || estimateTokens(JSON.stringify(modelMessages)),\n completionTokens: aiResult.usage?.completionTokens || estimateTokens(message),\n totalTokens:\n aiResult.usage?.totalTokens || estimateTokens(JSON.stringify(modelMessages)) + estimateTokens(message),\n };\n recordUsage(user.userId, usage.totalTokens);\n const assistantMessageId = crypto.randomUUID();\n const answeredAt = nowIso();\n statements.insertMessage.run({\n id: assistantMessageId,\n conversationId: run.conversation_id,\n role: 'assistant',\n content: message,\n promptTokens: usage.promptTokens,\n completionTokens: usage.completionTokens,\n totalTokens: usage.totalTokens,\n createdAt: answeredAt,\n });\n statements.updateConversation.run({\n id: run.conversation_id,\n userId: user.userId,\n title: titleFromMessage(message),\n updatedAt: answeredAt,\n });\n updateAgentRunStatus(run, 'completed', {\n openClawRunId: aiResult.runId,\n inputTokens: usage.promptTokens,\n outputTokens: usage.completionTokens,\n });\n writeSse(res, 'assistant.message', { messageId: assistantMessageId, content: message });\n writeSse(res, 'run.completed', { usage: agentUsagePayload(usage, user, budget) });\n res.end();\n } catch (error) {\n const status = error.name === 'AbortError' ? 'cancelled' : 'failed';\n updateAgentRunStatus(run, status, {\n errorCode: error.name || 'AGENT_RUN_FAILED',\n errorMessage: publicAiErrorMessage(error),\n });\n writeSse(res, status === 'cancelled' ? 'run.cancelled' : 'run.failed', {\n runId: run.id,\n message: publicAiErrorMessage(error),\n });\n res.end();\n } finally {\n activeAgentRuns.delete(run.id);\n }\n }),\n);\n\napp.post(\n '/api/agent/runs/:runId/cancel',\n asyncHandler(async (req, res) => {\n const user = await resolveUser(req, req.body || {});\n const run = statements.getAgentRun.get(req.params.runId, user.userId);\n if (!run) {\n return jsonResponse(res, 404, { success: false, message: 'run not found' });\n }\n const controller = activeAgentRuns.get(run.id);\n if (controller) controller.abort();\n updateAgentRunStatus(run, 'cancelled');\n return jsonResponse(res, 200, { success: true, runId: run.id, status: 'cancelled' });\n }),\n);\n\napp.post(\n '/api/agent/messages/:messageId/feedback',\n asyncHandler(async (req, res) => {\n const user = await resolveUser(req, req.body || {});\n const rating = safeUserText(req.body?.rating);\n if (!['up', 'down'].includes(rating)) {\n return jsonResponse(res, 400, { success: false, message: 'rating must be up or down' });\n }\n statements.insertAgentFeedback.run({\n id: crypto.randomUUID(),\n messageId: req.params.messageId,\n userId: user.userId,\n rating,\n reason: safeUserText(req.body?.reason),\n comment: safeUserText(req.body?.comment),\n createdAt: nowIso(),\n });\n return jsonResponse(res, 200, { success: true });\n }),\n);\n\napp.post(\n '/api/ai/chat',\n asyncHandler(async (req, res) => {\n const message = safeUserText(req.body?.message);\n if (!message) {\n return jsonResponse(res, 400, { success: false, message: 'message is required' });\n }\n\n const user = await resolveUser(req, req.body || {});\n const estimated = estimateTokens(message) + 600;\n const budget = enforceTokenBudget(user, estimated);\n const createdAt = nowIso();\n const conversationId = safeUserText(req.body?.conversationId) || crypto.randomUUID();\n let conversation = statements.getConversation.get(conversationId, user.userId);\n const isNewConversation = !conversation;\n\n if (isNewConversation) {\n conversation = {\n id: conversationId,\n user_id: user.userId,\n title: titleFromMessage(message),\n };\n statements.insertConversation.run({\n id: conversationId,\n userId: user.userId,\n title: conversation.title,\n provider: 'deepseek',\n model: deepseekModel,\n createdAt,\n updatedAt: createdAt,\n });\n }\n\n statements.insertMessage.run({\n id: crypto.randomUUID(),\n conversationId,\n role: 'user',\n content: message,\n promptTokens: 0,\n completionTokens: 0,\n totalTokens: 0,\n createdAt,\n });\n\n const libreChatRuntime = createLibreChatAgentRuntime();\n const skillResult = await runResourceFinderSkill({ message, user });\n const skillContext = resourceContextMessage(skillResult);\n const historyRows = statements.listMessages.all(conversationId).slice(-14);\n const modelMessages = [\n { role: 'system', content: systemPrompt },\n ...(skillContext ? [{ role: 'system', content: skillContext }] : []),\n ...normalizeMessages(historyRows),\n ];\n\n const openClawRuntime = createOpenClawRuntime();\n const codexRuntime = createCodexDeepSeekRuntime();\n let provider = openClawRuntime.enabled\n ? openClawRuntime.provider\n : libreChatRuntime.enabled\n ? libreChatRuntime.provider\n : 'deepseek';\n let responseModel = openClawRuntime.enabled\n ? openClawRuntime.model\n : libreChatRuntime.enabled\n ? libreChatRuntime.model\n : deepseekModel;\n let aiResult;\n try {\n if (openClawRuntime.enabled) {\n aiResult = await callOpenClawAgent({\n messages: modelMessages,\n user,\n conversationId,\n mode: normalizeAgentMode(req.body?.mode),\n });\n provider = aiResult.provider || provider;\n responseModel = aiResult.model || responseModel;\n } else if (libreChatRuntime.enabled) {\n aiResult = await callLibreChatAgent(modelMessages);\n } else if (codexRuntime.enabled) {\n provider = codexRuntime.provider;\n aiResult = await callCodexSdkDeepSeek(modelMessages);\n } else {\n aiResult = await callDeepSeek(modelMessages);\n }\n } catch (error) {\n if (libreChatRuntime.enabled) {\n logger.warn({ error: String(error) }, 'LibreChat Agent chat failed, falling back to Codex SDK or DeepSeek direct');\n provider = codexRuntime.enabled ? codexRuntime.provider : 'deepseek';\n responseModel = deepseekModel;\n if (codexRuntime.enabled) {\n try {\n aiResult = await callCodexSdkDeepSeek(modelMessages);\n } catch (codexError) {\n if (isDeepSeekQuotaError(codexError)) throw codexError;\n logger.warn({ error: String(codexError) }, 'Codex SDK chat failed, falling back to DeepSeek direct');\n provider = 'deepseek';\n aiResult = await callDeepSeek(modelMessages);\n }\n } else {\n aiResult = await callDeepSeek(modelMessages);\n }\n } else {\n if (!codexRuntime.enabled || isDeepSeekQuotaError(error)) throw error;\n logger.warn({ error: String(error) }, 'Codex SDK chat failed, falling back to DeepSeek direct');\n provider = 'deepseek';\n aiResult = await callDeepSeek(modelMessages);\n }\n }\n const usage = {\n promptTokens: aiResult.usage.promptTokens || estimateTokens(JSON.stringify(modelMessages)),\n completionTokens: aiResult.usage.completionTokens || estimateTokens(aiResult.message),\n totalTokens:\n aiResult.usage.totalTokens ||\n estimateTokens(JSON.stringify(modelMessages)) + estimateTokens(aiResult.message),\n };\n recordUsage(user.userId, usage.totalTokens);\n\n const answeredAt = nowIso();\n statements.insertMessage.run({\n id: crypto.randomUUID(),\n conversationId,\n role: 'assistant',\n content: aiResult.message,\n promptTokens: usage.promptTokens,\n completionTokens: usage.completionTokens,\n totalTokens: usage.totalTokens,\n createdAt: answeredAt,\n });\n statements.updateConversation.run({\n id: conversationId,\n userId: user.userId,\n title: isNewConversation ? titleFromMessage(message) : conversation.title,\n updatedAt: answeredAt,\n });\n\n const latestUsage = usageFor(user.userId);\n const monthlyLimit = budget.limit;\n jsonResponse(res, 200, {\n success: true,\n conversationId,\n provider,\n model: responseModel,\n message: aiResult.message,\n usage: {\n ...usage,\n monthlyLimit,\n remainingTokens: Math.max(0, monthlyLimit - latestUsage.used),\n },\n });\n }),\n);\n\napp.post('/api/ai/chat/stream', async (req, res) => {\n sseHeaders(res);\n\n try {\n const message = safeUserText(req.body?.message);\n if (!message) {\n writeSse(res, 'error', { message: 'message is required' });\n res.end();\n return;\n }\n\n const user = await resolveUser(req, req.body || {});\n const estimated = estimateTokens(message) + 600;\n const budget = enforceTokenBudget(user, estimated);\n const createdAt = nowIso();\n const conversationId = safeUserText(req.body?.conversationId) || crypto.randomUUID();\n let conversation = statements.getConversation.get(conversationId, user.userId);\n const isNewConversation = !conversation;\n\n if (isNewConversation) {\n conversation = {\n id: conversationId,\n user_id: user.userId,\n title: titleFromMessage(message),\n };\n statements.insertConversation.run({\n id: conversationId,\n userId: user.userId,\n title: conversation.title,\n provider: 'deepseek',\n model: deepseekModel,\n createdAt,\n updatedAt: createdAt,\n });\n }\n\n statements.insertMessage.run({\n id: crypto.randomUUID(),\n conversationId,\n role: 'user',\n content: message,\n promptTokens: 0,\n completionTokens: 0,\n totalTokens: 0,\n createdAt,\n });\n\n const codexRuntime = createCodexDeepSeekRuntime();\n const libreChatRuntime = createLibreChatAgentRuntime();\n const initialOpenClawRuntime = createOpenClawRuntime();\n let provider = initialOpenClawRuntime.enabled\n ? initialOpenClawRuntime.provider\n : libreChatRuntime.enabled\n ? libreChatRuntime.provider\n : codexRuntime.enabled\n ? codexRuntime.provider\n : 'deepseek';\n let responseModel = initialOpenClawRuntime.enabled\n ? initialOpenClawRuntime.model\n : libreChatRuntime.enabled\n ? libreChatRuntime.model\n : deepseekModel;\n writeSse(res, 'meta', {\n conversationId,\n provider,\n model: responseModel,\n });\n const skillResult = await runResourceFinderSkill({\n message,\n user,\n onStep: (step) => writeSse(res, 'step', step),\n });\n const skillContext = resourceContextMessage(skillResult);\n const historyRows = statements.listMessages.all(conversationId).slice(-14);\n const modelMessages = [\n { role: 'system', content: systemPrompt },\n ...(skillContext ? [{ role: 'system', content: skillContext }] : []),\n ...normalizeMessages(historyRows),\n ];\n\n let aiResult;\n const openClawRuntime = initialOpenClawRuntime;\n try {\n if (openClawRuntime.enabled) {\n aiResult = await callOpenClawAgent({\n messages: modelMessages,\n user,\n conversationId,\n mode: normalizeAgentMode(req.body?.mode),\n });\n provider = aiResult.provider || provider;\n responseModel = aiResult.model || responseModel;\n writeSse(res, 'delta', { text: aiResult.message });\n } else if (libreChatRuntime.enabled) {\n aiResult = await callLibreChatAgentStream(modelMessages, {\n onToken: (token) => writeSse(res, 'delta', { text: token }),\n });\n } else if (codexRuntime.enabled) {\n aiResult = await callCodexSdkDeepSeek(modelMessages, {\n onToken: (token) => writeSse(res, 'delta', { text: token }),\n onStep: (step) => writeSse(res, 'step', step),\n });\n } else {\n aiResult = await callDeepSeekStream(modelMessages, {\n onToken: (token) => writeSse(res, 'delta', { text: token }),\n });\n }\n } catch (error) {\n if (libreChatRuntime.enabled) {\n logger.warn({ error: String(error) }, 'LibreChat Agent streaming chat failed, falling back to Codex SDK or DeepSeek direct');\n provider = codexRuntime.enabled ? codexRuntime.provider : 'deepseek';\n responseModel = deepseekModel;\n writeSse(res, 'meta', {\n conversationId,\n provider,\n model: responseModel,\n });\n if (codexRuntime.enabled) {\n try {\n aiResult = await callCodexSdkDeepSeek(modelMessages, {\n onToken: (token) => writeSse(res, 'delta', { text: token }),\n onStep: (step) => writeSse(res, 'step', step),\n });\n } catch (codexError) {\n if (isDeepSeekQuotaError(codexError)) throw codexError;\n logger.warn({ error: String(codexError) }, 'Codex SDK streaming chat failed, falling back to DeepSeek direct');\n provider = 'deepseek';\n responseModel = deepseekModel;\n writeSse(res, 'meta', {\n conversationId,\n provider,\n model: responseModel,\n });\n aiResult = await callDeepSeekStream(modelMessages, {\n onToken: (token) => writeSse(res, 'delta', { text: token }),\n });\n }\n } else {\n aiResult = await callDeepSeekStream(modelMessages, {\n onToken: (token) => writeSse(res, 'delta', { text: token }),\n });\n }\n } else {\n if (!codexRuntime.enabled || isDeepSeekQuotaError(error)) throw error;\n logger.warn({ error: String(error) }, 'Codex SDK streaming chat failed, falling back to DeepSeek direct');\n provider = 'deepseek';\n responseModel = deepseekModel;\n writeSse(res, 'meta', {\n conversationId,\n provider,\n model: responseModel,\n });\n aiResult = await callDeepSeekStream(modelMessages, {\n onToken: (token) => writeSse(res, 'delta', { text: token }),\n });\n }\n }\n\n const usage = {\n promptTokens: aiResult.usage.promptTokens || estimateTokens(JSON.stringify(modelMessages)),\n completionTokens: aiResult.usage.completionTokens || estimateTokens(aiResult.message),\n totalTokens:\n aiResult.usage.totalTokens ||\n estimateTokens(JSON.stringify(modelMessages)) + estimateTokens(aiResult.message),\n };\n recordUsage(user.userId, usage.totalTokens);\n\n const answeredAt = nowIso();\n statements.insertMessage.run({\n id: crypto.randomUUID(),\n conversationId,\n role: 'assistant',\n content: aiResult.message,\n promptTokens: usage.promptTokens,\n completionTokens: usage.completionTokens,\n totalTokens: usage.totalTokens,\n createdAt: answeredAt,\n });\n statements.updateConversation.run({\n id: conversationId,\n userId: user.userId,\n title: isNewConversation ? titleFromMessage(message) : conversation.title,\n updatedAt: answeredAt,\n });\n\n const latestUsage = usageFor(user.userId);\n const monthlyLimit = budget.limit;\n writeSse(res, 'done', {\n conversationId,\n provider,\n model: responseModel,\n message: aiResult.message,\n usage: {\n ...usage,\n monthlyLimit,\n remainingTokens: Math.max(0, monthlyLimit - latestUsage.used),\n },\n });\n res.end();\n } catch (error) {\n logger.error({ error: error.stack || String(error) }, 'streaming chat failed');\n writeSse(res, 'error', {\n message: publicAiErrorMessage(error),\n details: error.details,\n });\n res.end();\n }\n});\n\napp.get(\n '/api/ai/conversations',\n asyncHandler(async (req, res) => {\n const user = await resolveUser(req, { username: req.query.username });\n const items = statements.listConversations.all(user.userId).map((item) => ({\n id: item.id,\n title: item.title,\n updatedAt: item.updatedAt,\n }));\n jsonResponse(res, 200, { success: true, items });\n }),\n);\n\napp.get(\n '/api/ai/conversations/:id',\n asyncHandler(async (req, res) => {\n const user = await resolveUser(req);\n const conversation = statements.getConversation.get(req.params.id, user.userId);\n if (!conversation) {\n return jsonResponse(res, 404, { success: false, message: 'conversation not found' });\n }\n const messages = statements.listMessages.all(req.params.id).map((item) => ({\n role: item.role,\n content: item.content,\n createdAt: item.createdAt,\n }));\n jsonResponse(res, 200, {\n success: true,\n conversation: {\n id: conversation.id,\n title: conversation.title,\n updatedAt: conversation.updated_at,\n },\n messages,\n });\n }),\n);\n\nfunction buildUrl(base, endpoint, params = {}) {\n const url = new URL(endpoint, `${base}/`);\n for (const [key, value] of Object.entries(params)) {\n if (Array.isArray(value)) {\n for (const item of value) {\n if (item !== undefined && item !== null && item !== '') {\n url.searchParams.append(key, String(item));\n }\n }\n } else if (value !== undefined && value !== null && value !== '') {\n url.searchParams.set(key, String(value));\n }\n }\n return url;\n}\n\nasync function fetchJson(url) {\n const response = await fetch(url, {\n headers: { Accept: 'application/json' },\n signal: AbortSignal.timeout(15_000),\n });\n if (!response.ok) {\n throw new Error(`Request failed: ${response.status}`);\n }\n return response.json();\n}\n\nfunction stripHtml(value) {\n return String(value || '')\n .replace(/<script[\\s\\S]*?<\\/script>/gi, ' ')\n .replace(/<style[\\s\\S]*?<\\/style>/gi, ' ')\n .replace(/<[^>]+>/g, ' ')\n .replace(/ /g, ' ')\n .replace(/</g, '<')\n .replace(/>/g, '>')\n .replace(/&/g, '&')\n .replace(/&#(\\d+);/g, (_match, code) => String.fromCharCode(Number(code)))\n .replace(/\\s+/g, ' ')\n .trim();\n}\n\nfunction cleanExtractedText(value) {\n let text = String(value || '')\n .replace(/\\b[A-Z]{1,3}\\d{2}n\\d{4,5}[a-z]?_p?\\d{4}[a-z]\\d{2}\\b/g, ' ')\n .replace(/\\uFFFD/g, '')\n .replace(/\\s+([,。;:!?、」』)》】])/g, '$1')\n .replace(/([「『《(【])\\s+/g, '$1');\n for (let i = 0; i < 3; i += 1) {\n text = text.replace(/([\\u3400-\\u9fff])\\s+([\\u3400-\\u9fff])/g, '$1$2');\n }\n return text.replace(/[ \\t]{2,}/g, ' ').trim();\n}\n\nfunction compactText(value, limit = 140) {\n const text = stripHtml(value);\n return text.length > limit ? `${text.slice(0, limit)}...` : text;\n}\n\nfunction collectSearchResults(data) {\n if (!data || typeof data !== 'object') return [];\n if (Array.isArray(data.results)) return data.results;\n if (Array.isArray(data?.results?.docs)) return data.results.docs;\n if (Array.isArray(data?.response?.docs)) return data.response.docs;\n if (Array.isArray(data.docs)) return data.docs;\n return [];\n}\n\nfunction firstText(item, keys) {\n for (const key of keys) {\n const value = item?.[key];\n if (Array.isArray(value)) {\n const first = value.map((entry) => safeUserText(String(entry))).find(Boolean);\n if (first) return first;\n }\n if (typeof value === 'string' && value.trim()) return value.trim();\n if (typeof value === 'number') return String(value);\n }\n return '';\n}\n\nfunction firstNumber(item, keys, fallback = undefined) {\n for (const key of keys) {\n const value = item?.[key];\n if (typeof value === 'number') return value;\n if (typeof value === 'string' && value.trim() && Number.isFinite(Number(value))) {\n return Number(value);\n }\n }\n return fallback;\n}\n\nasync function searchCbetaTitle(query, limit) {\n const url = buildUrl(cbetaApiRoot, 'search/title', { q: query, start: 0, rows: limit });\n const data = await fetchJson(url);\n return collectSearchResults(data).flatMap((item) => {\n const work = firstText(item, ['work', 'work_id', 'workId']);\n const title = firstText(item, ['content', 'title', 'work_title', 'workTitle']);\n if (!work || !title) return [];\n const juan = firstNumber(item, ['juan', 'juan_num', 'juanNum'], 1);\n return [\n {\n id: `cbeta-title-${work}-${juan}`,\n title,\n sourceName: 'CBETA 佛典',\n url: `cbeta:${work}:${juan}`,\n snippet: firstText(item, ['byline', 'creators', 'time_dynasty']) || '可下载经文正文并用于全球法布施。',\n resourceType: 'scripture',\n work,\n juan,\n },\n ];\n });\n}\n\nasync function searchCbetaContent(query, limit) {\n const endpoints = ['search/fulltext', 'search/content', 'search/all_in_one'];\n const settled = await Promise.allSettled(\n endpoints.map((endpoint) =>\n fetchJson(buildUrl(cbetaApiRoot, endpoint, { q: query, start: 0, rows: limit, field: 'content' })),\n ),\n );\n const results = [];\n for (const response of settled) {\n if (response.status !== 'fulfilled') continue;\n for (const item of collectSearchResults(response.value)) {\n const work = firstText(item, ['work', 'work_id', 'workId', 'sutra', 'id']);\n const snippet = compactText(firstText(item, ['kwic', 'highlight', 'snippet', 'content', 'text', 'body', 'p']));\n if (!work || !snippet) continue;\n const juan = firstNumber(item, ['juan', 'juan_num', 'juanNum', '卷'], 1);\n const title = firstText(item, ['title', 'work_title', 'workTitle', 'sutra_name', 'book', 'name']) || work;\n results.push({\n id: `cbeta-content-${work}-${juan}-${sha256(snippet).slice(0, 8)}`,\n title,\n sourceName: 'CBETA 佛典',\n url: `cbeta:${work}:${juan}`,\n snippet,\n resourceType: 'scripture',\n work,\n juan,\n });\n if (results.length >= limit) return results;\n }\n }\n return results;\n}\n\nfunction directUrlResource(query) {\n const url = safeUserText(query);\n let parsed;\n try {\n parsed = new URL(url);\n } catch {\n return null;\n }\n if (!['http:', 'https:'].includes(parsed.protocol)) return null;\n return {\n id: `url-${sha256(url).slice(0, 12)}`,\n title: parsed.hostname,\n sourceName: '网页资源',\n url,\n snippet: '直接下载并提取这个链接的可读正文。',\n resourceType: 'web',\n };\n}\n\nfunction looksLikeAudioQuery(query) {\n return /音乐|音樂|音频|音訊|mp3|audio|念诵|念誦|诵经|誦經|唱诵|唱誦|读诵|讀誦/.test(\n safeUserText(query).toLowerCase(),\n );\n}\n\nfunction archiveSearchTerm(query) {\n return (\n safeUserText(query)\n .replace(/下载|查找|寻找|找一?个|资源|音频|音訊|音乐|音樂|mp3|audio|念诵|念誦|诵经|誦經|唱诵|唱誦|读诵|讀誦/gi, ' ')\n .replace(/\\s+/g, ' ')\n .trim() || safeUserText(query)\n );\n}\n\nfunction solrPhrase(value) {\n return String(value).replace(/\\\\/g, '\\\\\\\\').replace(/\"/g, '\\\\\"');\n}\n\nasync function searchInternetArchiveAudio(query, limit) {\n if (!looksLikeAudioQuery(query)) return [];\n const term = archiveSearchTerm(query);\n if (!term) return [];\n const variants = Array.from(\n new Set([\n term,\n term.replaceAll('金刚经', '金剛經'),\n term.replaceAll('金剛經', '金刚经'),\n ].filter(Boolean)),\n );\n const phraseQuery = variants\n .flatMap((item) => [`title:\"${solrPhrase(item)}\"`, `description:\"${solrPhrase(item)}\"`])\n .join(' OR ');\n const url = buildUrl('https://archive.org', 'advancedsearch.php', {\n q: `(${phraseQuery}) AND mediatype:audio`,\n rows: Math.min(Math.max(limit, 1), 10),\n output: 'json',\n 'fl[]': ['identifier', 'title', 'description'],\n });\n const data = await fetchJson(url);\n const docs = Array.isArray(data?.response?.docs) ? data.response.docs : [];\n return docs\n .map((item) => {\n const identifier = firstText(item, ['identifier']);\n const title = firstText(item, ['title']) || identifier;\n if (!identifier || !title) return null;\n return {\n id: `ia-audio-${identifier}`,\n title,\n sourceName: 'Internet Archive 音频',\n url: `ia:${identifier}`,\n snippet: compactText(firstText(item, ['description'])) || '公开可访问的音频资源,将下载第一个可用 MP3 文件。',\n resourceType: 'audio',\n identifier,\n };\n })\n .filter(Boolean);\n}\n\nconst curatedCbetaWorks = [\n {\n work: 'T0235',\n juan: 1,\n title: '金剛般若波羅蜜經',\n keywords: ['金剛般若波羅蜜經', '金刚般若波罗蜜经', '金剛經', '金刚经'],\n snippet: '姚秦 鳩摩羅什譯。常用于读诵、共修与法布施的般若经典。',\n },\n {\n work: 'T0251',\n juan: 1,\n title: '般若波羅蜜多心經',\n keywords: ['般若波羅蜜多心經', '般若波罗蜜多心经', '心經', '心经'],\n snippet: '唐 玄奘譯。篇幅短,适合加入禅室功课本与日常持诵。',\n },\n {\n work: 'T0262',\n juan: 1,\n title: '妙法蓮華經',\n keywords: ['妙法蓮華經', '妙法莲华经', '法華經', '法华经'],\n snippet: '姚秦 鳩摩羅什譯。可按卷下载正文,用于长期功课与分享。',\n },\n {\n work: 'T0279',\n juan: 1,\n title: '大方廣佛華嚴經',\n keywords: ['大方廣佛華嚴經', '大方广佛华严经', '華嚴經', '华严经'],\n snippet: '唐 實叉難陀譯。可按卷下载正文,用于系统研读与法布施。',\n },\n {\n work: 'T0360',\n juan: 1,\n title: '佛說無量壽經',\n keywords: ['佛說無量壽經', '佛说无量寿经', '無量壽經', '无量寿经'],\n snippet: '净土经典。可下载正文,用于净土功课本与全球法布施。',\n },\n {\n work: 'T0365',\n juan: 1,\n title: '佛說觀無量壽佛經',\n keywords: ['佛說觀無量壽佛經', '佛说观无量寿佛经', '觀無量壽經', '观无量寿经'],\n snippet: '净土三经之一。适合查找下载后加入净土功课。',\n },\n {\n work: 'T0366',\n juan: 1,\n title: '佛說阿彌陀經',\n keywords: ['佛說阿彌陀經', '佛说阿弥陀经', '阿彌陀經', '阿弥陀经'],\n snippet: '姚秦 鳩摩羅什譯。篇幅适中,适合共修、持诵与分享。',\n },\n {\n work: 'T0475',\n juan: 1,\n title: '維摩詰所說經',\n keywords: ['維摩詰所說經', '维摩诘所说经', '維摩詰經', '维摩诘经'],\n snippet: '姚秦 鳩摩羅什譯。大乘经典,可按卷下载用于研读。',\n },\n {\n work: 'T0676',\n juan: 1,\n title: '解深密經',\n keywords: ['解深密經', '解深密经'],\n snippet: '唯识相关大乘经典。适合研读、整理笔记与法布施。',\n },\n {\n work: 'T1666',\n juan: 1,\n title: '大乘起信論',\n keywords: ['大乘起信論', '大乘起信论', '起信論', '起信论'],\n snippet: '大乘论典。适合下载后作为研习功课资料。',\n },\n];\n\nfunction localCbetaResults(query, limit) {\n const normalizedQuery = safeUserText(query).toLowerCase();\n if (!normalizedQuery) return [];\n return curatedCbetaWorks\n .filter((item) =>\n item.keywords.some((keyword) => {\n const normalizedKeyword = keyword.toLowerCase();\n return normalizedKeyword.includes(normalizedQuery) || normalizedQuery.includes(normalizedKeyword);\n }),\n )\n .slice(0, limit)\n .map((item) => ({\n id: `cbeta-curated-${item.work}-${item.juan}`,\n title: item.title,\n sourceName: 'CBETA 佛典',\n url: `cbeta:${item.work}:${item.juan}`,\n snippet: item.snippet,\n resourceType: 'scripture',\n work: item.work,\n juan: item.juan,\n }));\n}\n\nfunction uniqueResources(items, limit) {\n const seen = new Set();\n const results = [];\n for (const item of items) {\n const key = `${item.url}:${item.title}`;\n if (seen.has(key)) continue;\n seen.add(key);\n results.push(item);\n if (results.length >= limit) break;\n }\n return results;\n}\n\nasync function findResourceCandidates(query, limit) {\n const direct = directUrlResource(query);\n const results = [];\n if (direct) results.push(direct);\n try {\n results.push(...(await searchInternetArchiveAudio(query, limit)));\n } catch (error) {\n logger.warn({ error: String(error), query }, 'Internet Archive audio search skipped');\n }\n results.push(...localCbetaResults(query, limit));\n\n const [titleResults, contentResults] = await Promise.allSettled([\n searchCbetaTitle(query, limit),\n searchCbetaContent(query, limit),\n ]);\n if (titleResults.status === 'fulfilled') results.push(...titleResults.value);\n if (contentResults.status === 'fulfilled') results.push(...contentResults.value);\n\n return uniqueResources(results, limit);\n}\n\nfunction looksLikeResourceTask(message) {\n const text = safeUserText(message).toLowerCase();\n if (!text) return false;\n return /下载|查找|寻找|找一?个|资源|經文|经文|佛经|佛經|仪轨|儀軌|音乐|音樂|mp3|audio|念诵|念誦|诵经|誦經/.test(text);\n}\n\nfunction extractResourceQuery(message) {\n const text = safeUserText(message)\n .replace(/请|請|帮我|幫我|麻烦|麻煩|可以|能不能|需要|我要|想要/g, ' ')\n .replace(/下载|下載|查找|寻找|尋找|找一个|找一個|找|资源|資源|并|並|然后|然後|用于|用於|全球法布施|加入功课本|加入功課本/g, ' ')\n .replace(/\\s+/g, ' ')\n .trim();\n return text || safeUserText(message);\n}\n\nfunction resourceContextMessage(skillResult) {\n if (!skillResult) return '';\n const lines = [\n '本轮资源检索/下载已经由大乘 App 后端完成。',\n '不要再次调用 search_dharma_resources、download_dharma_resource 或其他资源 MCP 工具;请直接基于以下结果回答用户。',\n `后端已调用 skill: ${skillResult.skillName}`,\n `执行目标: ${skillResult.query}`,\n ];\n if (skillResult.selected) {\n lines.push(`候选资源: ${skillResult.selected.title}`);\n lines.push(`来源: ${skillResult.selected.sourceName}`);\n lines.push(`资源地址: ${skillResult.selected.url}`);\n }\n if (skillResult.downloaded) {\n lines.push(`下载标题: ${skillResult.downloaded.title}`);\n lines.push(`下载文件: ${skillResult.downloaded.fileName}`);\n const text = safeUserText(skillResult.downloaded.contentText);\n if (text) {\n lines.push('下载正文摘录:');\n lines.push(text.slice(0, 8000));\n }\n }\n return lines.join('\\n');\n}\n\nasync function runResourceFinderSkill({ message, user, onStep }) {\n if (!looksLikeResourceTask(message)) return null;\n\n const skillName = 'resource-finder-downloader';\n const query = extractResourceQuery(message);\n const audioTask = looksLikeAudioQuery(message) || looksLikeAudioQuery(query);\n onStep?.({\n type: 'skill.step',\n skillName,\n stage: 'start',\n title: '调用资源查找下载 skill',\n message: `识别到资源任务: ${query}`,\n });\n\n onStep?.({\n type: 'skill.step',\n skillName,\n stage: 'search',\n title: '搜索可分享资源',\n message: audioTask\n ? '优先检索公开可访问音频资源,失败时再回落到佛典正文资源。'\n : '优先检索 CBETA 佛典正文接口,搜索索引不可用时使用内置经典映射。',\n });\n const candidates = await findResourceCandidates(query, 8);\n if (candidates.length === 0) {\n onStep?.({\n type: 'skill.step',\n skillName,\n stage: 'empty',\n title: '没有找到可下载资源',\n message: '后端没有找到明确可下载且可分享的资源。',\n });\n return {\n skillName,\n query,\n candidates: [],\n selected: null,\n downloaded: null,\n };\n }\n\n const selected = candidates[0];\n onStep?.({\n type: 'skill.step',\n skillName,\n stage: 'select',\n title: '选择可信候选资源',\n message: `${selected.title} / ${selected.sourceName}`,\n });\n\n onStep?.({\n type: 'skill.step',\n skillName,\n stage: 'download',\n title: '下载并提取资源',\n message: selected.url,\n });\n const cbeta = parseCbetaResource(selected);\n const internetArchive = parseInternetArchiveResource(selected);\n const downloaded = cbeta\n ? await downloadCbetaResource({ ...selected, ...cbeta })\n : internetArchive\n ? await downloadInternetArchiveAudio({ ...selected, ...internetArchive })\n : await downloadWebResource(selected);\n const persisted = persistDownloadedResource(user, downloaded);\n\n onStep?.({\n type: 'skill.step',\n skillName,\n stage: 'done',\n title: '资源已准备完成',\n message: downloaded.fileName,\n });\n\n return {\n skillName,\n query,\n candidates,\n selected,\n downloaded: {\n ...downloaded,\n downloadId: persisted.id,\n },\n };\n}\n\napp.post(\n '/api/resources/search',\n asyncHandler(async (req, res) => {\n const query = safeUserText(req.body?.query);\n const limit = Math.min(Math.max(Number(req.body?.limit || 12), 1), 20);\n if (query.length < 2) {\n return jsonResponse(res, 400, { success: false, message: 'query is required' });\n }\n\n const items = await findResourceCandidates(query, limit);\n\n jsonResponse(res, 200, {\n success: true,\n source: 'resource-index',\n items,\n });\n }),\n);\n\nfunction parseCbetaResource(input) {\n const work = safeUserText(input.work);\n const juan = Number(input.juan || 1);\n if (work) return { work, juan: Number.isFinite(juan) ? juan : 1 };\n\n const url = safeUserText(input.url);\n const match = /^cbeta:([^:]+):(\\d+)$/i.exec(url);\n if (!match) return null;\n return {\n work: match[1].toUpperCase(),\n juan: Number(match[2] || 1),\n };\n}\n\nfunction parseInternetArchiveResource(input) {\n const url = safeUserText(input.url);\n const match = /^ia:([^:]+)$/i.exec(url);\n const identifier = match?.[1] || safeUserText(input.identifier);\n if (!identifier) return null;\n return { identifier };\n}\n\nfunction safeFileName(value) {\n const normalized = String(value || 'dharma-resource')\n .replace(/[\\\\/:*?\"<>|]+/g, '-')\n .replace(/\\s+/g, '-')\n .replace(/-+/g, '-')\n .replace(/^-|-$/g, '')\n .slice(0, 80);\n return normalized || 'dharma-resource';\n}\n\nfunction extractCbetaHtml(data) {\n const first = Array.isArray(data?.results) ? data.results[0] : null;\n if (typeof first === 'string') return first;\n if (first && typeof first === 'object' && typeof first.html === 'string') return first.html;\n if (typeof data?.html === 'string') return data.html;\n return '';\n}\n\nasync function downloadCbetaResource(resource) {\n const url = buildUrl(cbetaApiRoot, 'juans', {\n work: resource.work,\n juan: resource.juan,\n work_info: 1,\n toc: 1,\n });\n const data = await fetchJson(url);\n const workInfo = data.work_info || {};\n const title = safeUserText(workInfo.title) || safeUserText(resource.title) || resource.work;\n const html = extractCbetaHtml(data);\n const text = cleanExtractedText(stripHtml(html));\n if (!text) throw new Error('CBETA resource returned empty content');\n return {\n title,\n sourceName: 'CBETA 佛典',\n url: `cbeta:${resource.work}:${resource.juan}`,\n contentText: text.slice(0, maxResourceTextChars),\n fileName: `${resource.work}_${resource.juan}_${safeFileName(title)}.txt`,\n };\n}\n\nasync function downloadInternetArchiveAudio(resource) {\n const identifier = safeUserText(resource.identifier);\n if (!identifier) throw new Error('Internet Archive identifier is required');\n const metadata = await fetchJson(`https://archive.org/metadata/${encodeURIComponent(identifier)}`);\n const title = safeUserText(metadata?.metadata?.title) || safeUserText(resource.title) || identifier;\n const files = Array.isArray(metadata?.files) ? metadata.files : [];\n const audioFile = files.find((file) => {\n const name = safeUserText(file?.name);\n const format = safeUserText(file?.format).toLowerCase();\n return /\\.(mp3|m4a|ogg)$/i.test(name) || format.includes('mp3');\n });\n if (!audioFile?.name) {\n throw new Error('Internet Archive audio item has no downloadable audio file');\n }\n const sourceFileName = audioFile.name;\n const fileUrl = `https://archive.org/download/${encodeURIComponent(identifier)}/${sourceFileName\n .split('/')\n .map((part) => encodeURIComponent(part))\n .join('/')}`;\n const response = await fetch(fileUrl, {\n headers: {\n Accept: 'audio/mpeg, audio/*;q=0.9, */*;q=0.5',\n 'User-Agent': 'DachengResourceDownloader/1.0 (+https://ombhrum.com)',\n },\n signal: AbortSignal.timeout(60_000),\n });\n if (!response.ok) {\n throw new Error(`Internet Archive audio download failed: ${response.status}`);\n }\n const binaryContent = Buffer.from(await response.arrayBuffer());\n if (!binaryContent.length) throw new Error('Internet Archive audio returned empty content');\n const sizeMb = (binaryContent.length / 1024 / 1024).toFixed(1);\n return {\n title,\n sourceName: 'Internet Archive 音频',\n url: fileUrl,\n contentText: `已下载音频资源: ${title}\\n文件名: ${sourceFileName}\\n大小: ${sizeMb} MB\\n来源: ${fileUrl}`,\n binaryContent,\n fileName: safeFileName(sourceFileName),\n };\n}\n\nasync function downloadWebResource(input) {\n const url = safeUserText(input.url);\n const parsed = new URL(url);\n if (!['http:', 'https:'].includes(parsed.protocol)) {\n throw new Error('Only http/https resources can be downloaded');\n }\n\n const response = await fetch(parsed, {\n headers: {\n Accept: 'text/html, text/plain, application/xhtml+xml, application/xml;q=0.8, */*;q=0.5',\n 'User-Agent': 'DachengResourceDownloader/1.0 (+https://ombhrum.com)',\n },\n signal: AbortSignal.timeout(25_000),\n });\n if (!response.ok) throw new Error(`Resource download failed: ${response.status}`);\n const contentType = response.headers.get('content-type') || '';\n const body = await response.text();\n const title = safeUserText(input.title) || parsed.hostname;\n const contentText = contentType.includes('html') ? cleanExtractedText(stripHtml(body)) : body.trim();\n if (!contentText) throw new Error('Resource returned empty content');\n return {\n title,\n sourceName: safeUserText(input.sourceName) || parsed.hostname,\n url,\n contentText: contentText.slice(0, maxResourceTextChars),\n fileName: `${safeFileName(title)}.txt`,\n };\n}\n\nfunction persistDownloadedResource(user, content) {\n const id = crypto.randomUUID();\n const fileName = `${id}-${safeFileName(content.fileName)}`;\n const filePath = path.join(resourcesDir, fileName);\n if (content.binaryContent) {\n fs.writeFileSync(filePath, content.binaryContent);\n } else {\n fs.writeFileSync(filePath, content.contentText, 'utf8');\n }\n statements.insertResourceDownload.run({\n id,\n userId: user.userId,\n title: content.title,\n sourceName: content.sourceName,\n sourceUrl: content.url,\n fileName: content.fileName,\n filePath,\n createdAt: nowIso(),\n });\n return { id, fileName, filePath };\n}\n\nfunction createLibreChatMcpServer() {\n const server = new McpServer({\n name: 'dacheng-ai-tools',\n version: '0.1.0',\n });\n\n server.registerTool(\n 'search_dharma_resources',\n {\n title: 'Search dharma resources',\n description:\n 'Search for legally shareable Buddhist scripture, text, web, or audio resources that can be prepared for Fabushi sharing.',\n inputSchema: {\n query: z.string().min(2).max(200),\n limit: z.number().int().min(1).max(12).optional(),\n },\n outputSchema: {\n query: z.string(),\n items: z.array(\n z.object({\n id: z.string(),\n title: z.string(),\n sourceName: z.string(),\n url: z.string(),\n snippet: z.string(),\n resourceType: z.string(),\n work: z.string().optional(),\n juan: z.number().optional(),\n }),\n ),\n },\n annotations: {\n readOnlyHint: true,\n destructiveHint: false,\n openWorldHint: true,\n },\n },\n async ({ query, limit }) => {\n const items = await findResourceCandidates(query, limit ?? 8);\n return textToolResult(`Found ${items.length} candidate resources for \"${query}\".`, {\n query,\n items,\n });\n },\n );\n\n server.registerTool(\n 'download_dharma_resource',\n {\n title: 'Download dharma resource',\n description:\n 'Download and extract a selected resource. Use the url/work/juan returned by search_dharma_resources whenever possible.',\n inputSchema: {\n url: z.string().min(1).max(1000),\n title: z.string().max(300).optional(),\n sourceName: z.string().max(120).optional(),\n work: z.string().max(40).optional(),\n juan: z.number().int().min(1).optional(),\n identifier: z.string().max(300).optional(),\n },\n outputSchema: {\n title: z.string(),\n sourceName: z.string(),\n url: z.string(),\n fileName: z.string(),\n downloadId: z.string(),\n contentText: z.string(),\n },\n annotations: {\n readOnlyHint: false,\n destructiveHint: false,\n openWorldHint: true,\n },\n },\n async (input) => {\n const user = { userId: 'librechat:mcp' };\n const cbeta = parseCbetaResource(input);\n const internetArchive = parseInternetArchiveResource(input);\n const content = cbeta\n ? await downloadCbetaResource({ ...input, ...cbeta })\n : internetArchive\n ? await downloadInternetArchiveAudio({ ...input, ...internetArchive })\n : await downloadWebResource(input);\n const persisted = persistDownloadedResource(user, content);\n const { binaryContent: _binaryContent, ...publicContent } = content;\n return textToolResult(`Downloaded ${publicContent.title} as ${publicContent.fileName}.`, {\n ...publicContent,\n downloadId: persisted.id,\n });\n },\n );\n\n server.registerTool(\n 'prepare_dharma_share_text',\n {\n title: 'Prepare dharma sharing text',\n description:\n 'Prepare a text payload for the Fabushi app to confirm and add to global dharma sharing. This returns a clientAction; it does not start sending.',\n inputSchema: {\n title: z.string().min(1).max(120),\n text: z.string().min(1).max(20000),\n },\n outputSchema: {\n clientAction: z.object({\n type: z.literal('prepare_dharma_share_text'),\n title: z.string(),\n text: z.string(),\n }),\n },\n annotations: {\n readOnlyHint: false,\n destructiveHint: false,\n openWorldHint: false,\n },\n },\n async ({ title, text }) =>\n textToolResult(`Prepared \"${title}\" for user confirmation in the Fabushi app.`, {\n clientAction: {\n type: 'prepare_dharma_share_text',\n title: safeUserText(title).slice(0, 120),\n text: String(text || '').trim().slice(0, 20000),\n },\n }),\n );\n\n server.registerTool(\n 'prepare_practice_book_item',\n {\n title: 'Prepare practice book item',\n description:\n 'Prepare a scripture, chant, or practice item for the Fabushi app practice book. This returns a clientAction; it does not mutate the app by itself.',\n inputSchema: {\n title: z.string().min(1).max(120),\n sourceName: z.string().max(120).optional(),\n text: z.string().min(1).max(20000),\n },\n outputSchema: {\n clientAction: z.object({\n type: z.literal('prepare_practice_book_item'),\n title: z.string(),\n sourceName: z.string(),\n text: z.string(),\n }),\n },\n annotations: {\n readOnlyHint: false,\n destructiveHint: false,\n openWorldHint: false,\n },\n },\n async ({ title, sourceName, text }) =>\n textToolResult(`Prepared \"${title}\" for the Fabushi practice book confirmation flow.`, {\n clientAction: {\n type: 'prepare_practice_book_item',\n title: safeUserText(title).slice(0, 120),\n sourceName: safeUserText(sourceName || '大乘 AI').slice(0, 120),\n text: String(text || '').trim().slice(0, 20000),\n },\n }),\n );\n\n return server;\n}\n\nasync function handleMcpRequest(req, res) {\n const server = createLibreChatMcpServer();\n const transport = new StreamableHTTPServerTransport({\n sessionIdGenerator: undefined,\n enableJsonResponse: true,\n });\n\n res.on('close', () => {\n transport.close();\n server.close();\n });\n\n await server.connect(transport);\n await transport.handleRequest(req, res, req.body);\n}\n\napp.all('/mcp', async (req, res) => {\n try {\n await handleMcpRequest(req, res);\n } catch (error) {\n logger.error({ error: error.stack || String(error) }, 'MCP request failed');\n if (!res.headersSent) {\n res.status(500).json({\n error: 'MCP request failed',\n message: publicAiErrorMessage(error),\n });\n }\n }\n});\n\napp.post(\n '/api/resources/download',\n asyncHandler(async (req, res) => {\n const user = await resolveUser(req, req.body || {});\n const cbeta = parseCbetaResource(req.body || {});\n const internetArchive = parseInternetArchiveResource(req.body || {});\n const content = cbeta\n ? await downloadCbetaResource({ ...req.body, ...cbeta })\n : internetArchive\n ? await downloadInternetArchiveAudio({ ...req.body, ...internetArchive })\n : await downloadWebResource(req.body || {});\n const persisted = persistDownloadedResource(user, content);\n const { binaryContent: _binaryContent, ...publicContent } = content;\n jsonResponse(res, 200, {\n success: true,\n downloadId: persisted.id,\n ...publicContent,\n });\n }),\n);\n\napp.post(\n '/api/codex/resource-task',\n asyncHandler(async (req, res) => {\n if (process.env.ENABLE_CODEX_SDK !== 'true') {\n return jsonResponse(res, 403, {\n success: false,\n message: 'Codex SDK task runner is disabled. Set ENABLE_CODEX_SDK=true on the server to enable it.',\n });\n }\n const prompt = safeUserText(req.body?.prompt);\n if (!prompt) {\n return jsonResponse(res, 400, { success: false, message: 'prompt is required' });\n }\n const { Codex } = await import('@openai/codex-sdk');\n const codexRuntime = createCodexDeepSeekRuntime();\n const codex = new Codex(codexRuntime.enabled ? codexRuntime.options : {});\n const thread = codex.startThread(\n codexRuntime.enabled\n ? codexRuntime.threadOptions\n : { skipGitRepoCheck: true, networkAccessEnabled: true },\n );\n const result = await thread.run(prompt);\n jsonResponse(res, 200, {\n success: true,\n threadId: thread.id,\n provider: codexRuntime.provider,\n model: deepseekModel,\n result,\n });\n }),\n);\n\napp.use((error, _req, res, _next) => {\n const status = Number(error.statusCode || error.status || 500);\n const safeStatus = status >= 400 && status < 600 ? status : 500;\n logger.error({ error: error.stack || String(error) }, 'request failed');\n jsonResponse(res, safeStatus, {\n success: false,\n message: error.message || 'Internal Server Error',\n details: error.details,\n });\n});\n\nconst port = Number(process.env.PORT || 8788);\napp.listen(port, '0.0.0.0', () => {\n logger.info(\n {\n port,\n dbPath,\n dataDir,\n deepseekModel,\n cbetaApiRoot,\n },\n 'Dacheng AI backend started',\n );\n});\n"}, "398965e5ee855175c7e550ac2fa5b539f89aa19f": {"t": 1785757988.4720407, "n": 93694, "h": "8418f58ebc32a567", "c": "import crypto from 'node:crypto';\nimport fs from 'node:fs';\nimport path from 'node:path';\nimport process from 'node:process';\nimport { fileURLToPath } from 'node:url';\n\nimport { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';\nimport { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';\nimport Database from 'better-sqlite3';\nimport cors from 'cors';\nimport dotenv from 'dotenv';\nimport express from 'express';\nimport rateLimit from 'express-rate-limit';\nimport helmet from 'helmet';\nimport pino from 'pino';\nimport { z } from 'zod';\n\ndotenv.config();\n\nconst __dirname = path.dirname(fileURLToPath(import.meta.url));\nconst rootDir = path.resolve(__dirname, '..');\nconst dataDir = process.env.DATA_DIR || path.join(rootDir, 'data');\nconst resourcesDir = path.join(dataDir, 'resources');\nconst codexHomeDir = process.env.CODEX_HOME || path.join(dataDir, 'codex-home');\nconst codexTempDir = process.env.CODEX_TMPDIR || path.join(dataDir, 'codex-tmp');\nconst codexRuntimeDir = process.env.XDG_RUNTIME_DIR || path.join(dataDir, 'codex-runtime');\nconst dbPath = process.env.SQLITE_PATH || path.join(dataDir, 'dacheng-ai.sqlite');\n\nfs.mkdirSync(resourcesDir, { recursive: true });\nfs.mkdirSync(codexHomeDir, { recursive: true });\nfs.mkdirSync(codexTempDir, { recursive: true });\nfs.mkdirSync(codexRuntimeDir, { recursive: true });\n\nconst logger = pino({\n level: process.env.LOG_LEVEL || 'info',\n});\n\nconst app = express();\napp.disable('x-powered-by');\napp.set('trust proxy', Number(process.env.TRUST_PROXY_HOPS || 1));\napp.use(helmet());\nconst corsOrigin =\n !process.env.CORS_ORIGIN || process.env.CORS_ORIGIN.trim() === '*'\n ? true\n : process.env.CORS_ORIGIN.split(',').map((item) => item.trim()).filter(Boolean);\napp.use(cors({ origin: corsOrigin }));\napp.use(express.json({ limit: '1mb' }));\napp.use(\n rateLimit({\n windowMs: 60_000,\n limit: Number(process.env.RATE_LIMIT_PER_MINUTE || 90),\n standardHeaders: true,\n legacyHeaders: false,\n }),\n);\n\nconst db = new Database(dbPath);\ndb.pragma('journal_mode = WAL');\ndb.pragma('foreign_keys = ON');\n\ndb.exec(`\n CREATE TABLE IF NOT EXISTS conversations (\n id TEXT PRIMARY KEY,\n user_id TEXT NOT NULL,\n title TEXT NOT NULL,\n provider TEXT NOT NULL,\n model TEXT NOT NULL,\n created_at TEXT NOT NULL,\n updated_at TEXT NOT NULL\n );\n\n CREATE INDEX IF NOT EXISTS idx_conversations_user_updated\n ON conversations (user_id, updated_at DESC);\n\n CREATE TABLE IF NOT EXISTS messages (\n id TEXT PRIMARY KEY,\n conversation_id TEXT NOT NULL,\n role TEXT NOT NULL,\n content TEXT NOT NULL,\n prompt_tokens INTEGER NOT NULL DEFAULT 0,\n completion_tokens INTEGER NOT NULL DEFAULT 0,\n total_tokens INTEGER NOT NULL DEFAULT 0,\n created_at TEXT NOT NULL,\n FOREIGN KEY (conversation_id) REFERENCES conversations(id) ON DELETE CASCADE\n );\n\n CREATE INDEX IF NOT EXISTS idx_messages_conversation_created\n ON messages (conversation_id, created_at ASC);\n\n CREATE TABLE IF NOT EXISTS usage_months (\n user_id TEXT NOT NULL,\n month TEXT NOT NULL,\n total_tokens INTEGER NOT NULL DEFAULT 0,\n updated_at TEXT NOT NULL,\n PRIMARY KEY (user_id, month)\n );\n\n CREATE TABLE IF NOT EXISTS resource_downloads (\n id TEXT PRIMARY KEY,\n user_id TEXT NOT NULL,\n title TEXT NOT NULL,\n source_name TEXT NOT NULL,\n source_url TEXT NOT NULL,\n file_name TEXT NOT NULL,\n file_path TEXT NOT NULL,\n created_at TEXT NOT NULL\n );\n\n CREATE TABLE IF NOT EXISTS agent_runs (\n id TEXT PRIMARY KEY,\n user_id TEXT NOT NULL,\n conversation_id TEXT NOT NULL,\n message_id TEXT NOT NULL,\n openclaw_run_id TEXT,\n openclaw_session_key TEXT NOT NULL,\n status TEXT NOT NULL,\n mode TEXT NOT NULL,\n provider TEXT NOT NULL,\n model TEXT,\n input_tokens INTEGER NOT NULL DEFAULT 0,\n output_tokens INTEGER NOT NULL DEFAULT 0,\n tool_call_count INTEGER NOT NULL DEFAULT 0,\n cost_microusd INTEGER NOT NULL DEFAULT 0,\n started_at TEXT NOT NULL,\n completed_at TEXT,\n failed_at TEXT,\n error_code TEXT,\n error_message TEXT\n );\n\n CREATE INDEX IF NOT EXISTS idx_agent_runs_user_started\n ON agent_runs (user_id, started_at DESC);\n\n CREATE INDEX IF NOT EXISTS idx_agent_runs_status_started\n ON agent_runs (status, started_at DESC);\n\n CREATE TABLE IF NOT EXISTS agent_message_feedback (\n id TEXT PRIMARY KEY,\n message_id TEXT NOT NULL,\n user_id TEXT NOT NULL,\n rating TEXT NOT NULL,\n reason TEXT,\n comment TEXT,\n created_at TEXT NOT NULL\n );\n`);\n\nconst statements = {\n insertConversation: db.prepare(`\n INSERT INTO conversations (id, user_id, title, provider, model, created_at, updated_at)\n VALUES (@id, @userId, @title, @provider, @model, @createdAt, @updatedAt)\n `),\n updateConversation: db.prepare(`\n UPDATE conversations\n SET title = @title, updated_at = @updatedAt\n WHERE id = @id AND user_id = @userId\n `),\n getConversation: db.prepare(`\n SELECT * FROM conversations WHERE id = ? AND user_id = ? LIMIT 1\n `),\n listConversations: db.prepare(`\n SELECT id, title, updated_at AS updatedAt\n FROM conversations\n WHERE user_id = ?\n ORDER BY updated_at DESC\n LIMIT 30\n `),\n insertMessage: db.prepare(`\n INSERT INTO messages (\n id, conversation_id, role, content,\n prompt_tokens, completion_tokens, total_tokens, created_at\n )\n VALUES (\n @id, @conversationId, @role, @content,\n @promptTokens, @completionTokens, @totalTokens, @createdAt\n )\n `),\n listMessages: db.prepare(`\n SELECT role, content, prompt_tokens AS promptTokens,\n completion_tokens AS completionTokens, total_tokens AS totalTokens,\n created_at AS createdAt\n FROM messages\n WHERE conversation_id = ?\n ORDER BY created_at ASC\n `),\n getUsage: db.prepare(`\n SELECT total_tokens AS totalTokens FROM usage_months WHERE user_id = ? AND month = ?\n `),\n upsertUsage: db.prepare(`\n INSERT INTO usage_months (user_id, month, total_tokens, updated_at)\n VALUES (@userId, @month, @totalTokens, @updatedAt)\n ON CONFLICT(user_id, month)\n DO UPDATE SET total_tokens = usage_months.total_tokens + excluded.total_tokens,\n updated_at = excluded.updated_at\n `),\n insertResourceDownload: db.prepare(`\n INSERT INTO resource_downloads (\n id, user_id, title, source_name, source_url, file_name, file_path, created_at\n )\n VALUES (\n @id, @userId, @title, @sourceName, @sourceUrl, @fileName, @filePath, @createdAt\n )\n `),\n insertAgentRun: db.prepare(`\n INSERT INTO agent_runs (\n id, user_id, conversation_id, message_id, openclaw_run_id, openclaw_session_key,\n status, mode, provider, model, started_at\n )\n VALUES (\n @id, @userId, @conversationId, @messageId, @openClawRunId, @openClawSessionKey,\n @status, @mode, @provider, @model, @startedAt\n )\n `),\n getAgentRun: db.prepare(`\n SELECT * FROM agent_runs WHERE id = ? AND user_id = ? LIMIT 1\n `),\n updateAgentRunStatus: db.prepare(`\n UPDATE agent_runs\n SET status = @status,\n openclaw_run_id = COALESCE(@openClawRunId, openclaw_run_id),\n input_tokens = COALESCE(@inputTokens, input_tokens),\n output_tokens = COALESCE(@outputTokens, output_tokens),\n tool_call_count = COALESCE(@toolCallCount, tool_call_count),\n completed_at = @completedAt,\n failed_at = @failedAt,\n error_code = @errorCode,\n error_message = @errorMessage\n WHERE id = @id AND user_id = @userId\n `),\n insertAgentFeedback: db.prepare(`\n INSERT INTO agent_message_feedback (id, message_id, user_id, rating, reason, comment, created_at)\n VALUES (@id, @messageId, @userId, @rating, @reason, @comment, @createdAt)\n `),\n};\n\nconst deepseekApiKey = process.env.DEEPSEEK_API_KEY || '';\nconst deepseekBaseUrl = (process.env.DEEPSEEK_BASE_URL || 'https://api.deepseek.com').replace(/\\/+$/, '');\nconst deepseekModel = process.env.DEEPSEEK_MODEL || 'deepseek-chat';\nconst cbetaApiRoot = (process.env.CBETA_API_ROOT || 'https://144.24.17.21.sslip.io').replace(/\\/+$/, '');\nconst fabushiApiBaseUrl = (process.env.FABUSHI_API_BASE_URL || 'http://141.148.140.39').replace(/\\/+$/, '');\nconst memberMonthlyLimit = Number(process.env.MEMBER_MONTHLY_TOKEN_LIMIT || 1_000_000);\nconst freeMonthlyLimit = Number(process.env.FREE_MONTHLY_TOKEN_LIMIT || 50_000);\nconst maxResourceTextChars = Number(process.env.MAX_RESOURCE_TEXT_CHARS || 80_000);\nconst enableLibreChatAgentChat = process.env.ENABLE_LIBRECHAT_AGENT_CHAT === 'true';\nconst libreChatAgentsBaseUrl = (\n process.env.LIBRECHAT_AGENTS_BASE_URL || 'http://127.0.0.1:3080/api/agents/v1'\n).replace(/\\/+$/, '');\nconst libreChatAgentApiKey = process.env.LIBRECHAT_AGENT_API_KEY || '';\nconst libreChatAgentId = process.env.LIBRECHAT_AGENT_ID || '';\nconst enableCodexSdkChat = process.env.ENABLE_CODEX_SDK_CHAT === 'true';\nconst enableOpenClawAgentChat = process.env.ENABLE_OPENCLAW_AGENT_CHAT === 'true';\nconst openClawGatewayUrl = (process.env.OPENCLAW_GATEWAY_URL || 'http://127.0.0.1:18789').replace(/\\/+$/, '');\nconst openClawGatewayToken = process.env.OPENCLAW_GATEWAY_TOKEN || '';\nconst openClawAgentId = process.env.OPENCLAW_AGENT_ID || 'fabushi-public-agent';\nconst openClawRunsEndpoint = process.env.OPENCLAW_RUNS_ENDPOINT || '';\nconst codexDeepSeekProviderId = 'deepseek-chat-completions';\nconst codexResponsesBaseUrl = (\n process.env.CODEX_DEEPSEEK_RESPONSES_BASE_URL ||\n `http://127.0.0.1:${process.env.PORT || 8788}/codex-deepseek/v1`\n).replace(/\\/+$/, '');\n\nconst systemPrompt = [\n '你是“大乘”App 的 AI 助手。',\n '你的核心任务是帮助用户查找、整理、理解并全球法布施合法可分享的佛法资源。',\n '回答要庄重、简洁、可执行;涉及经典、仪轨或资源时,提醒用户尊重版权、来源和当地法规。',\n '如果用户想找资源、下载经文、寻找音频或准备可分享资料,你必须先在后端自动执行搜索、验证和下载步骤;不要要求用户去前端 + 菜单手动查找。',\n '你可以把执行进度总结给用户,但不要输出隐藏推理原文。',\n].join('\\n');\n\nfunction nowIso() {\n return new Date().toISOString();\n}\n\nfunction monthKey(date = new Date()) {\n return `${date.getUTCFullYear()}-${String(date.getUTCMonth() + 1).padStart(2, '0')}`;\n}\n\nfunction jsonResponse(res, status, payload) {\n res.status(status).json(payload);\n}\n\nfunction textToolResult(text, structuredContent = {}) {\n return {\n structuredContent,\n content: [{ type: 'text', text }],\n };\n}\n\nfunction sseHeaders(res) {\n res.status(200);\n res.setHeader('Content-Type', 'text/event-stream; charset=utf-8');\n res.setHeader('Cache-Control', 'no-cache, no-transform');\n res.setHeader('Connection', 'keep-alive');\n res.setHeader('X-Accel-Buffering', 'no');\n if (typeof res.flushHeaders === 'function') res.flushHeaders();\n}\n\nfunction writeSse(res, event, payload) {\n res.write(`event: ${event}\\n`);\n res.write(`data: ${JSON.stringify(payload)}\\n\\n`);\n}\n\nfunction asyncHandler(fn) {\n return (req, res, next) => {\n Promise.resolve(fn(req, res, next)).catch(next);\n };\n}\n\nfunction bearerToken(req) {\n const header = req.get('Authorization') || '';\n if (!header.startsWith('Bearer ')) return '';\n return header.slice('Bearer '.length).trim();\n}\n\nfunction sha256(value) {\n return crypto.createHash('sha256').update(value).digest('hex');\n}\n\nfunction safeUserText(value, fallback = '') {\n if (typeof value !== 'string') return fallback;\n return value.replace(/\\u0000/g, '').trim();\n}\n\nfunction estimateTokens(text) {\n const normalized = String(text || '').trim();\n if (!normalized) return 0;\n return Math.max(1, Math.ceil(normalized.length / 3));\n}\n\nfunction normalizeMembershipPayload(payload) {\n if (!payload || typeof payload !== 'object') return false;\n const data = payload.data && typeof payload.data === 'object' ? payload.data : payload;\n return Boolean(\n data.isActive ||\n data.active ||\n data.hasPremiumAccess ||\n data.hasPremiumMembership ||\n data.isPremium ||\n data.status === 'active' ||\n data.type === 'paid' ||\n data.membershipType === 'paid',\n );\n}\n\nasync function checkRemoteMembership(token) {\n if (!token || !fabushiApiBaseUrl) return false;\n const endpoints = [\n '/api/stripe/membership-status',\n '/api/alipay/check-membership',\n '/api/auth/user-info',\n ];\n\n for (const endpoint of endpoints) {\n try {\n const response = await fetch(`${fabushiApiBaseUrl}${endpoint}`, {\n headers: {\n Accept: 'application/json',\n Authorization: `Bearer ${token}`,\n },\n signal: AbortSignal.timeout(8_000),\n });\n if (!response.ok) continue;\n const payload = await response.json();\n if (normalizeMembershipPayload(payload)) return true;\n } catch (error) {\n logger.debug({ endpoint, error: String(error) }, 'membership lookup skipped');\n }\n }\n\n return false;\n}\n\nasync function resolveUser(req, body = {}) {\n const token = bearerToken(req);\n const username = safeUserText(body.username || req.query.username);\n const tokenHash = token ? sha256(token).slice(0, 24) : '';\n const anonymousHash = sha256(`${req.ip}|${req.get('User-Agent') || ''}`).slice(0, 24);\n const userId = username\n ? `user:${username}`\n : tokenHash\n ? `token:${tokenHash}`\n : `anon:${anonymousHash}`;\n const remoteMember = await checkRemoteMembership(token);\n const memberHint = body.clientMembershipHint === true;\n return {\n userId,\n username,\n tokenHash,\n isMember: remoteMember || memberHint,\n };\n}\n\nfunction usageFor(userId) {\n const month = monthKey();\n const row = statements.getUsage.get(userId, month);\n return {\n month,\n used: Number(row?.totalTokens || 0),\n };\n}\n\nfunction enforceTokenBudget(user, estimatedTokensForRequest) {\n const limit = user.isMember ? memberMonthlyLimit : freeMonthlyLimit;\n const usage = usageFor(user.userId);\n if (usage.used + estimatedTokensForRequest > limit) {\n const error = new Error('本月 AI token 额度已不足');\n error.statusCode = 429;\n error.details = {\n monthlyLimit: limit,\n usedTokens: usage.used,\n remainingTokens: Math.max(0, limit - usage.used),\n };\n throw error;\n }\n return { limit, usage };\n}\n\nfunction recordUsage(userId, totalTokens) {\n if (!totalTokens || totalTokens < 1) return;\n statements.upsertUsage.run({\n userId,\n month: monthKey(),\n totalTokens,\n updatedAt: nowIso(),\n });\n}\n\nfunction titleFromMessage(message) {\n const compact = message.replace(/\\s+/g, ' ').trim();\n if (!compact) return '新对话';\n return compact.length > 22 ? `${compact.slice(0, 22)}...` : compact;\n}\n\nfunction normalizeMessages(rows) {\n return rows.map((row) => ({\n role: row.role,\n content: row.content,\n }));\n}\n\nasync function callDeepSeek(messages) {\n if (!deepseekApiKey) {\n const error = new Error('DeepSeek API key is not configured');\n error.statusCode = 503;\n throw error;\n }\n\n const response = await fetch(`${deepseekBaseUrl}/chat/completions`, {\n method: 'POST',\n headers: {\n Accept: 'application/json',\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${deepseekApiKey}`,\n },\n body: JSON.stringify({\n model: deepseekModel,\n messages,\n temperature: 0.4,\n max_tokens: 1400,\n stream: false,\n }),\n signal: AbortSignal.timeout(60_000),\n });\n\n const bodyText = await response.text();\n let payload;\n try {\n payload = bodyText ? JSON.parse(bodyText) : {};\n } catch {\n payload = { error: bodyText };\n }\n\n if (!response.ok) {\n const upstreamMessage = payload?.error?.message || payload?.message || '';\n const friendlyMessage =\n response.status === 402 || /insufficient\\s+balance/i.test(upstreamMessage)\n ? '大乘 AI 服务额度暂不可用,请联系管理员充值 DeepSeek 账户后重试。'\n : upstreamMessage || `DeepSeek request failed: ${response.status}`;\n const error = new Error(friendlyMessage);\n error.statusCode = response.status;\n throw error;\n }\n\n const message = payload?.choices?.[0]?.message?.content?.trim();\n if (!message) {\n const error = new Error('DeepSeek returned an empty response');\n error.statusCode = 502;\n throw error;\n }\n\n return {\n message,\n usage: {\n promptTokens: Number(payload?.usage?.prompt_tokens || 0),\n completionTokens: Number(payload?.usage?.completion_tokens || 0),\n totalTokens: Number(payload?.usage?.total_tokens || 0),\n },\n };\n}\n\nasync function callDeepSeekStream(messages, callbacks = {}) {\n if (!deepseekApiKey) {\n const error = new Error('DeepSeek API key is not configured');\n error.statusCode = 503;\n throw error;\n }\n\n const response = await fetch(`${deepseekBaseUrl}/chat/completions`, {\n method: 'POST',\n headers: {\n Accept: 'text/event-stream',\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${deepseekApiKey}`,\n },\n body: JSON.stringify({\n model: deepseekModel,\n messages,\n temperature: 0.4,\n max_tokens: 1400,\n stream: true,\n stream_options: { include_usage: true },\n }),\n signal: callbacks.signal || AbortSignal.timeout(90_000),\n });\n\n if (!response.ok) {\n const bodyText = await response.text();\n let payload;\n try {\n payload = bodyText ? JSON.parse(bodyText) : {};\n } catch {\n payload = { error: bodyText };\n }\n const upstreamMessage = payload?.error?.message || payload?.message || '';\n const friendlyMessage =\n response.status === 402 || /insufficient\\s+balance/i.test(upstreamMessage)\n ? '大乘 AI 服务额度暂不可用,请联系管理员充值 DeepSeek 账户后重试。'\n : upstreamMessage || `DeepSeek request failed: ${response.status}`;\n const error = new Error(friendlyMessage);\n error.statusCode = response.status;\n throw error;\n }\n\n let buffer = '';\n let message = '';\n let usage = {\n promptTokens: 0,\n completionTokens: 0,\n totalTokens: 0,\n };\n const decoder = new TextDecoder();\n\n for await (const chunk of response.body) {\n buffer += decoder.decode(chunk, { stream: true });\n const lines = buffer.split(/\\r?\\n/);\n buffer = lines.pop() || '';\n\n for (const rawLine of lines) {\n const line = rawLine.trim();\n if (!line.startsWith('data:')) continue;\n const data = line.slice('data:'.length).trim();\n if (!data || data === '[DONE]') continue;\n\n let payload;\n try {\n payload = JSON.parse(data);\n } catch {\n continue;\n }\n\n const delta = payload?.choices?.[0]?.delta?.content || '';\n if (delta) {\n message += delta;\n callbacks.onToken?.(delta);\n }\n\n if (payload?.usage) {\n usage = {\n promptTokens: Number(payload.usage.prompt_tokens || usage.promptTokens || 0),\n completionTokens: Number(payload.usage.completion_tokens || usage.completionTokens || 0),\n totalTokens: Number(payload.usage.total_tokens || usage.totalTokens || 0),\n };\n }\n }\n }\n\n message = message.trim();\n if (!message) {\n const error = new Error('DeepSeek returned an empty response');\n error.statusCode = 502;\n throw error;\n }\n\n return { message, usage };\n}\n\nfunction createCodexDeepSeekRuntime() {\n if (!enableCodexSdkChat || !deepseekApiKey) {\n return {\n enabled: false,\n provider: 'deepseek-direct',\n reason: enableCodexSdkChat ? 'DeepSeek API key is not configured' : 'ENABLE_CODEX_SDK_CHAT is not true',\n };\n }\n\n return {\n enabled: true,\n provider: 'codex-sdk-deepseek',\n options: {\n apiKey: deepseekApiKey,\n baseUrl: deepseekBaseUrl,\n env: {\n ...process.env,\n DEEPSEEK_API_KEY: deepseekApiKey,\n HOME: codexHomeDir,\n CODEX_HOME: codexHomeDir,\n TMPDIR: codexTempDir,\n XDG_RUNTIME_DIR: codexRuntimeDir,\n XDG_CACHE_HOME: path.join(codexHomeDir, '.cache'),\n XDG_CONFIG_HOME: path.join(codexHomeDir, '.config'),\n XDG_DATA_HOME: path.join(codexHomeDir, '.local', 'share'),\n PATH: process.env.PATH || '/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin',\n },\n config: {\n model_provider: codexDeepSeekProviderId,\n model_providers: {\n [codexDeepSeekProviderId]: {\n name: 'DeepSeek Chat Completions',\n base_url: codexResponsesBaseUrl,\n env_key: 'DEEPSEEK_API_KEY',\n wire_api: 'responses',\n query_params: {},\n },\n },\n },\n },\n threadOptions: {\n model: deepseekModel,\n approvalPolicy: 'never',\n sandboxMode: 'read-only',\n skipGitRepoCheck: true,\n networkAccessEnabled: true,\n },\n };\n}\n\nfunction codexPromptFromMessages(messages) {\n return messages\n .map((message) => {\n const role = String(message.role || 'user').toUpperCase();\n return `${role}:\\n${message.content}`;\n })\n .join('\\n\\n');\n}\n\nfunction responseContentText(content) {\n if (typeof content === 'string') return content;\n if (!Array.isArray(content)) return '';\n return content\n .map((part) => {\n if (typeof part === 'string') return part;\n if (!part || typeof part !== 'object') return '';\n return String(part.text || part.input_text || part.output_text || '');\n })\n .filter(Boolean)\n .join('\\n');\n}\n\nfunction codexResponsesPrompt(body) {\n const input = Array.isArray(body?.input) ? body.input : [];\n const userItems = input.filter((item) => item?.role === 'user');\n const source = userItems[userItems.length - 1] || input[input.length - 1];\n const prompt = responseContentText(source?.content).trim();\n if (prompt) return prompt;\n if (typeof body?.input === 'string') return body.input.trim();\n return '';\n}\n\nfunction writeResponsesEvent(res, event, data) {\n res.write(`event: ${event}\\n`);\n res.write(`data: ${JSON.stringify(data)}\\n\\n`);\n}\n\nfunction responseUsage(usage = {}) {\n return {\n input_tokens: Number(usage.promptTokens || usage.input_tokens || 0),\n cached_input_tokens: 0,\n output_tokens: Number(usage.completionTokens || usage.output_tokens || 0),\n reasoning_output_tokens: 0,\n total_tokens: Number(usage.totalTokens || usage.total_tokens || 0),\n };\n}\n\nfunction createLibreChatAgentRuntime() {\n if (!enableLibreChatAgentChat || !libreChatAgentApiKey || !libreChatAgentId) {\n return {\n enabled: false,\n provider: 'librechat-agent',\n reason: !enableLibreChatAgentChat\n ? 'ENABLE_LIBRECHAT_AGENT_CHAT is not true'\n : !libreChatAgentApiKey\n ? 'LIBRECHAT_AGENT_API_KEY is not configured'\n : 'LIBRECHAT_AGENT_ID is not configured',\n };\n }\n\n return {\n enabled: true,\n provider: 'librechat-agent',\n model: libreChatAgentId,\n chatCompletionsUrl: `${libreChatAgentsBaseUrl}/chat/completions`,\n };\n}\n\nfunction libreChatUsage(usage = {}) {\n return {\n promptTokens: Number(usage.prompt_tokens || usage.input_tokens || usage.promptTokens || 0),\n completionTokens: Number(usage.completion_tokens || usage.output_tokens || usage.completionTokens || 0),\n totalTokens: Number(usage.total_tokens || usage.totalTokens || 0),\n };\n}\n\nasync function callLibreChatAgent(messages) {\n const runtime = createLibreChatAgentRuntime();\n if (!runtime.enabled) {\n const error = new Error(runtime.reason || 'LibreChat Agent chat is not enabled');\n error.statusCode = 503;\n throw error;\n }\n\n const response = await fetch(runtime.chatCompletionsUrl, {\n method: 'POST',\n headers: {\n Accept: 'application/json',\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${libreChatAgentApiKey}`,\n },\n body: JSON.stringify({\n model: runtime.model,\n messages,\n temperature: 0.4,\n stream: false,\n }),\n signal: AbortSignal.timeout(120_000),\n });\n\n const bodyText = await response.text();\n let payload;\n try {\n payload = bodyText ? JSON.parse(bodyText) : {};\n } catch {\n payload = { error: bodyText };\n }\n\n if (!response.ok) {\n const upstreamMessage = payload?.error?.message || payload?.message || bodyText || '';\n const error = new Error(upstreamMessage || `LibreChat Agent request failed: ${response.status}`);\n error.statusCode = response.status;\n throw error;\n }\n\n const message = payload?.choices?.[0]?.message?.content?.trim();\n if (!message) {\n const error = new Error('LibreChat Agent returned an empty response');\n error.statusCode = 502;\n throw error;\n }\n\n return {\n message,\n usage: libreChatUsage(payload?.usage),\n };\n}\n\nasync function callLibreChatAgentStream(messages, callbacks = {}) {\n const runtime = createLibreChatAgentRuntime();\n if (!runtime.enabled) {\n const error = new Error(runtime.reason || 'LibreChat Agent chat is not enabled');\n error.statusCode = 503;\n throw error;\n }\n\n const response = await fetch(runtime.chatCompletionsUrl, {\n method: 'POST',\n headers: {\n Accept: 'text/event-stream',\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${libreChatAgentApiKey}`,\n },\n body: JSON.stringify({\n model: runtime.model,\n messages,\n temperature: 0.4,\n stream: true,\n }),\n signal: callbacks.signal || AbortSignal.timeout(180_000),\n });\n\n if (!response.ok) {\n const bodyText = await response.text();\n let payload;\n try {\n payload = bodyText ? JSON.parse(bodyText) : {};\n } catch {\n payload = { error: bodyText };\n }\n const upstreamMessage = payload?.error?.message || payload?.message || bodyText || '';\n const error = new Error(upstreamMessage || `LibreChat Agent request failed: ${response.status}`);\n error.statusCode = response.status;\n throw error;\n }\n\n let buffer = '';\n let message = '';\n let usage = { promptTokens: 0, completionTokens: 0, totalTokens: 0 };\n const decoder = new TextDecoder();\n\n for await (const chunk of response.body) {\n buffer += decoder.decode(chunk, { stream: true });\n const lines = buffer.split(/\\r?\\n/);\n buffer = lines.pop() || '';\n\n for (const rawLine of lines) {\n const line = rawLine.trim();\n if (!line.startsWith('data:')) continue;\n const data = line.slice('data:'.length).trim();\n if (!data || data === '[DONE]') continue;\n\n let payload;\n try {\n payload = JSON.parse(data);\n } catch {\n continue;\n }\n\n const delta = payload?.choices?.[0]?.delta?.content || '';\n if (delta) {\n message += delta;\n callbacks.onToken?.(delta);\n }\n\n if (payload?.usage) {\n usage = libreChatUsage(payload.usage);\n }\n }\n }\n\n message = message.trim();\n if (!message) {\n const error = new Error('LibreChat Agent returned an empty response');\n error.statusCode = 502;\n throw error;\n }\n\n return { message, usage };\n}\n\nfunction isDeepSeekQuotaError(error) {\n const message = String(error?.message || error || '');\n return /额度|insufficient\\s+balance|balance/i.test(message);\n}\n\nfunction publicAiErrorMessage(error) {\n if (isDeepSeekQuotaError(error)) {\n return '大乘 AI 服务额度暂不可用,请联系管理员充值 DeepSeek 账户后重试。';\n }\n return error?.message || 'Internal Server Error';\n}\n\nfunction responsesPayload({\n responseId,\n itemId,\n status,\n model,\n text = '',\n usage = null,\n error = null,\n}) {\n return {\n id: responseId,\n object: 'response',\n status,\n model,\n output:\n status === 'completed'\n ? [\n {\n id: itemId,\n type: 'message',\n role: 'assistant',\n status: 'completed',\n content: [{ type: 'output_text', text }],\n },\n ]\n : [],\n ...(usage ? { usage: responseUsage(usage) } : {}),\n ...(error\n ? {\n error: {\n message: error.message || String(error),\n type: 'server_error',\n },\n }\n : {}),\n };\n}\n\nasync function callCodexSdkDeepSeek(messages, callbacks = {}) {\n const codexRuntime = createCodexDeepSeekRuntime();\n if (!codexRuntime.enabled) {\n const error = new Error(codexRuntime.reason || 'Codex SDK chat is not enabled');\n error.statusCode = 503;\n throw error;\n }\n\n const { Codex } = await import('@openai/codex-sdk');\n const codex = new Codex(codexRuntime.options);\n const thread = codex.startThread(codexRuntime.threadOptions);\n const prompt = codexPromptFromMessages(messages);\n\n if (callbacks.onToken || callbacks.onStep) {\n const { events } = await thread.runStreamed(prompt, { signal: callbacks.signal });\n let finalResponse = '';\n let usage = null;\n\n for await (const event of events) {\n if (event.type === 'item.completed') {\n const item = event.item;\n if (item.type === 'agent_message') {\n finalResponse = item.text || finalResponse;\n callbacks.onToken?.(item.text || '');\n } else if (item.type === 'todo_list') {\n callbacks.onStep?.({\n title: 'Codex SDK 执行计划',\n message: item.items.map((todo) => `${todo.completed ? '✓' : '•'} ${todo.text}`).join('\\n'),\n });\n } else if (item.type === 'command_execution') {\n callbacks.onStep?.({\n title: 'Codex SDK 执行命令',\n message: item.command,\n });\n } else if (item.type === 'mcp_tool_call') {\n callbacks.onStep?.({\n title: 'Codex SDK 调用工具',\n message: `${item.server}.${item.tool}`,\n });\n }\n } else if (event.type === 'turn.completed') {\n usage = event.usage;\n } else if (event.type === 'turn.failed') {\n throw new Error(event.error?.message || 'Codex SDK turn failed');\n } else if (event.type === 'error') {\n throw new Error(event.message || 'Codex SDK stream failed');\n }\n }\n\n if (!finalResponse.trim()) {\n const error = new Error('Codex SDK returned an empty response');\n error.statusCode = 502;\n throw error;\n }\n\n return {\n message: finalResponse.trim(),\n usage: {\n promptTokens: Number(usage?.input_tokens || 0),\n completionTokens: Number(usage?.output_tokens || 0),\n totalTokens: Number((usage?.input_tokens || 0) + (usage?.output_tokens || 0)),\n },\n };\n }\n\n const turn = await thread.run(prompt);\n if (!turn.finalResponse?.trim()) {\n const error = new Error('Codex SDK returned an empty response');\n error.statusCode = 502;\n throw error;\n }\n\n return {\n message: turn.finalResponse.trim(),\n usage: {\n promptTokens: Number(turn.usage?.input_tokens || 0),\n completionTokens: Number(turn.usage?.output_tokens || 0),\n totalTokens: Number((turn.usage?.input_tokens || 0) + (turn.usage?.output_tokens || 0)),\n },\n };\n}\n\nconst activeAgentRuns = new Map();\n\nfunction normalizeAgentMode(value) {\n const mode = safeUserText(value || 'dharma_guide');\n return /^[a-z][a-z0-9_:-]{0,48}$/i.test(mode) ? mode : 'dharma_guide';\n}\n\nfunction openClawSessionKey(user, conversationId) {\n const env = process.env.NODE_ENV === 'production' ? 'prod' : 'dev';\n return `fabushi:env:${env}:user:${user.userId}:conversation:${conversationId}`;\n}\n\nfunction createOpenClawRuntime() {\n if (!enableOpenClawAgentChat || !openClawGatewayToken || !openClawGatewayUrl) {\n return {\n enabled: false,\n provider: 'openclaw-gateway',\n reason: !enableOpenClawAgentChat\n ? 'ENABLE_OPENCLAW_AGENT_CHAT is not true'\n : !openClawGatewayToken\n ? 'OPENCLAW_GATEWAY_TOKEN is not configured'\n : 'OPENCLAW_GATEWAY_URL is not configured',\n };\n }\n\n return {\n enabled: true,\n provider: 'openclaw-gateway',\n model: openClawAgentId,\n runsUrl: openClawRunsEndpoint || `${openClawGatewayUrl}/v1/agents/${encodeURIComponent(openClawAgentId)}/runs`,\n };\n}\n\nfunction extractOpenClawText(payload) {\n if (!payload || typeof payload !== 'object') return '';\n const candidates = [\n payload.message,\n payload.content,\n payload.output_text,\n payload.finalResponse,\n payload.final_response,\n payload.result?.message,\n payload.result?.content,\n payload.output?.message,\n payload.output?.content,\n payload.choices?.[0]?.message?.content,\n ];\n for (const candidate of candidates) {\n if (typeof candidate === 'string' && candidate.trim()) return candidate.trim();\n }\n return '';\n}\n\nfunction extractOpenClawUsage(payload) {\n const usage = payload?.usage || payload?.result?.usage || {};\n return {\n promptTokens: Number(usage.input_tokens || usage.prompt_tokens || usage.promptTokens || 0),\n completionTokens: Number(usage.output_tokens || usage.completion_tokens || usage.completionTokens || 0),\n totalTokens: Number(usage.total_tokens || usage.totalTokens || 0),\n };\n}\n\nasync function callOpenClawAgent({ messages, user, conversationId, mode, signal }) {\n const runtime = createOpenClawRuntime();\n if (!runtime.enabled) {\n const error = new Error(runtime.reason || 'OpenClaw Agent chat is not enabled');\n error.statusCode = 503;\n throw error;\n }\n\n const sessionKey = openClawSessionKey(user, conversationId);\n const response = await fetch(runtime.runsUrl, {\n method: 'POST',\n headers: {\n Accept: 'application/json',\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${openClawGatewayToken}`,\n },\n body: JSON.stringify({\n agentId: openClawAgentId,\n sessionKey,\n mode,\n stream: false,\n input: { messages },\n messages,\n metadata: {\n app: 'fabushi',\n userId: user.userId,\n conversationId,\n toolPolicy: 'public-user-read-mostly',\n },\n }),\n signal: signal || AbortSignal.timeout(180_000),\n });\n\n const bodyText = await response.text();\n let payload;\n try {\n payload = bodyText ? JSON.parse(bodyText) : {};\n } catch {\n payload = { message: bodyText };\n }\n\n if (!response.ok) {\n const error = new Error(payload?.error?.message || payload?.message || `OpenClaw request failed: ${response.status}`);\n error.statusCode = response.status;\n throw error;\n }\n\n const message = extractOpenClawText(payload);\n if (!message) {\n const error = new Error('OpenClaw returned an empty response');\n error.statusCode = 502;\n throw error;\n }\n\n return {\n message,\n runId: payload.runId || payload.id || payload.result?.runId || null,\n provider: runtime.provider,\n model: runtime.model,\n usage: extractOpenClawUsage(payload),\n };\n}\n\nasync function runAgentModel({ messages, user, conversationId, mode, callbacks = {}, signal }) {\n const openClawRuntime = createOpenClawRuntime();\n if (openClawRuntime.enabled && !callbacks.onToken) {\n return await callOpenClawAgent({ messages, user, conversationId, mode, signal });\n }\n\n const codexRuntime = createCodexDeepSeekRuntime();\n const libreChatRuntime = createLibreChatAgentRuntime();\n let provider = libreChatRuntime.enabled\n ? libreChatRuntime.provider\n : codexRuntime.enabled\n ? codexRuntime.provider\n : 'deepseek';\n let model = libreChatRuntime.enabled ? libreChatRuntime.model : deepseekModel;\n\n try {\n if (libreChatRuntime.enabled) {\n const result = callbacks.onToken\n ? await callLibreChatAgentStream(messages, { ...callbacks, signal })\n : await callLibreChatAgent(messages);\n return { ...result, provider, model };\n }\n if (codexRuntime.enabled) {\n const result = await callCodexSdkDeepSeek(messages, { ...callbacks, signal });\n return { ...result, provider, model };\n }\n const result = callbacks.onToken\n ? await callDeepSeekStream(messages, { ...callbacks, signal })\n : await callDeepSeek(messages);\n return { ...result, provider, model };\n } catch (error) {\n if (!codexRuntime.enabled || isDeepSeekQuotaError(error)) throw error;\n logger.warn({ error: String(error) }, 'Agent model failed, falling back to DeepSeek direct');\n const result = callbacks.onToken\n ? await callDeepSeekStream(messages, { ...callbacks, signal })\n : await callDeepSeek(messages);\n return { ...result, provider: 'deepseek', model: deepseekModel };\n }\n}\n\nfunction updateAgentRunStatus(run, status, fields = {}) {\n statements.updateAgentRunStatus.run({\n id: run.id,\n userId: run.user_id || run.userId,\n status,\n openClawRunId: fields.openClawRunId ?? null,\n inputTokens: fields.inputTokens ?? null,\n outputTokens: fields.outputTokens ?? null,\n toolCallCount: fields.toolCallCount ?? null,\n completedAt: status === 'completed' || status === 'cancelled' ? nowIso() : null,\n failedAt: status === 'failed' ? nowIso() : null,\n errorCode: fields.errorCode ?? null,\n errorMessage: fields.errorMessage ?? null,\n });\n}\n\nfunction buildAgentMessages(conversationId) {\n const historyRows = statements.listMessages.all(conversationId).slice(-14);\n return [{ role: 'system', content: systemPrompt }, ...normalizeMessages(historyRows)];\n}\n\nfunction agentUsagePayload(usage, user, budget) {\n const promptTokens = usage.promptTokens || usage.inputTokens || 0;\n const completionTokens = usage.completionTokens || usage.outputTokens || 0;\n const totalTokens = usage.totalTokens || promptTokens + completionTokens;\n const latestUsage = usageFor(user.userId);\n return {\n inputTokens: promptTokens,\n outputTokens: completionTokens,\n totalTokens,\n monthlyLimit: budget.limit,\n remainingTokens: Math.max(0, budget.limit - latestUsage.used),\n };\n}\n\napp.post(\n '/codex-deepseek/v1/responses',\n asyncHandler(async (req, res) => {\n const bearer = bearerToken(req);\n if (!deepseekApiKey || bearer !== deepseekApiKey) {\n res.status(401).json({\n error: { message: 'Unauthorized Codex DeepSeek adapter request' },\n });\n return;\n }\n\n const prompt = codexResponsesPrompt(req.body);\n if (!prompt) {\n res.status(400).json({ error: { message: 'Responses input is required' } });\n return;\n }\n\n const model = String(req.body?.model || deepseekModel);\n const responseId = `resp_${crypto.randomUUID().replaceAll('-', '')}`;\n const itemId = `msg_${crypto.randomUUID().replaceAll('-', '')}`;\n const wantsStream =\n req.body?.stream !== false ||\n String(req.get('accept') || '').includes('text/event-stream');\n const messages = [{ role: 'user', content: prompt }];\n\n if (!wantsStream) {\n const result = await callDeepSeek(messages);\n res.json(\n responsesPayload({\n responseId,\n itemId,\n status: 'completed',\n model,\n text: result.message,\n usage: result.usage,\n }),\n );\n return;\n }\n\n res.status(200);\n res.set({\n 'Content-Type': 'text/event-stream; charset=utf-8',\n 'Cache-Control': 'no-cache, no-transform',\n Connection: 'keep-alive',\n 'X-Accel-Buffering': 'no',\n });\n res.flushHeaders?.();\n\n writeResponsesEvent(res, 'response.created', {\n type: 'response.created',\n response: responsesPayload({\n responseId,\n itemId,\n status: 'in_progress',\n model,\n }),\n });\n writeResponsesEvent(res, 'response.output_item.added', {\n type: 'response.output_item.added',\n output_index: 0,\n item: {\n id: itemId,\n type: 'message',\n role: 'assistant',\n status: 'in_progress',\n content: [],\n },\n });\n writeResponsesEvent(res, 'response.content_part.added', {\n type: 'response.content_part.added',\n item_id: itemId,\n output_index: 0,\n content_index: 0,\n part: { type: 'output_text', text: '' },\n });\n\n let text = '';\n try {\n const result = await callDeepSeekStream(messages, {\n onToken: (delta) => {\n text += delta;\n writeResponsesEvent(res, 'response.output_text.delta', {\n type: 'response.output_text.delta',\n item_id: itemId,\n output_index: 0,\n content_index: 0,\n delta,\n });\n },\n });\n text = result.message || text;\n writeResponsesEvent(res, 'response.output_text.done', {\n type: 'response.output_text.done',\n item_id: itemId,\n output_index: 0,\n content_index: 0,\n text,\n });\n writeResponsesEvent(res, 'response.content_part.done', {\n type: 'response.content_part.done',\n item_id: itemId,\n output_index: 0,\n content_index: 0,\n part: { type: 'output_text', text },\n });\n writeResponsesEvent(res, 'response.output_item.done', {\n type: 'response.output_item.done',\n output_index: 0,\n item: {\n id: itemId,\n type: 'message',\n role: 'assistant',\n status: 'completed',\n content: [{ type: 'output_text', text }],\n },\n });\n writeResponsesEvent(res, 'response.completed', {\n type: 'response.completed',\n response: responsesPayload({\n responseId,\n itemId,\n status: 'completed',\n model,\n text,\n usage: result.usage,\n }),\n });\n res.end();\n } catch (error) {\n writeResponsesEvent(res, 'response.failed', {\n type: 'response.failed',\n response: responsesPayload({\n responseId,\n itemId,\n status: 'failed',\n model,\n text,\n error,\n }),\n });\n res.end();\n }\n }),\n);\n\napp.get('/health', (_req, res) => {\n const codexRuntime = createCodexDeepSeekRuntime();\n const libreChatRuntime = createLibreChatAgentRuntime();\n const openClawRuntime = createOpenClawRuntime();\n jsonResponse(res, 200, {\n status: 'ok',\n service: 'dacheng-ai-backend',\n provider: libreChatRuntime.enabled ? libreChatRuntime.provider : 'deepseek',\n model: libreChatRuntime.enabled ? libreChatRuntime.model : deepseekModel,\n libreChatAgentChatEnabled: libreChatRuntime.enabled,\n libreChatAgentProvider: libreChatRuntime.provider,\n libreChatAgentReason: libreChatRuntime.enabled ? undefined : libreChatRuntime.reason,\n codexSdkAvailable: true,\n codexSdkChatEnabled: codexRuntime.enabled,\n codexSdkProvider: codexRuntime.provider,\n openClawAgentChatEnabled: openClawRuntime.enabled,\n openClawAgentProvider: openClawRuntime.provider,\n openClawAgentReason: openClawRuntime.enabled ? undefined : openClawRuntime.reason,\n cbetaApiRoot,\n timestamp: nowIso(),\n });\n});\n\napp.post(\n '/api/agent/chat',\n asyncHandler(async (req, res) => {\n const message = safeUserText(req.body?.message);\n if (!message) {\n return jsonResponse(res, 400, { success: false, message: 'message is required' });\n }\n\n const user = await resolveUser(req, req.body || {});\n const estimated = estimateTokens(message) + 600;\n const budget = enforceTokenBudget(user, estimated);\n const createdAt = nowIso();\n const conversationId = safeUserText(req.body?.conversationId) || crypto.randomUUID();\n const mode = normalizeAgentMode(req.body?.mode);\n const stream = req.body?.stream === true;\n let conversation = statements.getConversation.get(conversationId, user.userId);\n const isNewConversation = !conversation;\n\n if (isNewConversation) {\n conversation = { id: conversationId, user_id: user.userId, title: titleFromMessage(message) };\n statements.insertConversation.run({\n id: conversationId,\n userId: user.userId,\n title: conversation.title,\n provider: 'agent',\n model: openClawAgentId,\n createdAt,\n updatedAt: createdAt,\n });\n }\n\n const messageId = safeUserText(req.body?.messageId) || crypto.randomUUID();\n statements.insertMessage.run({\n id: messageId,\n conversationId,\n role: 'user',\n content: message,\n promptTokens: 0,\n completionTokens: 0,\n totalTokens: 0,\n createdAt,\n });\n\n const runId = `run_${crypto.randomUUID().replaceAll('-', '')}`;\n const sessionKey = openClawSessionKey(user, conversationId);\n const openClawRuntime = createOpenClawRuntime();\n statements.insertAgentRun.run({\n id: runId,\n userId: user.userId,\n conversationId,\n messageId,\n openClawRunId: null,\n openClawSessionKey: sessionKey,\n status: stream ? 'queued' : 'running',\n mode,\n provider: openClawRuntime.enabled ? openClawRuntime.provider : 'dacheng-ai-fallback',\n model: openClawRuntime.enabled ? openClawAgentId : deepseekModel,\n startedAt: createdAt,\n });\n\n if (stream) {\n return jsonResponse(res, 200, {\n success: true,\n runId,\n conversationId,\n streamUrl: `/api/agent/runs/${runId}/events`,\n });\n }\n\n const controller = new AbortController();\n activeAgentRuns.set(runId, controller);\n try {\n const modelMessages = buildAgentMessages(conversationId);\n const aiResult = await runAgentModel({\n messages: modelMessages,\n user,\n conversationId,\n mode,\n signal: controller.signal,\n });\n const usage = {\n promptTokens: aiResult.usage?.promptTokens || estimateTokens(JSON.stringify(modelMessages)),\n completionTokens: aiResult.usage?.completionTokens || estimateTokens(aiResult.message),\n totalTokens:\n aiResult.usage?.totalTokens ||\n estimateTokens(JSON.stringify(modelMessages)) + estimateTokens(aiResult.message),\n };\n recordUsage(user.userId, usage.totalTokens);\n const answeredAt = nowIso();\n const assistantMessageId = crypto.randomUUID();\n statements.insertMessage.run({\n id: assistantMessageId,\n conversationId,\n role: 'assistant',\n content: aiResult.message,\n promptTokens: usage.promptTokens,\n completionTokens: usage.completionTokens,\n totalTokens: usage.totalTokens,\n createdAt: answeredAt,\n });\n statements.updateConversation.run({\n id: conversationId,\n userId: user.userId,\n title: isNewConversation ? titleFromMessage(message) : conversation.title,\n updatedAt: answeredAt,\n });\n updateAgentRunStatus({ id: runId, user_id: user.userId }, 'completed', {\n openClawRunId: aiResult.runId,\n inputTokens: usage.promptTokens,\n outputTokens: usage.completionTokens,\n });\n return jsonResponse(res, 200, {\n success: true,\n runId,\n conversationId,\n messageId: assistantMessageId,\n provider: aiResult.provider,\n model: aiResult.model,\n message: aiResult.message,\n usage: agentUsagePayload(usage, user, budget),\n });\n } catch (error) {\n const status = error.name === 'AbortError' ? 'cancelled' : 'failed';\n updateAgentRunStatus({ id: runId, user_id: user.userId }, status, {\n errorCode: error.name || 'AGENT_RUN_FAILED',\n errorMessage: publicAiErrorMessage(error),\n });\n if (status === 'cancelled') {\n return jsonResponse(res, 499, { success: false, runId, status: 'cancelled' });\n }\n throw error;\n } finally {\n activeAgentRuns.delete(runId);\n }\n }),\n);\n\napp.get(\n '/api/agent/runs/:runId/events',\n asyncHandler(async (req, res) => {\n const user = await resolveUser(req, {});\n const run = statements.getAgentRun.get(req.params.runId, user.userId);\n if (!run) {\n return jsonResponse(res, 404, { success: false, message: 'run not found' });\n }\n if (run.status === 'completed' || run.status === 'failed' || run.status === 'cancelled') {\n return jsonResponse(res, 409, { success: false, message: `run is already ${run.status}` });\n }\n\n sseHeaders(res);\n const controller = new AbortController();\n activeAgentRuns.set(run.id, controller);\n updateAgentRunStatus(run, 'running');\n writeSse(res, 'run.started', { runId: run.id, conversationId: run.conversation_id });\n\n try {\n const estimated = 600;\n const budget = enforceTokenBudget(user, estimated);\n let streamedText = '';\n const modelMessages = buildAgentMessages(run.conversation_id);\n const aiResult = await runAgentModel({\n messages: modelMessages,\n user,\n conversationId: run.conversation_id,\n mode: run.mode,\n signal: controller.signal,\n callbacks: {\n onStep: (step) => writeSse(res, 'tool.call.completed', {\n tool: 'agent.step',\n displayName: step.title || 'Agent step',\n summary: step.message || '',\n }),\n onToken: (delta) => {\n streamedText += delta;\n writeSse(res, 'assistant.delta', { text: delta });\n },\n },\n });\n const message = aiResult.message || streamedText.trim();\n const usage = {\n promptTokens: aiResult.usage?.promptTokens || estimateTokens(JSON.stringify(modelMessages)),\n completionTokens: aiResult.usage?.completionTokens || estimateTokens(message),\n totalTokens:\n aiResult.usage?.totalTokens || estimateTokens(JSON.stringify(modelMessages)) + estimateTokens(message),\n };\n recordUsage(user.userId, usage.totalTokens);\n const assistantMessageId = crypto.randomUUID();\n const answeredAt = nowIso();\n statements.insertMessage.run({\n id: assistantMessageId,\n conversationId: run.conversation_id,\n role: 'assistant',\n content: message,\n promptTokens: usage.promptTokens,\n completionTokens: usage.completionTokens,\n totalTokens: usage.totalTokens,\n createdAt: answeredAt,\n });\n statements.updateConversation.run({\n id: run.conversation_id,\n userId: user.userId,\n title: titleFromMessage(message),\n updatedAt: answeredAt,\n });\n updateAgentRunStatus(run, 'completed', {\n openClawRunId: aiResult.runId,\n inputTokens: usage.promptTokens,\n outputTokens: usage.completionTokens,\n });\n writeSse(res, 'assistant.message', { messageId: assistantMessageId, content: message });\n writeSse(res, 'run.completed', { usage: agentUsagePayload(usage, user, budget) });\n res.end();\n } catch (error) {\n const status = error.name === 'AbortError' ? 'cancelled' : 'failed';\n updateAgentRunStatus(run, status, {\n errorCode: error.name || 'AGENT_RUN_FAILED',\n errorMessage: publicAiErrorMessage(error),\n });\n writeSse(res, status === 'cancelled' ? 'run.cancelled' : 'run.failed', {\n runId: run.id,\n message: publicAiErrorMessage(error),\n });\n res.end();\n } finally {\n activeAgentRuns.delete(run.id);\n }\n }),\n);\n\napp.post(\n '/api/agent/runs/:runId/cancel',\n asyncHandler(async (req, res) => {\n const user = await resolveUser(req, req.body || {});\n const run = statements.getAgentRun.get(req.params.runId, user.userId);\n if (!run) {\n return jsonResponse(res, 404, { success: false, message: 'run not found' });\n }\n const controller = activeAgentRuns.get(run.id);\n if (controller) controller.abort();\n updateAgentRunStatus(run, 'cancelled');\n return jsonResponse(res, 200, { success: true, runId: run.id, status: 'cancelled' });\n }),\n);\n\napp.post(\n '/api/agent/messages/:messageId/feedback',\n asyncHandler(async (req, res) => {\n const user = await resolveUser(req, req.body || {});\n const rating = safeUserText(req.body?.rating);\n if (!['up', 'down'].includes(rating)) {\n return jsonResponse(res, 400, { success: false, message: 'rating must be up or down' });\n }\n statements.insertAgentFeedback.run({\n id: crypto.randomUUID(),\n messageId: req.params.messageId,\n userId: user.userId,\n rating,\n reason: safeUserText(req.body?.reason),\n comment: safeUserText(req.body?.comment),\n createdAt: nowIso(),\n });\n return jsonResponse(res, 200, { success: true });\n }),\n);\n\napp.post(\n '/api/ai/chat',\n asyncHandler(async (req, res) => {\n const message = safeUserText(req.body?.message);\n if (!message) {\n return jsonResponse(res, 400, { success: false, message: 'message is required' });\n }\n\n const user = await resolveUser(req, req.body || {});\n const estimated = estimateTokens(message) + 600;\n const budget = enforceTokenBudget(user, estimated);\n const createdAt = nowIso();\n const conversationId = safeUserText(req.body?.conversationId) || crypto.randomUUID();\n let conversation = statements.getConversation.get(conversationId, user.userId);\n const isNewConversation = !conversation;\n\n if (isNewConversation) {\n conversation = {\n id: conversationId,\n user_id: user.userId,\n title: titleFromMessage(message),\n };\n statements.insertConversation.run({\n id: conversationId,\n userId: user.userId,\n title: conversation.title,\n provider: 'deepseek',\n model: deepseekModel,\n createdAt,\n updatedAt: createdAt,\n });\n }\n\n statements.insertMessage.run({\n id: crypto.randomUUID(),\n conversationId,\n role: 'user',\n content: message,\n promptTokens: 0,\n completionTokens: 0,\n totalTokens: 0,\n createdAt,\n });\n\n const libreChatRuntime = createLibreChatAgentRuntime();\n const skillResult = await runResourceFinderSkill({ message, user });\n const skillContext = resourceContextMessage(skillResult);\n const historyRows = statements.listMessages.all(conversationId).slice(-14);\n const modelMessages = [\n { role: 'system', content: systemPrompt },\n ...(skillContext ? [{ role: 'system', content: skillContext }] : []),\n ...normalizeMessages(historyRows),\n ];\n\n const openClawRuntime = createOpenClawRuntime();\n const codexRuntime = createCodexDeepSeekRuntime();\n let provider = openClawRuntime.enabled\n ? openClawRuntime.provider\n : libreChatRuntime.enabled\n ? libreChatRuntime.provider\n : 'deepseek';\n let responseModel = openClawRuntime.enabled\n ? openClawRuntime.model\n : libreChatRuntime.enabled\n ? libreChatRuntime.model\n : deepseekModel;\n let aiResult;\n try {\n if (openClawRuntime.enabled) {\n aiResult = await callOpenClawAgent({\n messages: modelMessages,\n user,\n conversationId,\n mode: normalizeAgentMode(req.body?.mode),\n });\n provider = aiResult.provider || provider;\n responseModel = aiResult.model || responseModel;\n } else if (libreChatRuntime.enabled) {\n aiResult = await callLibreChatAgent(modelMessages);\n } else if (codexRuntime.enabled) {\n provider = codexRuntime.provider;\n aiResult = await callCodexSdkDeepSeek(modelMessages);\n } else {\n aiResult = await callDeepSeek(modelMessages);\n }\n } catch (error) {\n if (libreChatRuntime.enabled) {\n logger.warn({ error: String(error) }, 'LibreChat Agent chat failed, falling back to Codex SDK or DeepSeek direct');\n provider = codexRuntime.enabled ? codexRuntime.provider : 'deepseek';\n responseModel = deepseekModel;\n if (codexRuntime.enabled) {\n try {\n aiResult = await callCodexSdkDeepSeek(modelMessages);\n } catch (codexError) {\n if (isDeepSeekQuotaError(codexError)) throw codexError;\n logger.warn({ error: String(codexError) }, 'Codex SDK chat failed, falling back to DeepSeek direct');\n provider = 'deepseek';\n aiResult = await callDeepSeek(modelMessages);\n }\n } else {\n aiResult = await callDeepSeek(modelMessages);\n }\n } else {\n if (!codexRuntime.enabled || isDeepSeekQuotaError(error)) throw error;\n logger.warn({ error: String(error) }, 'Codex SDK chat failed, falling back to DeepSeek direct');\n provider = 'deepseek';\n aiResult = await callDeepSeek(modelMessages);\n }\n }\n const usage = {\n promptTokens: aiResult.usage.promptTokens || estimateTokens(JSON.stringify(modelMessages)),\n completionTokens: aiResult.usage.completionTokens || estimateTokens(aiResult.message),\n totalTokens:\n aiResult.usage.totalTokens ||\n estimateTokens(JSON.stringify(modelMessages)) + estimateTokens(aiResult.message),\n };\n recordUsage(user.userId, usage.totalTokens);\n\n const answeredAt = nowIso();\n statements.insertMessage.run({\n id: crypto.randomUUID(),\n conversationId,\n role: 'assistant',\n content: aiResult.message,\n promptTokens: usage.promptTokens,\n completionTokens: usage.completionTokens,\n totalTokens: usage.totalTokens,\n createdAt: answeredAt,\n });\n statements.updateConversation.run({\n id: conversationId,\n userId: user.userId,\n title: isNewConversation ? titleFromMessage(message) : conversation.title,\n updatedAt: answeredAt,\n });\n\n const latestUsage = usageFor(user.userId);\n const monthlyLimit = budget.limit;\n jsonResponse(res, 200, {\n success: true,\n conversationId,\n provider,\n model: responseModel,\n message: aiResult.message,\n usage: {\n ...usage,\n monthlyLimit,\n remainingTokens: Math.max(0, monthlyLimit - latestUsage.used),\n },\n });\n }),\n);\n\napp.post('/api/ai/chat/stream', async (req, res) => {\n sseHeaders(res);\n\n try {\n const message = safeUserText(req.body?.message);\n if (!message) {\n writeSse(res, 'error', { message: 'message is required' });\n res.end();\n return;\n }\n\n const user = await resolveUser(req, req.body || {});\n const estimated = estimateTokens(message) + 600;\n const budget = enforceTokenBudget(user, estimated);\n const createdAt = nowIso();\n const conversationId = safeUserText(req.body?.conversationId) || crypto.randomUUID();\n let conversation = statements.getConversation.get(conversationId, user.userId);\n const isNewConversation = !conversation;\n\n if (isNewConversation) {\n conversation = {\n id: conversationId,\n user_id: user.userId,\n title: titleFromMessage(message),\n };\n statements.insertConversation.run({\n id: conversationId,\n userId: user.userId,\n title: conversation.title,\n provider: 'deepseek',\n model: deepseekModel,\n createdAt,\n updatedAt: createdAt,\n });\n }\n\n statements.insertMessage.run({\n id: crypto.randomUUID(),\n conversationId,\n role: 'user',\n content: message,\n promptTokens: 0,\n completionTokens: 0,\n totalTokens: 0,\n createdAt,\n });\n\n const codexRuntime = createCodexDeepSeekRuntime();\n const libreChatRuntime = createLibreChatAgentRuntime();\n const initialOpenClawRuntime = createOpenClawRuntime();\n let provider = initialOpenClawRuntime.enabled\n ? initialOpenClawRuntime.provider\n : libreChatRuntime.enabled\n ? libreChatRuntime.provider\n : codexRuntime.enabled\n ? codexRuntime.provider\n : 'deepseek';\n let responseModel = initialOpenClawRuntime.enabled\n ? initialOpenClawRuntime.model\n : libreChatRuntime.enabled\n ? libreChatRuntime.model\n : deepseekModel;\n writeSse(res, 'meta', {\n conversationId,\n provider,\n model: responseModel,\n });\n const skillResult = await runResourceFinderSkill({\n message,\n user,\n onStep: (step) => writeSse(res, 'step', step),\n });\n const skillContext = resourceContextMessage(skillResult);\n const historyRows = statements.listMessages.all(conversationId).slice(-14);\n const modelMessages = [\n { role: 'system', content: systemPrompt },\n ...(skillContext ? [{ role: 'system', content: skillContext }] : []),\n ...normalizeMessages(historyRows),\n ];\n\n let aiResult;\n const openClawRuntime = initialOpenClawRuntime;\n try {\n if (openClawRuntime.enabled) {\n aiResult = await callOpenClawAgent({\n messages: modelMessages,\n user,\n conversationId,\n mode: normalizeAgentMode(req.body?.mode),\n });\n provider = aiResult.provider || provider;\n responseModel = aiResult.model || responseModel;\n writeSse(res, 'delta', { text: aiResult.message });\n } else if (libreChatRuntime.enabled) {\n aiResult = await callLibreChatAgentStream(modelMessages, {\n onToken: (token) => writeSse(res, 'delta', { text: token }),\n });\n } else if (codexRuntime.enabled) {\n aiResult = await callCodexSdkDeepSeek(modelMessages, {\n onToken: (token) => writeSse(res, 'delta', { text: token }),\n onStep: (step) => writeSse(res, 'step', step),\n });\n } else {\n aiResult = await callDeepSeekStream(modelMessages, {\n onToken: (token) => writeSse(res, 'delta', { text: token }),\n });\n }\n } catch (error) {\n if (libreChatRuntime.enabled) {\n logger.warn({ error: String(error) }, 'LibreChat Agent streaming chat failed, falling back to Codex SDK or DeepSeek direct');\n provider = codexRuntime.enabled ? codexRuntime.provider : 'deepseek';\n responseModel = deepseekModel;\n writeSse(res, 'meta', {\n conversationId,\n provider,\n model: responseModel,\n });\n if (codexRuntime.enabled) {\n try {\n aiResult = await callCodexSdkDeepSeek(modelMessages, {\n onToken: (token) => writeSse(res, 'delta', { text: token }),\n onStep: (step) => writeSse(res, 'step', step),\n });\n } catch (codexError) {\n if (isDeepSeekQuotaError(codexError)) throw codexError;\n logger.warn({ error: String(codexError) }, 'Codex SDK streaming chat failed, falling back to DeepSeek direct');\n provider = 'deepseek';\n responseModel = deepseekModel;\n writeSse(res, 'meta', {\n conversationId,\n provider,\n model: responseModel,\n });\n aiResult = await callDeepSeekStream(modelMessages, {\n onToken: (token) => writeSse(res, 'delta', { text: token }),\n });\n }\n } else {\n aiResult = await callDeepSeekStream(modelMessages, {\n onToken: (token) => writeSse(res, 'delta', { text: token }),\n });\n }\n } else {\n if (!codexRuntime.enabled || isDeepSeekQuotaError(error)) throw error;\n logger.warn({ error: String(error) }, 'Codex SDK streaming chat failed, falling back to DeepSeek direct');\n provider = 'deepseek';\n responseModel = deepseekModel;\n writeSse(res, 'meta', {\n conversationId,\n provider,\n model: responseModel,\n });\n aiResult = await callDeepSeekStream(modelMessages, {\n onToken: (token) => writeSse(res, 'delta', { text: token }),\n });\n }\n }\n\n const usage = {\n promptTokens: aiResult.usage.promptTokens || estimateTokens(JSON.stringify(modelMessages)),\n completionTokens: aiResult.usage.completionTokens || estimateTokens(aiResult.message),\n totalTokens:\n aiResult.usage.totalTokens ||\n estimateTokens(JSON.stringify(modelMessages)) + estimateTokens(aiResult.message),\n };\n recordUsage(user.userId, usage.totalTokens);\n\n const answeredAt = nowIso();\n statements.insertMessage.run({\n id: crypto.randomUUID(),\n conversationId,\n role: 'assistant',\n content: aiResult.message,\n promptTokens: usage.promptTokens,\n completionTokens: usage.completionTokens,\n totalTokens: usage.totalTokens,\n createdAt: answeredAt,\n });\n statements.updateConversation.run({\n id: conversationId,\n userId: user.userId,\n title: isNewConversation ? titleFromMessage(message) : conversation.title,\n updatedAt: answeredAt,\n });\n\n const latestUsage = usageFor(user.userId);\n const monthlyLimit = budget.limit;\n writeSse(res, 'done', {\n conversationId,\n provider,\n model: responseModel,\n message: aiResult.message,\n usage: {\n ...usage,\n monthlyLimit,\n remainingTokens: Math.max(0, monthlyLimit - latestUsage.used),\n },\n });\n res.end();\n } catch (error) {\n logger.error({ error: error.stack || String(error) }, 'streaming chat failed');\n writeSse(res, 'error', {\n message: publicAiErrorMessage(error),\n details: error.details,\n });\n res.end();\n }\n});\n\napp.get(\n '/api/ai/conversations',\n asyncHandler(async (req, res) => {\n const user = await resolveUser(req, { username: req.query.username });\n const items = statements.listConversations.all(user.userId).map((item) => ({\n id: item.id,\n title: item.title,\n updatedAt: item.updatedAt,\n }));\n jsonResponse(res, 200, { success: true, items });\n }),\n);\n\napp.get(\n '/api/ai/conversations/:id',\n asyncHandler(async (req, res) => {\n const user = await resolveUser(req);\n const conversation = statements.getConversation.get(req.params.id, user.userId);\n if (!conversation) {\n return jsonResponse(res, 404, { success: false, message: 'conversation not found' });\n }\n const messages = statements.listMessages.all(req.params.id).map((item) => ({\n role: item.role,\n content: item.content,\n createdAt: item.createdAt,\n }));\n jsonResponse(res, 200, {\n success: true,\n conversation: {\n id: conversation.id,\n title: conversation.title,\n updatedAt: conversation.updated_at,\n },\n messages,\n });\n }),\n);\n\nfunction buildUrl(base, endpoint, params = {}) {\n const url = new URL(endpoint, `${base}/`);\n for (const [key, value] of Object.entries(params)) {\n if (Array.isArray(value)) {\n for (const item of value) {\n if (item !== undefined && item !== null && item !== '') {\n url.searchParams.append(key, String(item));\n }\n }\n } else if (value !== undefined && value !== null && value !== '') {\n url.searchParams.set(key, String(value));\n }\n }\n return url;\n}\n\nasync function fetchJson(url) {\n const response = await fetch(url, {\n headers: { Accept: 'application/json' },\n signal: AbortSignal.timeout(15_000),\n });\n if (!response.ok) {\n throw new Error(`Request failed: ${response.status}`);\n }\n return response.json();\n}\n\nfunction stripHtml(value) {\n return String(value || '')\n .replace(/<script[\\s\\S]*?<\\/script>/gi, ' ')\n .replace(/<style[\\s\\S]*?<\\/style>/gi, ' ')\n .replace(/<[^>]+>/g, ' ')\n .replace(/ /g, ' ')\n .replace(/</g, '<')\n .replace(/>/g, '>')\n .replace(/&/g, '&')\n .replace(/&#(\\d+);/g, (_match, code) => String.fromCharCode(Number(code)))\n .replace(/\\s+/g, ' ')\n .trim();\n}\n\nfunction cleanExtractedText(value) {\n let text = String(value || '')\n .replace(/\\b[A-Z]{1,3}\\d{2}n\\d{4,5}[a-z]?_p?\\d{4}[a-z]\\d{2}\\b/g, ' ')\n .replace(/\\uFFFD/g, '')\n .replace(/\\s+([,。;:!?、」』)》】])/g, '$1')\n .replace(/([「『《(【])\\s+/g, '$1');\n for (let i = 0; i < 3; i += 1) {\n text = text.replace(/([\\u3400-\\u9fff])\\s+([\\u3400-\\u9fff])/g, '$1$2');\n }\n return text.replace(/[ \\t]{2,}/g, ' ').trim();\n}\n\nfunction compactText(value, limit = 140) {\n const text = stripHtml(value);\n return text.length > limit ? `${text.slice(0, limit)}...` : text;\n}\n\nfunction collectSearchResults(data) {\n if (!data || typeof data !== 'object') return [];\n if (Array.isArray(data.results)) return data.results;\n if (Array.isArray(data?.results?.docs)) return data.results.docs;\n if (Array.isArray(data?.response?.docs)) return data.response.docs;\n if (Array.isArray(data.docs)) return data.docs;\n return [];\n}\n\nfunction firstText(item, keys) {\n for (const key of keys) {\n const value = item?.[key];\n if (Array.isArray(value)) {\n const first = value.map((entry) => safeUserText(String(entry))).find(Boolean);\n if (first) return first;\n }\n if (typeof value === 'string' && value.trim()) return value.trim();\n if (typeof value === 'number') return String(value);\n }\n return '';\n}\n\nfunction firstNumber(item, keys, fallback = undefined) {\n for (const key of keys) {\n const value = item?.[key];\n if (typeof value === 'number') return value;\n if (typeof value === 'string' && value.trim() && Number.isFinite(Number(value))) {\n return Number(value);\n }\n }\n return fallback;\n}\n\nasync function searchCbetaTitle(query, limit) {\n const url = buildUrl(cbetaApiRoot, 'search/title', { q: query, start: 0, rows: limit });\n const data = await fetchJson(url);\n return collectSearchResults(data).flatMap((item) => {\n const work = firstText(item, ['work', 'work_id', 'workId']);\n const title = firstText(item, ['content', 'title', 'work_title', 'workTitle']);\n if (!work || !title) return [];\n const juan = firstNumber(item, ['juan', 'juan_num', 'juanNum'], 1);\n return [\n {\n id: `cbeta-title-${work}-${juan}`,\n title,\n sourceName: 'CBETA 佛典',\n url: `cbeta:${work}:${juan}`,\n snippet: firstText(item, ['byline', 'creators', 'time_dynasty']) || '可下载经文正文并用于全球法布施。',\n resourceType: 'scripture',\n work,\n juan,\n },\n ];\n });\n}\n\nasync function searchCbetaContent(query, limit) {\n const endpoints = ['search/fulltext', 'search/content', 'search/all_in_one'];\n const settled = await Promise.allSettled(\n endpoints.map((endpoint) =>\n fetchJson(buildUrl(cbetaApiRoot, endpoint, { q: query, start: 0, rows: limit, field: 'content' })),\n ),\n );\n const results = [];\n for (const response of settled) {\n if (response.status !== 'fulfilled') continue;\n for (const item of collectSearchResults(response.value)) {\n const work = firstText(item, ['work', 'work_id', 'workId', 'sutra', 'id']);\n const snippet = compactText(firstText(item, ['kwic', 'highlight', 'snippet', 'content', 'text', 'body', 'p']));\n if (!work || !snippet) continue;\n const juan = firstNumber(item, ['juan', 'juan_num', 'juanNum', '卷'], 1);\n const title = firstText(item, ['title', 'work_title', 'workTitle', 'sutra_name', 'book', 'name']) || work;\n results.push({\n id: `cbeta-content-${work}-${juan}-${sha256(snippet).slice(0, 8)}`,\n title,\n sourceName: 'CBETA 佛典',\n url: `cbeta:${work}:${juan}`,\n snippet,\n resourceType: 'scripture',\n work,\n juan,\n });\n if (results.length >= limit) return results;\n }\n }\n return results;\n}\n\nfunction directUrlResource(query) {\n const url = safeUserText(query);\n let parsed;\n try {\n parsed = new URL(url);\n } catch {\n return null;\n }\n if (!['http:', 'https:'].includes(parsed.protocol)) return null;\n return {\n id: `url-${sha256(url).slice(0, 12)}`,\n title: parsed.hostname,\n sourceName: '网页资源',\n url,\n snippet: '直接下载并提取这个链接的可读正文。',\n resourceType: 'web',\n };\n}\n\nfunction looksLikeAudioQuery(query) {\n return /音乐|音樂|音频|音訊|mp3|audio|念诵|念誦|诵经|誦經|唱诵|唱誦|读诵|讀誦/.test(\n safeUserText(query).toLowerCase(),\n );\n}\n\nfunction archiveSearchTerm(query) {\n return (\n safeUserText(query)\n .replace(/下载|查找|寻找|找一?个|资源|音频|音訊|音乐|音樂|mp3|audio|念诵|念誦|诵经|誦經|唱诵|唱誦|读诵|讀誦/gi, ' ')\n .replace(/\\s+/g, ' ')\n .trim() || safeUserText(query)\n );\n}\n\nfunction solrPhrase(value) {\n return String(value).replace(/\\\\/g, '\\\\\\\\').replace(/\"/g, '\\\\\"');\n}\n\nasync function searchInternetArchiveAudio(query, limit) {\n if (!looksLikeAudioQuery(query)) return [];\n const term = archiveSearchTerm(query);\n if (!term) return [];\n const variants = Array.from(\n new Set([\n term,\n term.replaceAll('金刚经', '金剛經'),\n term.replaceAll('金剛經', '金刚经'),\n ].filter(Boolean)),\n );\n const phraseQuery = variants\n .flatMap((item) => [`title:\"${solrPhrase(item)}\"`, `description:\"${solrPhrase(item)}\"`])\n .join(' OR ');\n const url = buildUrl('https://archive.org', 'advancedsearch.php', {\n q: `(${phraseQuery}) AND mediatype:audio`,\n rows: Math.min(Math.max(limit, 1), 10),\n output: 'json',\n 'fl[]': ['identifier', 'title', 'description'],\n });\n const data = await fetchJson(url);\n const docs = Array.isArray(data?.response?.docs) ? data.response.docs : [];\n return docs\n .map((item) => {\n const identifier = firstText(item, ['identifier']);\n const title = firstText(item, ['title']) || identifier;\n if (!identifier || !title) return null;\n return {\n id: `ia-audio-${identifier}`,\n title,\n sourceName: 'Internet Archive 音频',\n url: `ia:${identifier}`,\n snippet: compactText(firstText(item, ['description'])) || '公开可访问的音频资源,将下载第一个可用 MP3 文件。',\n resourceType: 'audio',\n identifier,\n };\n })\n .filter(Boolean);\n}\n\nconst curatedCbetaWorks = [\n {\n work: 'T0235',\n juan: 1,\n title: '金剛般若波羅蜜經',\n keywords: ['金剛般若波羅蜜經', '金刚般若波罗蜜经', '金剛經', '金刚经'],\n snippet: '姚秦 鳩摩羅什譯。常用于读诵、共修与法布施的般若经典。',\n },\n {\n work: 'T0251',\n juan: 1,\n title: '般若波羅蜜多心經',\n keywords: ['般若波羅蜜多心經', '般若波罗蜜多心经', '心經', '心经'],\n snippet: '唐 玄奘譯。篇幅短,适合加入禅室功课本与日常持诵。',\n },\n {\n work: 'T0262',\n juan: 1,\n title: '妙法蓮華經',\n keywords: ['妙法蓮華經', '妙法莲华经', '法華經', '法华经'],\n snippet: '姚秦 鳩摩羅什譯。可按卷下载正文,用于长期功课与分享。',\n },\n {\n work: 'T0279',\n juan: 1,\n title: '大方廣佛華嚴經',\n keywords: ['大方廣佛華嚴經', '大方广佛华严经', '華嚴經', '华严经'],\n snippet: '唐 實叉難陀譯。可按卷下载正文,用于系统研读与法布施。',\n },\n {\n work: 'T0360',\n juan: 1,\n title: '佛說無量壽經',\n keywords: ['佛說無量壽經', '佛说无量寿经', '無量壽經', '无量寿经'],\n snippet: '净土经典。可下载正文,用于净土功课本与全球法布施。',\n },\n {\n work: 'T0365',\n juan: 1,\n title: '佛說觀無量壽佛經',\n keywords: ['佛說觀無量壽佛經', '佛说观无量寿佛经', '觀無量壽經', '观无量寿经'],\n snippet: '净土三经之一。适合查找下载后加入净土功课。',\n },\n {\n work: 'T0366',\n juan: 1,\n title: '佛說阿彌陀經',\n keywords: ['佛說阿彌陀經', '佛说阿弥陀经', '阿彌陀經', '阿弥陀经'],\n snippet: '姚秦 鳩摩羅什譯。篇幅适中,适合共修、持诵与分享。',\n },\n {\n work: 'T0475',\n juan: 1,\n title: '維摩詰所說經',\n keywords: ['維摩詰所說經', '维摩诘所说经', '維摩詰經', '维摩诘经'],\n snippet: '姚秦 鳩摩羅什譯。大乘经典,可按卷下载用于研读。',\n },\n {\n work: 'T0676',\n juan: 1,\n title: '解深密經',\n keywords: ['解深密經', '解深密经'],\n snippet: '唯识相关大乘经典。适合研读、整理笔记与法布施。',\n },\n {\n work: 'T1666',\n juan: 1,\n title: '大乘起信論',\n keywords: ['大乘起信論', '大乘起信论', '起信論', '起信论'],\n snippet: '大乘论典。适合下载后作为研习功课资料。',\n },\n];\n\nfunction localCbetaResults(query, limit) {\n const normalizedQuery = safeUserText(query).toLowerCase();\n if (!normalizedQuery) return [];\n return curatedCbetaWorks\n .filter((item) =>\n item.keywords.some((keyword) => {\n const normalizedKeyword = keyword.toLowerCase();\n return normalizedKeyword.includes(normalizedQuery) || normalizedQuery.includes(normalizedKeyword);\n }),\n )\n .slice(0, limit)\n .map((item) => ({\n id: `cbeta-curated-${item.work}-${item.juan}`,\n title: item.title,\n sourceName: 'CBETA 佛典',\n url: `cbeta:${item.work}:${item.juan}`,\n snippet: item.snippet,\n resourceType: 'scripture',\n work: item.work,\n juan: item.juan,\n }));\n}\n\nfunction uniqueResources(items, limit) {\n const seen = new Set();\n const results = [];\n for (const item of items) {\n const key = `${item.url}:${item.title}`;\n if (seen.has(key)) continue;\n seen.add(key);\n results.push(item);\n if (results.length >= limit) break;\n }\n return results;\n}\n\nasync function findResourceCandidates(query, limit) {\n const direct = directUrlResource(query);\n const results = [];\n if (direct) results.push(direct);\n try {\n results.push(...(await searchInternetArchiveAudio(query, limit)));\n } catch (error) {\n logger.warn({ error: String(error), query }, 'Internet Archive audio search skipped');\n }\n results.push(...localCbetaResults(query, limit));\n\n const [titleResults, contentResults] = await Promise.allSettled([\n searchCbetaTitle(query, limit),\n searchCbetaContent(query, limit),\n ]);\n if (titleResults.status === 'fulfilled') results.push(...titleResults.value);\n if (contentResults.status === 'fulfilled') results.push(...contentResults.value);\n\n return uniqueResources(results, limit);\n}\n\nfunction looksLikeResourceTask(message) {\n const text = safeUserText(message).toLowerCase();\n if (!text) return false;\n return /下载|查找|寻找|找一?个|资源|經文|经文|佛经|佛經|仪轨|儀軌|音乐|音樂|mp3|audio|念诵|念誦|诵经|誦經/.test(text);\n}\n\nfunction extractResourceQuery(message) {\n const text = safeUserText(message)\n .replace(/请|請|帮我|幫我|麻烦|麻煩|可以|能不能|需要|我要|想要/g, ' ')\n .replace(/下载|下載|查找|寻找|尋找|找一个|找一個|找|资源|資源|并|並|然后|然後|用于|用於|全球法布施|加入功课本|加入功課本/g, ' ')\n .replace(/\\s+/g, ' ')\n .trim();\n return text || safeUserText(message);\n}\n\nfunction resourceContextMessage(skillResult) {\n if (!skillResult) return '';\n const lines = [\n '本轮资源检索/下载已经由大乘 App 后端完成。',\n '不要再次调用 search_dharma_resources、download_dharma_resource 或其他资源 MCP 工具;请直接基于以下结果回答用户。',\n `后端已调用 skill: ${skillResult.skillName}`,\n `执行目标: ${skillResult.query}`,\n ];\n if (skillResult.selected) {\n lines.push(`候选资源: ${skillResult.selected.title}`);\n lines.push(`来源: ${skillResult.selected.sourceName}`);\n lines.push(`资源地址: ${skillResult.selected.url}`);\n }\n if (skillResult.downloaded) {\n lines.push(`下载标题: ${skillResult.downloaded.title}`);\n lines.push(`下载文件: ${skillResult.downloaded.fileName}`);\n const text = safeUserText(skillResult.downloaded.contentText);\n if (text) {\n lines.push('下载正文摘录:');\n lines.push(text.slice(0, 8000));\n }\n }\n return lines.join('\\n');\n}\n\nasync function runResourceFinderSkill({ message, user, onStep }) {\n if (!looksLikeResourceTask(message)) return null;\n\n const skillName = 'resource-finder-downloader';\n const query = extractResourceQuery(message);\n const audioTask = looksLikeAudioQuery(message) || looksLikeAudioQuery(query);\n onStep?.({\n type: 'skill.step',\n skillName,\n stage: 'start',\n title: '调用资源查找下载 skill',\n message: `识别到资源任务: ${query}`,\n });\n\n onStep?.({\n type: 'skill.step',\n skillName,\n stage: 'search',\n title: '搜索可分享资源',\n message: audioTask\n ? '优先检索公开可访问音频资源,失败时再回落到佛典正文资源。'\n : '优先检索 CBETA 佛典正文接口,搜索索引不可用时使用内置经典映射。',\n });\n const candidates = await findResourceCandidates(query, 8);\n if (candidates.length === 0) {\n onStep?.({\n type: 'skill.step',\n skillName,\n stage: 'empty',\n title: '没有找到可下载资源',\n message: '后端没有找到明确可下载且可分享的资源。',\n });\n return {\n skillName,\n query,\n candidates: [],\n selected: null,\n downloaded: null,\n };\n }\n\n const selected = candidates[0];\n onStep?.({\n type: 'skill.step',\n skillName,\n stage: 'select',\n title: '选择可信候选资源',\n message: `${selected.title} / ${selected.sourceName}`,\n });\n\n onStep?.({\n type: 'skill.step',\n skillName,\n stage: 'download',\n title: '下载并提取资源',\n message: selected.url,\n });\n const cbeta = parseCbetaResource(selected);\n const internetArchive = parseInternetArchiveResource(selected);\n const downloaded = cbeta\n ? await downloadCbetaResource({ ...selected, ...cbeta })\n : internetArchive\n ? await downloadInternetArchiveAudio({ ...selected, ...internetArchive })\n : await downloadWebResource(selected);\n const persisted = persistDownloadedResource(user, downloaded);\n\n onStep?.({\n type: 'skill.step',\n skillName,\n stage: 'done',\n title: '资源已准备完成',\n message: downloaded.fileName,\n });\n\n return {\n skillName,\n query,\n candidates,\n selected,\n downloaded: {\n ...downloaded,\n downloadId: persisted.id,\n },\n };\n}\n\napp.post(\n '/api/resources/search',\n asyncHandler(async (req, res) => {\n const query = safeUserText(req.body?.query);\n const limit = Math.min(Math.max(Number(req.body?.limit || 12), 1), 20);\n if (query.length < 2) {\n return jsonResponse(res, 400, { success: false, message: 'query is required' });\n }\n\n const items = await findResourceCandidates(query, limit);\n\n jsonResponse(res, 200, {\n success: true,\n source: 'resource-index',\n items,\n });\n }),\n);\n\nfunction parseCbetaResource(input) {\n const work = safeUserText(input.work);\n const juan = Number(input.juan || 1);\n if (work) return { work, juan: Number.isFinite(juan) ? juan : 1 };\n\n const url = safeUserText(input.url);\n const match = /^cbeta:([^:]+):(\\d+)$/i.exec(url);\n if (!match) return null;\n return {\n work: match[1].toUpperCase(),\n juan: Number(match[2] || 1),\n };\n}\n\nfunction parseInternetArchiveResource(input) {\n const url = safeUserText(input.url);\n const match = /^ia:([^:]+)$/i.exec(url);\n const identifier = match?.[1] || safeUserText(input.identifier);\n if (!identifier) return null;\n return { identifier };\n}\n\nfunction safeFileName(value) {\n const normalized = String(value || 'dharma-resource')\n .replace(/[\\\\/:*?\"<>|]+/g, '-')\n .replace(/\\s+/g, '-')\n .replace(/-+/g, '-')\n .replace(/^-|-$/g, '')\n .slice(0, 80);\n return normalized || 'dharma-resource';\n}\n\nfunction extractCbetaHtml(data) {\n const first = Array.isArray(data?.results) ? data.results[0] : null;\n if (typeof first === 'string') return first;\n if (first && typeof first === 'object' && typeof first.html === 'string') return first.html;\n if (typeof data?.html === 'string') return data.html;\n return '';\n}\n\nasync function downloadCbetaResource(resource) {\n const url = buildUrl(cbetaApiRoot, 'juans', {\n work: resource.work,\n juan: resource.juan,\n work_info: 1,\n toc: 1,\n });\n const data = await fetchJson(url);\n const workInfo = data.work_info || {};\n const title = safeUserText(workInfo.title) || safeUserText(resource.title) || resource.work;\n const html = extractCbetaHtml(data);\n const text = cleanExtractedText(stripHtml(html));\n if (!text) throw new Error('CBETA resource returned empty content');\n return {\n title,\n sourceName: 'CBETA 佛典',\n url: `cbeta:${resource.work}:${resource.juan}`,\n contentText: text.slice(0, maxResourceTextChars),\n fileName: `${resource.work}_${resource.juan}_${safeFileName(title)}.txt`,\n };\n}\n\nasync function downloadInternetArchiveAudio(resource) {\n const identifier = safeUserText(resource.identifier);\n if (!identifier) throw new Error('Internet Archive identifier is required');\n const metadata = await fetchJson(`https://archive.org/metadata/${encodeURIComponent(identifier)}`);\n const title = safeUserText(metadata?.metadata?.title) || safeUserText(resource.title) || identifier;\n const files = Array.isArray(metadata?.files) ? metadata.files : [];\n const audioFile = files.find((file) => {\n const name = safeUserText(file?.name);\n const format = safeUserText(file?.format).toLowerCase();\n return /\\.(mp3|m4a|ogg)$/i.test(name) || format.includes('mp3');\n });\n if (!audioFile?.name) {\n throw new Error('Internet Archive audio item has no downloadable audio file');\n }\n const sourceFileName = audioFile.name;\n const fileUrl = `https://archive.org/download/${encodeURIComponent(identifier)}/${sourceFileName\n .split('/')\n .map((part) => encodeURIComponent(part))\n .join('/')}`;\n const response = await fetch(fileUrl, {\n headers: {\n Accept: 'audio/mpeg, audio/*;q=0.9, */*;q=0.5',\n 'User-Agent': 'DachengResourceDownloader/1.0 (+https://ombhrum.com)',\n },\n signal: AbortSignal.timeout(60_000),\n });\n if (!response.ok) {\n throw new Error(`Internet Archive audio download failed: ${response.status}`);\n }\n const binaryContent = Buffer.from(await response.arrayBuffer());\n if (!binaryContent.length) throw new Error('Internet Archive audio returned empty content');\n const sizeMb = (binaryContent.length / 1024 / 1024).toFixed(1);\n return {\n title,\n sourceName: 'Internet Archive 音频',\n url: fileUrl,\n contentText: `已下载音频资源: ${title}\\n文件名: ${sourceFileName}\\n大小: ${sizeMb} MB\\n来源: ${fileUrl}`,\n binaryContent,\n fileName: safeFileName(sourceFileName),\n };\n}\n\nasync function downloadWebResource(input) {\n const url = safeUserText(input.url);\n const parsed = new URL(url);\n if (!['http:', 'https:'].includes(parsed.protocol)) {\n throw new Error('Only http/https resources can be downloaded');\n }\n\n const response = await fetch(parsed, {\n headers: {\n Accept: 'text/html, text/plain, application/xhtml+xml, application/xml;q=0.8, */*;q=0.5',\n 'User-Agent': 'DachengResourceDownloader/1.0 (+https://ombhrum.com)',\n },\n signal: AbortSignal.timeout(25_000),\n });\n if (!response.ok) throw new Error(`Resource download failed: ${response.status}`);\n const contentType = response.headers.get('content-type') || '';\n const body = await response.text();\n const title = safeUserText(input.title) || parsed.hostname;\n const contentText = contentType.includes('html') ? cleanExtractedText(stripHtml(body)) : body.trim();\n if (!contentText) throw new Error('Resource returned empty content');\n return {\n title,\n sourceName: safeUserText(input.sourceName) || parsed.hostname,\n url,\n contentText: contentText.slice(0, maxResourceTextChars),\n fileName: `${safeFileName(title)}.txt`,\n };\n}\n\nfunction persistDownloadedResource(user, content) {\n const id = crypto.randomUUID();\n const fileName = `${id}-${safeFileName(content.fileName)}`;\n const filePath = path.join(resourcesDir, fileName);\n if (content.binaryContent) {\n fs.writeFileSync(filePath, content.binaryContent);\n } else {\n fs.writeFileSync(filePath, content.contentText, 'utf8');\n }\n statements.insertResourceDownload.run({\n id,\n userId: user.userId,\n title: content.title,\n sourceName: content.sourceName,\n sourceUrl: content.url,\n fileName: content.fileName,\n filePath,\n createdAt: nowIso(),\n });\n return { id, fileName, filePath };\n}\n\nfunction createLibreChatMcpServer() {\n const server = new McpServer({\n name: 'dacheng-ai-tools',\n version: '0.1.0',\n });\n\n server.registerTool(\n 'search_dharma_resources',\n {\n title: 'Search dharma resources',\n description:\n 'Search for legally shareable Buddhist scripture, text, web, or audio resources that can be prepared for Fabushi sharing.',\n inputSchema: {\n query: z.string().min(2).max(200),\n limit: z.number().int().min(1).max(12).optional(),\n },\n outputSchema: {\n query: z.string(),\n items: z.array(\n z.object({\n id: z.string(),\n title: z.string(),\n sourceName: z.string(),\n url: z.string(),\n snippet: z.string(),\n resourceType: z.string(),\n work: z.string().optional(),\n juan: z.number().optional(),\n }),\n ),\n },\n annotations: {\n readOnlyHint: true,\n destructiveHint: false,\n openWorldHint: true,\n },\n },\n async ({ query, limit }) => {\n const items = await findResourceCandidates(query, limit ?? 8);\n return textToolResult(`Found ${items.length} candidate resources for \"${query}\".`, {\n query,\n items,\n });\n },\n );\n\n server.registerTool(\n 'download_dharma_resource',\n {\n title: 'Download dharma resource',\n description:\n 'Download and extract a selected resource. Use the url/work/juan returned by search_dharma_resources whenever possible.',\n inputSchema: {\n url: z.string().min(1).max(1000),\n title: z.string().max(300).optional(),\n sourceName: z.string().max(120).optional(),\n work: z.string().max(40).optional(),\n juan: z.number().int().min(1).optional(),\n identifier: z.string().max(300).optional(),\n },\n outputSchema: {\n title: z.string(),\n sourceName: z.string(),\n url: z.string(),\n fileName: z.string(),\n downloadId: z.string(),\n contentText: z.string(),\n },\n annotations: {\n readOnlyHint: false,\n destructiveHint: false,\n openWorldHint: true,\n },\n },\n async (input) => {\n const user = { userId: 'librechat:mcp' };\n const cbeta = parseCbetaResource(input);\n const internetArchive = parseInternetArchiveResource(input);\n const content = cbeta\n ? await downloadCbetaResource({ ...input, ...cbeta })\n : internetArchive\n ? await downloadInternetArchiveAudio({ ...input, ...internetArchive })\n : await downloadWebResource(input);\n const persisted = persistDownloadedResource(user, content);\n const { binaryContent: _binaryContent, ...publicContent } = content;\n return textToolResult(`Downloaded ${publicContent.title} as ${publicContent.fileName}.`, {\n ...publicContent,\n downloadId: persisted.id,\n });\n },\n );\n\n server.registerTool(\n 'prepare_dharma_share_text',\n {\n title: 'Prepare dharma sharing text',\n description:\n 'Prepare a text payload for the Fabushi app to confirm and add to global dharma sharing. This returns a clientAction; it does not start sending.',\n inputSchema: {\n title: z.string().min(1).max(120),\n text: z.string().min(1).max(20000),\n },\n outputSchema: {\n clientAction: z.object({\n type: z.literal('prepare_dharma_share_text'),\n title: z.string(),\n text: z.string(),\n }),\n },\n annotations: {\n readOnlyHint: false,\n destructiveHint: false,\n openWorldHint: false,\n },\n },\n async ({ title, text }) =>\n textToolResult(`Prepared \"${title}\" for user confirmation in the Fabushi app.`, {\n clientAction: {\n type: 'prepare_dharma_share_text',\n title: safeUserText(title).slice(0, 120),\n text: String(text || '').trim().slice(0, 20000),\n },\n }),\n );\n\n server.registerTool(\n 'prepare_practice_book_item',\n {\n title: 'Prepare practice book item',\n description:\n 'Prepare a scripture, chant, or practice item for the Fabushi app practice book. This returns a clientAction; it does not mutate the app by itself.',\n inputSchema: {\n title: z.string().min(1).max(120),\n sourceName: z.string().max(120).optional(),\n text: z.string().min(1).max(20000),\n },\n outputSchema: {\n clientAction: z.object({\n type: z.literal('prepare_practice_book_item'),\n title: z.string(),\n sourceName: z.string(),\n text: z.string(),\n }),\n },\n annotations: {\n readOnlyHint: false,\n destructiveHint: false,\n openWorldHint: false,\n },\n },\n async ({ title, sourceName, text }) =>\n textToolResult(`Prepared \"${title}\" for the Fabushi practice book confirmation flow.`, {\n clientAction: {\n type: 'prepare_practice_book_item',\n title: safeUserText(title).slice(0, 120),\n sourceName: safeUserText(sourceName || '大乘 AI').slice(0, 120),\n text: String(text || '').trim().slice(0, 20000),\n },\n }),\n );\n\n return server;\n}\n\nasync function handleMcpRequest(req, res) {\n const server = createLibreChatMcpServer();\n const transport = new StreamableHTTPServerTransport({\n sessionIdGenerator: undefined,\n enableJsonResponse: true,\n });\n\n res.on('close', () => {\n transport.close();\n server.close();\n });\n\n await server.connect(transport);\n await transport.handleRequest(req, res, req.body);\n}\n\napp.all('/mcp', async (req, res) => {\n try {\n await handleMcpRequest(req, res);\n } catch (error) {\n logger.error({ error: error.stack || String(error) }, 'MCP request failed');\n if (!res.headersSent) {\n res.status(500).json({\n error: 'MCP request failed',\n message: publicAiErrorMessage(error),\n });\n }\n }\n});\n\napp.post(\n '/api/resources/download',\n asyncHandler(async (req, res) => {\n const user = await resolveUser(req, req.body || {});\n const cbeta = parseCbetaResource(req.body || {});\n const internetArchive = parseInternetArchiveResource(req.body || {});\n const content = cbeta\n ? await downloadCbetaResource({ ...req.body, ...cbeta })\n : internetArchive\n ? await downloadInternetArchiveAudio({ ...req.body, ...internetArchive })\n : await downloadWebResource(req.body || {});\n const persisted = persistDownloadedResource(user, content);\n const { binaryContent: _binaryContent, ...publicContent } = content;\n jsonResponse(res, 200, {\n success: true,\n downloadId: persisted.id,\n ...publicContent,\n });\n }),\n);\n\napp.post(\n '/api/codex/resource-task',\n asyncHandler(async (req, res) => {\n if (process.env.ENABLE_CODEX_SDK !== 'true') {\n return jsonResponse(res, 403, {\n success: false,\n message: 'Codex SDK task runner is disabled. Set ENABLE_CODEX_SDK=true on the server to enable it.',\n });\n }\n const prompt = safeUserText(req.body?.prompt);\n if (!prompt) {\n return jsonResponse(res, 400, { success: false, message: 'prompt is required' });\n }\n const { Codex } = await import('@openai/codex-sdk');\n const codexRuntime = createCodexDeepSeekRuntime();\n const codex = new Codex(codexRuntime.enabled ? codexRuntime.options : {});\n const thread = codex.startThread(\n codexRuntime.enabled\n ? codexRuntime.threadOptions\n : { skipGitRepoCheck: true, networkAccessEnabled: true },\n );\n const result = await thread.run(prompt);\n jsonResponse(res, 200, {\n success: true,\n threadId: thread.id,\n provider: codexRuntime.provider,\n model: deepseekModel,\n result,\n });\n }),\n);\n\napp.use((error, _req, res, _next) => {\n const status = Number(error.statusCode || error.status || 500);\n const safeStatus = status >= 400 && status < 600 ? status : 500;\n logger.error({ error: error.stack || String(error) }, 'request failed');\n jsonResponse(res, safeStatus, {\n success: false,\n message: error.message || 'Internal Server Error',\n details: error.details,\n });\n});\n\nconst port = Number(process.env.PORT || 8788);\napp.listen(port, '0.0.0.0', () => {\n logger.info(\n {\n port,\n dbPath,\n dataDir,\n deepseekModel,\n cbetaApiRoot,\n },\n 'Dacheng AI backend started',\n );\n});\n"}, "a123d97fb8371710504a8535d0ca5e9878e632e5": {"t": 1785757991.5724072, "n": 92457, "h": "9e9461c35a804658", "c": "import crypto from 'node:crypto';\nimport fs from 'node:fs';\nimport path from 'node:path';\nimport process from 'node:process';\nimport { fileURLToPath } from 'node:url';\n\nimport { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';\nimport { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';\nimport Database from 'better-sqlite3';\nimport cors from 'cors';\nimport dotenv from 'dotenv';\nimport express from 'express';\nimport rateLimit from 'express-rate-limit';\nimport helmet from 'helmet';\nimport pino from 'pino';\nimport { z } from 'zod';\n\ndotenv.config();\n\nconst __dirname = path.dirname(fileURLToPath(import.meta.url));\nconst rootDir = path.resolve(__dirname, '..');\nconst dataDir = process.env.DATA_DIR || path.join(rootDir, 'data');\nconst resourcesDir = path.join(dataDir, 'resources');\nconst codexHomeDir = process.env.CODEX_HOME || path.join(dataDir, 'codex-home');\nconst codexTempDir = process.env.CODEX_TMPDIR || path.join(dataDir, 'codex-tmp');\nconst codexRuntimeDir = process.env.XDG_RUNTIME_DIR || path.join(dataDir, 'codex-runtime');\nconst dbPath = process.env.SQLITE_PATH || path.join(dataDir, 'dacheng-ai.sqlite');\n\nfs.mkdirSync(resourcesDir, { recursive: true });\nfs.mkdirSync(codexHomeDir, { recursive: true });\nfs.mkdirSync(codexTempDir, { recursive: true });\nfs.mkdirSync(codexRuntimeDir, { recursive: true });\n\nconst logger = pino({\n level: process.env.LOG_LEVEL || 'info',\n});\n\nconst app = express();\napp.disable('x-powered-by');\napp.set('trust proxy', Number(process.env.TRUST_PROXY_HOPS || 1));\napp.use(helmet());\nconst corsOrigin =\n !process.env.CORS_ORIGIN || process.env.CORS_ORIGIN.trim() === '*'\n ? true\n : process.env.CORS_ORIGIN.split(',').map((item) => item.trim()).filter(Boolean);\napp.use(cors({ origin: corsOrigin }));\napp.use(express.json({ limit: '1mb' }));\napp.use(\n rateLimit({\n windowMs: 60_000,\n limit: Number(process.env.RATE_LIMIT_PER_MINUTE || 90),\n standardHeaders: true,\n legacyHeaders: false,\n }),\n);\n\nconst db = new Database(dbPath);\ndb.pragma('journal_mode = WAL');\ndb.pragma('foreign_keys = ON');\n\ndb.exec(`\n CREATE TABLE IF NOT EXISTS conversations (\n id TEXT PRIMARY KEY,\n user_id TEXT NOT NULL,\n title TEXT NOT NULL,\n provider TEXT NOT NULL,\n model TEXT NOT NULL,\n created_at TEXT NOT NULL,\n updated_at TEXT NOT NULL\n );\n\n CREATE INDEX IF NOT EXISTS idx_conversations_user_updated\n ON conversations (user_id, updated_at DESC);\n\n CREATE TABLE IF NOT EXISTS messages (\n id TEXT PRIMARY KEY,\n conversation_id TEXT NOT NULL,\n role TEXT NOT NULL,\n content TEXT NOT NULL,\n prompt_tokens INTEGER NOT NULL DEFAULT 0,\n completion_tokens INTEGER NOT NULL DEFAULT 0,\n total_tokens INTEGER NOT NULL DEFAULT 0,\n created_at TEXT NOT NULL,\n FOREIGN KEY (conversation_id) REFERENCES conversations(id) ON DELETE CASCADE\n );\n\n CREATE INDEX IF NOT EXISTS idx_messages_conversation_created\n ON messages (conversation_id, created_at ASC);\n\n CREATE TABLE IF NOT EXISTS usage_months (\n user_id TEXT NOT NULL,\n month TEXT NOT NULL,\n total_tokens INTEGER NOT NULL DEFAULT 0,\n updated_at TEXT NOT NULL,\n PRIMARY KEY (user_id, month)\n );\n\n CREATE TABLE IF NOT EXISTS resource_downloads (\n id TEXT PRIMARY KEY,\n user_id TEXT NOT NULL,\n title TEXT NOT NULL,\n source_name TEXT NOT NULL,\n source_url TEXT NOT NULL,\n file_name TEXT NOT NULL,\n file_path TEXT NOT NULL,\n created_at TEXT NOT NULL\n );\n\n CREATE TABLE IF NOT EXISTS agent_runs (\n id TEXT PRIMARY KEY,\n user_id TEXT NOT NULL,\n conversation_id TEXT NOT NULL,\n message_id TEXT NOT NULL,\n openclaw_run_id TEXT,\n openclaw_session_key TEXT NOT NULL,\n status TEXT NOT NULL,\n mode TEXT NOT NULL,\n provider TEXT NOT NULL,\n model TEXT,\n input_tokens INTEGER NOT NULL DEFAULT 0,\n output_tokens INTEGER NOT NULL DEFAULT 0,\n tool_call_count INTEGER NOT NULL DEFAULT 0,\n cost_microusd INTEGER NOT NULL DEFAULT 0,\n started_at TEXT NOT NULL,\n completed_at TEXT,\n failed_at TEXT,\n error_code TEXT,\n error_message TEXT\n );\n\n CREATE INDEX IF NOT EXISTS idx_agent_runs_user_started\n ON agent_runs (user_id, started_at DESC);\n\n CREATE INDEX IF NOT EXISTS idx_agent_runs_status_started\n ON agent_runs (status, started_at DESC);\n\n CREATE TABLE IF NOT EXISTS agent_message_feedback (\n id TEXT PRIMARY KEY,\n message_id TEXT NOT NULL,\n user_id TEXT NOT NULL,\n rating TEXT NOT NULL,\n reason TEXT,\n comment TEXT,\n created_at TEXT NOT NULL\n );\n`);\n\nconst statements = {\n insertConversation: db.prepare(`\n INSERT INTO conversations (id, user_id, title, provider, model, created_at, updated_at)\n VALUES (@id, @userId, @title, @provider, @model, @createdAt, @updatedAt)\n `),\n updateConversation: db.prepare(`\n UPDATE conversations\n SET title = @title, updated_at = @updatedAt\n WHERE id = @id AND user_id = @userId\n `),\n getConversation: db.prepare(`\n SELECT * FROM conversations WHERE id = ? AND user_id = ? LIMIT 1\n `),\n listConversations: db.prepare(`\n SELECT id, title, updated_at AS updatedAt\n FROM conversations\n WHERE user_id = ?\n ORDER BY updated_at DESC\n LIMIT 30\n `),\n insertMessage: db.prepare(`\n INSERT INTO messages (\n id, conversation_id, role, content,\n prompt_tokens, completion_tokens, total_tokens, created_at\n )\n VALUES (\n @id, @conversationId, @role, @content,\n @promptTokens, @completionTokens, @totalTokens, @createdAt\n )\n `),\n listMessages: db.prepare(`\n SELECT role, content, prompt_tokens AS promptTokens,\n completion_tokens AS completionTokens, total_tokens AS totalTokens,\n created_at AS createdAt\n FROM messages\n WHERE conversation_id = ?\n ORDER BY created_at ASC\n `),\n getUsage: db.prepare(`\n SELECT total_tokens AS totalTokens FROM usage_months WHERE user_id = ? AND month = ?\n `),\n upsertUsage: db.prepare(`\n INSERT INTO usage_months (user_id, month, total_tokens, updated_at)\n VALUES (@userId, @month, @totalTokens, @updatedAt)\n ON CONFLICT(user_id, month)\n DO UPDATE SET total_tokens = usage_months.total_tokens + excluded.total_tokens,\n updated_at = excluded.updated_at\n `),\n insertResourceDownload: db.prepare(`\n INSERT INTO resource_downloads (\n id, user_id, title, source_name, source_url, file_name, file_path, created_at\n )\n VALUES (\n @id, @userId, @title, @sourceName, @sourceUrl, @fileName, @filePath, @createdAt\n )\n `),\n insertAgentRun: db.prepare(`\n INSERT INTO agent_runs (\n id, user_id, conversation_id, message_id, openclaw_run_id, openclaw_session_key,\n status, mode, provider, model, started_at\n )\n VALUES (\n @id, @userId, @conversationId, @messageId, @openClawRunId, @openClawSessionKey,\n @status, @mode, @provider, @model, @startedAt\n )\n `),\n getAgentRun: db.prepare(`\n SELECT * FROM agent_runs WHERE id = ? AND user_id = ? LIMIT 1\n `),\n updateAgentRunStatus: db.prepare(`\n UPDATE agent_runs\n SET status = @status,\n openclaw_run_id = COALESCE(@openClawRunId, openclaw_run_id),\n input_tokens = COALESCE(@inputTokens, input_tokens),\n output_tokens = COALESCE(@outputTokens, output_tokens),\n tool_call_count = COALESCE(@toolCallCount, tool_call_count),\n completed_at = @completedAt,\n failed_at = @failedAt,\n error_code = @errorCode,\n error_message = @errorMessage\n WHERE id = @id AND user_id = @userId\n `),\n insertAgentFeedback: db.prepare(`\n INSERT INTO agent_message_feedback (id, message_id, user_id, rating, reason, comment, created_at)\n VALUES (@id, @messageId, @userId, @rating, @reason, @comment, @createdAt)\n `),\n};\n\nconst deepseekApiKey = process.env.DEEPSEEK_API_KEY || '';\nconst deepseekBaseUrl = (process.env.DEEPSEEK_BASE_URL || 'https://api.deepseek.com').replace(/\\/+$/, '');\nconst deepseekModel = process.env.DEEPSEEK_MODEL || 'deepseek-chat';\nconst cbetaApiRoot = (process.env.CBETA_API_ROOT || 'https://144.24.17.21.sslip.io').replace(/\\/+$/, '');\nconst fabushiApiBaseUrl = (process.env.FABUSHI_API_BASE_URL || 'http://141.148.140.39').replace(/\\/+$/, '');\nconst memberMonthlyLimit = Number(process.env.MEMBER_MONTHLY_TOKEN_LIMIT || 1_000_000);\nconst freeMonthlyLimit = Number(process.env.FREE_MONTHLY_TOKEN_LIMIT || 50_000);\nconst maxResourceTextChars = Number(process.env.MAX_RESOURCE_TEXT_CHARS || 80_000);\nconst enableLibreChatAgentChat = process.env.ENABLE_LIBRECHAT_AGENT_CHAT === 'true';\nconst libreChatAgentsBaseUrl = (\n process.env.LIBRECHAT_AGENTS_BASE_URL || 'http://127.0.0.1:3080/api/agents/v1'\n).replace(/\\/+$/, '');\nconst libreChatAgentApiKey = process.env.LIBRECHAT_AGENT_API_KEY || '';\nconst libreChatAgentId = process.env.LIBRECHAT_AGENT_ID || '';\nconst enableCodexSdkChat = process.env.ENABLE_CODEX_SDK_CHAT === 'true';\nconst enableOpenClawAgentChat = process.env.ENABLE_OPENCLAW_AGENT_CHAT === 'true';\nconst openClawGatewayUrl = (process.env.OPENCLAW_GATEWAY_URL || 'http://127.0.0.1:18789').replace(/\\/+$/, '');\nconst openClawGatewayToken = process.env.OPENCLAW_GATEWAY_TOKEN || '';\nconst openClawAgentId = process.env.OPENCLAW_AGENT_ID || 'fabushi-public-agent';\nconst openClawRunsEndpoint = process.env.OPENCLAW_RUNS_ENDPOINT || '';\nconst codexDeepSeekProviderId = 'deepseek-chat-completions';\nconst codexResponsesBaseUrl = (\n process.env.CODEX_DEEPSEEK_RESPONSES_BASE_URL ||\n `http://127.0.0.1:${process.env.PORT || 8788}/codex-deepseek/v1`\n).replace(/\\/+$/, '');\n\nconst systemPrompt = [\n '你是“大乘”App 的 AI 助手。',\n '你的核心任务是帮助用户查找、整理、理解并全球法布施合法可分享的佛法资源。',\n '回答要庄重、简洁、可执行;涉及经典、仪轨或资源时,提醒用户尊重版权、来源和当地法规。',\n '如果用户想找资源、下载经文、寻找音频或准备可分享资料,你必须先在后端自动执行搜索、验证和下载步骤;不要要求用户去前端 + 菜单手动查找。',\n '你可以把执行进度总结给用户,但不要输出隐藏推理原文。',\n].join('\\n');\n\nfunction nowIso() {\n return new Date().toISOString();\n}\n\nfunction monthKey(date = new Date()) {\n return `${date.getUTCFullYear()}-${String(date.getUTCMonth() + 1).padStart(2, '0')}`;\n}\n\nfunction jsonResponse(res, status, payload) {\n res.status(status).json(payload);\n}\n\nfunction textToolResult(text, structuredContent = {}) {\n return {\n structuredContent,\n content: [{ type: 'text', text }],\n };\n}\n\nfunction sseHeaders(res) {\n res.status(200);\n res.setHeader('Content-Type', 'text/event-stream; charset=utf-8');\n res.setHeader('Cache-Control', 'no-cache, no-transform');\n res.setHeader('Connection', 'keep-alive');\n res.setHeader('X-Accel-Buffering', 'no');\n if (typeof res.flushHeaders === 'function') res.flushHeaders();\n}\n\nfunction writeSse(res, event, payload) {\n res.write(`event: ${event}\\n`);\n res.write(`data: ${JSON.stringify(payload)}\\n\\n`);\n}\n\nfunction asyncHandler(fn) {\n return (req, res, next) => {\n Promise.resolve(fn(req, res, next)).catch(next);\n };\n}\n\nfunction bearerToken(req) {\n const header = req.get('Authorization') || '';\n if (!header.startsWith('Bearer ')) return '';\n return header.slice('Bearer '.length).trim();\n}\n\nfunction sha256(value) {\n return crypto.createHash('sha256').update(value).digest('hex');\n}\n\nfunction safeUserText(value, fallback = '') {\n if (typeof value !== 'string') return fallback;\n return value.replace(/\\u0000/g, '').trim();\n}\n\nfunction estimateTokens(text) {\n const normalized = String(text || '').trim();\n if (!normalized) return 0;\n return Math.max(1, Math.ceil(normalized.length / 3));\n}\n\nfunction normalizeMembershipPayload(payload) {\n if (!payload || typeof payload !== 'object') return false;\n const data = payload.data && typeof payload.data === 'object' ? payload.data : payload;\n return Boolean(\n data.isActive ||\n data.active ||\n data.hasPremiumAccess ||\n data.hasPremiumMembership ||\n data.isPremium ||\n data.status === 'active' ||\n data.type === 'paid' ||\n data.membershipType === 'paid',\n );\n}\n\nasync function checkRemoteMembership(token) {\n if (!token || !fabushiApiBaseUrl) return false;\n const endpoints = [\n '/api/stripe/membership-status',\n '/api/alipay/check-membership',\n '/api/auth/user-info',\n ];\n\n for (const endpoint of endpoints) {\n try {\n const response = await fetch(`${fabushiApiBaseUrl}${endpoint}`, {\n headers: {\n Accept: 'application/json',\n Authorization: `Bearer ${token}`,\n },\n signal: AbortSignal.timeout(8_000),\n });\n if (!response.ok) continue;\n const payload = await response.json();\n if (normalizeMembershipPayload(payload)) return true;\n } catch (error) {\n logger.debug({ endpoint, error: String(error) }, 'membership lookup skipped');\n }\n }\n\n return false;\n}\n\nasync function resolveUser(req, body = {}) {\n const token = bearerToken(req);\n const username = safeUserText(body.username || req.query.username);\n const tokenHash = token ? sha256(token).slice(0, 24) : '';\n const anonymousHash = sha256(`${req.ip}|${req.get('User-Agent') || ''}`).slice(0, 24);\n const userId = username\n ? `user:${username}`\n : tokenHash\n ? `token:${tokenHash}`\n : `anon:${anonymousHash}`;\n const remoteMember = await checkRemoteMembership(token);\n const memberHint = body.clientMembershipHint === true;\n return {\n userId,\n username,\n tokenHash,\n isMember: remoteMember || memberHint,\n };\n}\n\nfunction usageFor(userId) {\n const month = monthKey();\n const row = statements.getUsage.get(userId, month);\n return {\n month,\n used: Number(row?.totalTokens || 0),\n };\n}\n\nfunction enforceTokenBudget(user, estimatedTokensForRequest) {\n const limit = user.isMember ? memberMonthlyLimit : freeMonthlyLimit;\n const usage = usageFor(user.userId);\n if (usage.used + estimatedTokensForRequest > limit) {\n const error = new Error('本月 AI token 额度已不足');\n error.statusCode = 429;\n error.details = {\n monthlyLimit: limit,\n usedTokens: usage.used,\n remainingTokens: Math.max(0, limit - usage.used),\n };\n throw error;\n }\n return { limit, usage };\n}\n\nfunction recordUsage(userId, totalTokens) {\n if (!totalTokens || totalTokens < 1) return;\n statements.upsertUsage.run({\n userId,\n month: monthKey(),\n totalTokens,\n updatedAt: nowIso(),\n });\n}\n\nfunction titleFromMessage(message) {\n const compact = message.replace(/\\s+/g, ' ').trim();\n if (!compact) return '新对话';\n return compact.length > 22 ? `${compact.slice(0, 22)}...` : compact;\n}\n\nfunction normalizeMessages(rows) {\n return rows.map((row) => ({\n role: row.role,\n content: row.content,\n }));\n}\n\nasync function callDeepSeek(messages) {\n if (!deepseekApiKey) {\n const error = new Error('DeepSeek API key is not configured');\n error.statusCode = 503;\n throw error;\n }\n\n const response = await fetch(`${deepseekBaseUrl}/chat/completions`, {\n method: 'POST',\n headers: {\n Accept: 'application/json',\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${deepseekApiKey}`,\n },\n body: JSON.stringify({\n model: deepseekModel,\n messages,\n temperature: 0.4,\n max_tokens: 1400,\n stream: false,\n }),\n signal: AbortSignal.timeout(60_000),\n });\n\n const bodyText = await response.text();\n let payload;\n try {\n payload = bodyText ? JSON.parse(bodyText) : {};\n } catch {\n payload = { error: bodyText };\n }\n\n if (!response.ok) {\n const upstreamMessage = payload?.error?.message || payload?.message || '';\n const friendlyMessage =\n response.status === 402 || /insufficient\\s+balance/i.test(upstreamMessage)\n ? '大乘 AI 服务额度暂不可用,请联系管理员充值 DeepSeek 账户后重试。'\n : upstreamMessage || `DeepSeek request failed: ${response.status}`;\n const error = new Error(friendlyMessage);\n error.statusCode = response.status;\n throw error;\n }\n\n const message = payload?.choices?.[0]?.message?.content?.trim();\n if (!message) {\n const error = new Error('DeepSeek returned an empty response');\n error.statusCode = 502;\n throw error;\n }\n\n return {\n message,\n usage: {\n promptTokens: Number(payload?.usage?.prompt_tokens || 0),\n completionTokens: Number(payload?.usage?.completion_tokens || 0),\n totalTokens: Number(payload?.usage?.total_tokens || 0),\n },\n };\n}\n\nasync function callDeepSeekStream(messages, callbacks = {}) {\n if (!deepseekApiKey) {\n const error = new Error('DeepSeek API key is not configured');\n error.statusCode = 503;\n throw error;\n }\n\n const response = await fetch(`${deepseekBaseUrl}/chat/completions`, {\n method: 'POST',\n headers: {\n Accept: 'text/event-stream',\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${deepseekApiKey}`,\n },\n body: JSON.stringify({\n model: deepseekModel,\n messages,\n temperature: 0.4,\n max_tokens: 1400,\n stream: true,\n stream_options: { include_usage: true },\n }),\n signal: callbacks.signal || AbortSignal.timeout(90_000),\n });\n\n if (!response.ok) {\n const bodyText = await response.text();\n let payload;\n try {\n payload = bodyText ? JSON.parse(bodyText) : {};\n } catch {\n payload = { error: bodyText };\n }\n const upstreamMessage = payload?.error?.message || payload?.message || '';\n const friendlyMessage =\n response.status === 402 || /insufficient\\s+balance/i.test(upstreamMessage)\n ? '大乘 AI 服务额度暂不可用,请联系管理员充值 DeepSeek 账户后重试。'\n : upstreamMessage || `DeepSeek request failed: ${response.status}`;\n const error = new Error(friendlyMessage);\n error.statusCode = response.status;\n throw error;\n }\n\n let buffer = '';\n let message = '';\n let usage = {\n promptTokens: 0,\n completionTokens: 0,\n totalTokens: 0,\n };\n const decoder = new TextDecoder();\n\n for await (const chunk of response.body) {\n buffer += decoder.decode(chunk, { stream: true });\n const lines = buffer.split(/\\r?\\n/);\n buffer = lines.pop() || '';\n\n for (const rawLine of lines) {\n const line = rawLine.trim();\n if (!line.startsWith('data:')) continue;\n const data = line.slice('data:'.length).trim();\n if (!data || data === '[DONE]') continue;\n\n let payload;\n try {\n payload = JSON.parse(data);\n } catch {\n continue;\n }\n\n const delta = payload?.choices?.[0]?.delta?.content || '';\n if (delta) {\n message += delta;\n callbacks.onToken?.(delta);\n }\n\n if (payload?.usage) {\n usage = {\n promptTokens: Number(payload.usage.prompt_tokens || usage.promptTokens || 0),\n completionTokens: Number(payload.usage.completion_tokens || usage.completionTokens || 0),\n totalTokens: Number(payload.usage.total_tokens || usage.totalTokens || 0),\n };\n }\n }\n }\n\n message = message.trim();\n if (!message) {\n const error = new Error('DeepSeek returned an empty response');\n error.statusCode = 502;\n throw error;\n }\n\n return { message, usage };\n}\n\nfunction createCodexDeepSeekRuntime() {\n if (!enableCodexSdkChat || !deepseekApiKey) {\n return {\n enabled: false,\n provider: 'deepseek-direct',\n reason: enableCodexSdkChat ? 'DeepSeek API key is not configured' : 'ENABLE_CODEX_SDK_CHAT is not true',\n };\n }\n\n return {\n enabled: true,\n provider: 'codex-sdk-deepseek',\n options: {\n apiKey: deepseekApiKey,\n baseUrl: deepseekBaseUrl,\n env: {\n ...process.env,\n DEEPSEEK_API_KEY: deepseekApiKey,\n HOME: codexHomeDir,\n CODEX_HOME: codexHomeDir,\n TMPDIR: codexTempDir,\n XDG_RUNTIME_DIR: codexRuntimeDir,\n XDG_CACHE_HOME: path.join(codexHomeDir, '.cache'),\n XDG_CONFIG_HOME: path.join(codexHomeDir, '.config'),\n XDG_DATA_HOME: path.join(codexHomeDir, '.local', 'share'),\n PATH: process.env.PATH || '/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin',\n },\n config: {\n model_provider: codexDeepSeekProviderId,\n model_providers: {\n [codexDeepSeekProviderId]: {\n name: 'DeepSeek Chat Completions',\n base_url: codexResponsesBaseUrl,\n env_key: 'DEEPSEEK_API_KEY',\n wire_api: 'responses',\n query_params: {},\n },\n },\n },\n },\n threadOptions: {\n model: deepseekModel,\n approvalPolicy: 'never',\n sandboxMode: 'read-only',\n skipGitRepoCheck: true,\n networkAccessEnabled: true,\n },\n };\n}\n\nfunction codexPromptFromMessages(messages) {\n return messages\n .map((message) => {\n const role = String(message.role || 'user').toUpperCase();\n return `${role}:\\n${message.content}`;\n })\n .join('\\n\\n');\n}\n\nfunction responseContentText(content) {\n if (typeof content === 'string') return content;\n if (!Array.isArray(content)) return '';\n return content\n .map((part) => {\n if (typeof part === 'string') return part;\n if (!part || typeof part !== 'object') return '';\n return String(part.text || part.input_text || part.output_text || '');\n })\n .filter(Boolean)\n .join('\\n');\n}\n\nfunction codexResponsesPrompt(body) {\n const input = Array.isArray(body?.input) ? body.input : [];\n const userItems = input.filter((item) => item?.role === 'user');\n const source = userItems[userItems.length - 1] || input[input.length - 1];\n const prompt = responseContentText(source?.content).trim();\n if (prompt) return prompt;\n if (typeof body?.input === 'string') return body.input.trim();\n return '';\n}\n\nfunction writeResponsesEvent(res, event, data) {\n res.write(`event: ${event}\\n`);\n res.write(`data: ${JSON.stringify(data)}\\n\\n`);\n}\n\nfunction responseUsage(usage = {}) {\n return {\n input_tokens: Number(usage.promptTokens || usage.input_tokens || 0),\n cached_input_tokens: 0,\n output_tokens: Number(usage.completionTokens || usage.output_tokens || 0),\n reasoning_output_tokens: 0,\n total_tokens: Number(usage.totalTokens || usage.total_tokens || 0),\n };\n}\n\nfunction createLibreChatAgentRuntime() {\n if (!enableLibreChatAgentChat || !libreChatAgentApiKey || !libreChatAgentId) {\n return {\n enabled: false,\n provider: 'librechat-agent',\n reason: !enableLibreChatAgentChat\n ? 'ENABLE_LIBRECHAT_AGENT_CHAT is not true'\n : !libreChatAgentApiKey\n ? 'LIBRECHAT_AGENT_API_KEY is not configured'\n : 'LIBRECHAT_AGENT_ID is not configured',\n };\n }\n\n return {\n enabled: true,\n provider: 'librechat-agent',\n model: libreChatAgentId,\n chatCompletionsUrl: `${libreChatAgentsBaseUrl}/chat/completions`,\n };\n}\n\nfunction libreChatUsage(usage = {}) {\n return {\n promptTokens: Number(usage.prompt_tokens || usage.input_tokens || usage.promptTokens || 0),\n completionTokens: Number(usage.completion_tokens || usage.output_tokens || usage.completionTokens || 0),\n totalTokens: Number(usage.total_tokens || usage.totalTokens || 0),\n };\n}\n\nasync function callLibreChatAgent(messages) {\n const runtime = createLibreChatAgentRuntime();\n if (!runtime.enabled) {\n const error = new Error(runtime.reason || 'LibreChat Agent chat is not enabled');\n error.statusCode = 503;\n throw error;\n }\n\n const response = await fetch(runtime.chatCompletionsUrl, {\n method: 'POST',\n headers: {\n Accept: 'application/json',\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${libreChatAgentApiKey}`,\n },\n body: JSON.stringify({\n model: runtime.model,\n messages,\n temperature: 0.4,\n stream: false,\n }),\n signal: AbortSignal.timeout(120_000),\n });\n\n const bodyText = await response.text();\n let payload;\n try {\n payload = bodyText ? JSON.parse(bodyText) : {};\n } catch {\n payload = { error: bodyText };\n }\n\n if (!response.ok) {\n const upstreamMessage = payload?.error?.message || payload?.message || bodyText || '';\n const error = new Error(upstreamMessage || `LibreChat Agent request failed: ${response.status}`);\n error.statusCode = response.status;\n throw error;\n }\n\n const message = payload?.choices?.[0]?.message?.content?.trim();\n if (!message) {\n const error = new Error('LibreChat Agent returned an empty response');\n error.statusCode = 502;\n throw error;\n }\n\n return {\n message,\n usage: libreChatUsage(payload?.usage),\n };\n}\n\nasync function callLibreChatAgentStream(messages, callbacks = {}) {\n const runtime = createLibreChatAgentRuntime();\n if (!runtime.enabled) {\n const error = new Error(runtime.reason || 'LibreChat Agent chat is not enabled');\n error.statusCode = 503;\n throw error;\n }\n\n const response = await fetch(runtime.chatCompletionsUrl, {\n method: 'POST',\n headers: {\n Accept: 'text/event-stream',\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${libreChatAgentApiKey}`,\n },\n body: JSON.stringify({\n model: runtime.model,\n messages,\n temperature: 0.4,\n stream: true,\n }),\n signal: callbacks.signal || AbortSignal.timeout(180_000),\n });\n\n if (!response.ok) {\n const bodyText = await response.text();\n let payload;\n try {\n payload = bodyText ? JSON.parse(bodyText) : {};\n } catch {\n payload = { error: bodyText };\n }\n const upstreamMessage = payload?.error?.message || payload?.message || bodyText || '';\n const error = new Error(upstreamMessage || `LibreChat Agent request failed: ${response.status}`);\n error.statusCode = response.status;\n throw error;\n }\n\n let buffer = '';\n let message = '';\n let usage = { promptTokens: 0, completionTokens: 0, totalTokens: 0 };\n const decoder = new TextDecoder();\n\n for await (const chunk of response.body) {\n buffer += decoder.decode(chunk, { stream: true });\n const lines = buffer.split(/\\r?\\n/);\n buffer = lines.pop() || '';\n\n for (const rawLine of lines) {\n const line = rawLine.trim();\n if (!line.startsWith('data:')) continue;\n const data = line.slice('data:'.length).trim();\n if (!data || data === '[DONE]') continue;\n\n let payload;\n try {\n payload = JSON.parse(data);\n } catch {\n continue;\n }\n\n const delta = payload?.choices?.[0]?.delta?.content || '';\n if (delta) {\n message += delta;\n callbacks.onToken?.(delta);\n }\n\n if (payload?.usage) {\n usage = libreChatUsage(payload.usage);\n }\n }\n }\n\n message = message.trim();\n if (!message) {\n const error = new Error('LibreChat Agent returned an empty response');\n error.statusCode = 502;\n throw error;\n }\n\n return { message, usage };\n}\n\nfunction isDeepSeekQuotaError(error) {\n const message = String(error?.message || error || '');\n return /额度|insufficient\\s+balance|balance/i.test(message);\n}\n\nfunction publicAiErrorMessage(error) {\n if (isDeepSeekQuotaError(error)) {\n return '大乘 AI 服务额度暂不可用,请联系管理员充值 DeepSeek 账户后重试。';\n }\n return error?.message || 'Internal Server Error';\n}\n\nfunction responsesPayload({\n responseId,\n itemId,\n status,\n model,\n text = '',\n usage = null,\n error = null,\n}) {\n return {\n id: responseId,\n object: 'response',\n status,\n model,\n output:\n status === 'completed'\n ? [\n {\n id: itemId,\n type: 'message',\n role: 'assistant',\n status: 'completed',\n content: [{ type: 'output_text', text }],\n },\n ]\n : [],\n ...(usage ? { usage: responseUsage(usage) } : {}),\n ...(error\n ? {\n error: {\n message: error.message || String(error),\n type: 'server_error',\n },\n }\n : {}),\n };\n}\n\nasync function callCodexSdkDeepSeek(messages, callbacks = {}) {\n const codexRuntime = createCodexDeepSeekRuntime();\n if (!codexRuntime.enabled) {\n const error = new Error(codexRuntime.reason || 'Codex SDK chat is not enabled');\n error.statusCode = 503;\n throw error;\n }\n\n const { Codex } = await import('@openai/codex-sdk');\n const codex = new Codex(codexRuntime.options);\n const thread = codex.startThread(codexRuntime.threadOptions);\n const prompt = codexPromptFromMessages(messages);\n\n if (callbacks.onToken || callbacks.onStep) {\n const { events } = await thread.runStreamed(prompt, { signal: callbacks.signal });\n let finalResponse = '';\n let usage = null;\n\n for await (const event of events) {\n if (event.type === 'item.completed') {\n const item = event.item;\n if (item.type === 'agent_message') {\n finalResponse = item.text || finalResponse;\n callbacks.onToken?.(item.text || '');\n } else if (item.type === 'todo_list') {\n callbacks.onStep?.({\n title: 'Codex SDK 执行计划',\n message: item.items.map((todo) => `${todo.completed ? '✓' : '•'} ${todo.text}`).join('\\n'),\n });\n } else if (item.type === 'command_execution') {\n callbacks.onStep?.({\n title: 'Codex SDK 执行命令',\n message: item.command,\n });\n } else if (item.type === 'mcp_tool_call') {\n callbacks.onStep?.({\n title: 'Codex SDK 调用工具',\n message: `${item.server}.${item.tool}`,\n });\n }\n } else if (event.type === 'turn.completed') {\n usage = event.usage;\n } else if (event.type === 'turn.failed') {\n throw new Error(event.error?.message || 'Codex SDK turn failed');\n } else if (event.type === 'error') {\n throw new Error(event.message || 'Codex SDK stream failed');\n }\n }\n\n if (!finalResponse.trim()) {\n const error = new Error('Codex SDK returned an empty response');\n error.statusCode = 502;\n throw error;\n }\n\n return {\n message: finalResponse.trim(),\n usage: {\n promptTokens: Number(usage?.input_tokens || 0),\n completionTokens: Number(usage?.output_tokens || 0),\n totalTokens: Number((usage?.input_tokens || 0) + (usage?.output_tokens || 0)),\n },\n };\n }\n\n const turn = await thread.run(prompt);\n if (!turn.finalResponse?.trim()) {\n const error = new Error('Codex SDK returned an empty response');\n error.statusCode = 502;\n throw error;\n }\n\n return {\n message: turn.finalResponse.trim(),\n usage: {\n promptTokens: Number(turn.usage?.input_tokens || 0),\n completionTokens: Number(turn.usage?.output_tokens || 0),\n totalTokens: Number((turn.usage?.input_tokens || 0) + (turn.usage?.output_tokens || 0)),\n },\n };\n}\n\nconst activeAgentRuns = new Map();\n\nfunction normalizeAgentMode(value) {\n const mode = safeUserText(value || 'dharma_guide');\n return /^[a-z][a-z0-9_:-]{0,48}$/i.test(mode) ? mode : 'dharma_guide';\n}\n\nfunction openClawSessionKey(user, conversationId) {\n const env = process.env.NODE_ENV === 'production' ? 'prod' : 'dev';\n return `fabushi:env:${env}:user:${user.userId}:conversation:${conversationId}`;\n}\n\nfunction createOpenClawRuntime() {\n if (!enableOpenClawAgentChat || !openClawGatewayToken || !openClawGatewayUrl) {\n return {\n enabled: false,\n provider: 'openclaw-gateway',\n reason: !enableOpenClawAgentChat\n ? 'ENABLE_OPENCLAW_AGENT_CHAT is not true'\n : !openClawGatewayToken\n ? 'OPENCLAW_GATEWAY_TOKEN is not configured'\n : 'OPENCLAW_GATEWAY_URL is not configured',\n };\n }\n\n return {\n enabled: true,\n provider: 'openclaw-gateway',\n model: openClawAgentId,\n runsUrl: openClawRunsEndpoint || `${openClawGatewayUrl}/v1/agents/${encodeURIComponent(openClawAgentId)}/runs`,\n };\n}\n\nfunction extractOpenClawText(payload) {\n if (!payload || typeof payload !== 'object') return '';\n const candidates = [\n payload.message,\n payload.content,\n payload.output_text,\n payload.finalResponse,\n payload.final_response,\n payload.result?.message,\n payload.result?.content,\n payload.output?.message,\n payload.output?.content,\n payload.choices?.[0]?.message?.content,\n ];\n for (const candidate of candidates) {\n if (typeof candidate === 'string' && candidate.trim()) return candidate.trim();\n }\n return '';\n}\n\nfunction extractOpenClawUsage(payload) {\n const usage = payload?.usage || payload?.result?.usage || {};\n return {\n promptTokens: Number(usage.input_tokens || usage.prompt_tokens || usage.promptTokens || 0),\n completionTokens: Number(usage.output_tokens || usage.completion_tokens || usage.completionTokens || 0),\n totalTokens: Number(usage.total_tokens || usage.totalTokens || 0),\n };\n}\n\nasync function callOpenClawAgent({ messages, user, conversationId, mode, signal }) {\n const runtime = createOpenClawRuntime();\n if (!runtime.enabled) {\n const error = new Error(runtime.reason || 'OpenClaw Agent chat is not enabled');\n error.statusCode = 503;\n throw error;\n }\n\n const sessionKey = openClawSessionKey(user, conversationId);\n const response = await fetch(runtime.runsUrl, {\n method: 'POST',\n headers: {\n Accept: 'application/json',\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${openClawGatewayToken}`,\n },\n body: JSON.stringify({\n agentId: openClawAgentId,\n sessionKey,\n mode,\n stream: false,\n input: { messages },\n messages,\n metadata: {\n app: 'fabushi',\n userId: user.userId,\n conversationId,\n toolPolicy: 'public-user-read-mostly',\n },\n }),\n signal: signal || AbortSignal.timeout(180_000),\n });\n\n const bodyText = await response.text();\n let payload;\n try {\n payload = bodyText ? JSON.parse(bodyText) : {};\n } catch {\n payload = { message: bodyText };\n }\n\n if (!response.ok) {\n const error = new Error(payload?.error?.message || payload?.message || `OpenClaw request failed: ${response.status}`);\n error.statusCode = response.status;\n throw error;\n }\n\n const message = extractOpenClawText(payload);\n if (!message) {\n const error = new Error('OpenClaw returned an empty response');\n error.statusCode = 502;\n throw error;\n }\n\n return {\n message,\n runId: payload.runId || payload.id || payload.result?.runId || null,\n provider: runtime.provider,\n model: runtime.model,\n usage: extractOpenClawUsage(payload),\n };\n}\n\nasync function runAgentModel({ messages, user, conversationId, mode, callbacks = {}, signal }) {\n const openClawRuntime = createOpenClawRuntime();\n if (openClawRuntime.enabled && !callbacks.onToken) {\n return await callOpenClawAgent({ messages, user, conversationId, mode, signal });\n }\n\n const codexRuntime = createCodexDeepSeekRuntime();\n const libreChatRuntime = createLibreChatAgentRuntime();\n let provider = libreChatRuntime.enabled\n ? libreChatRuntime.provider\n : codexRuntime.enabled\n ? codexRuntime.provider\n : 'deepseek';\n let model = libreChatRuntime.enabled ? libreChatRuntime.model : deepseekModel;\n\n try {\n if (libreChatRuntime.enabled) {\n const result = callbacks.onToken\n ? await callLibreChatAgentStream(messages, { ...callbacks, signal })\n : await callLibreChatAgent(messages);\n return { ...result, provider, model };\n }\n if (codexRuntime.enabled) {\n const result = await callCodexSdkDeepSeek(messages, { ...callbacks, signal });\n return { ...result, provider, model };\n }\n const result = callbacks.onToken\n ? await callDeepSeekStream(messages, { ...callbacks, signal })\n : await callDeepSeek(messages);\n return { ...result, provider, model };\n } catch (error) {\n if (!codexRuntime.enabled || isDeepSeekQuotaError(error)) throw error;\n logger.warn({ error: String(error) }, 'Agent model failed, falling back to DeepSeek direct');\n const result = callbacks.onToken\n ? await callDeepSeekStream(messages, { ...callbacks, signal })\n : await callDeepSeek(messages);\n return { ...result, provider: 'deepseek', model: deepseekModel };\n }\n}\n\nfunction updateAgentRunStatus(run, status, fields = {}) {\n statements.updateAgentRunStatus.run({\n id: run.id,\n userId: run.user_id || run.userId,\n status,\n openClawRunId: fields.openClawRunId ?? null,\n inputTokens: fields.inputTokens ?? null,\n outputTokens: fields.outputTokens ?? null,\n toolCallCount: fields.toolCallCount ?? null,\n completedAt: status === 'completed' || status === 'cancelled' ? nowIso() : null,\n failedAt: status === 'failed' ? nowIso() : null,\n errorCode: fields.errorCode ?? null,\n errorMessage: fields.errorMessage ?? null,\n });\n}\n\nfunction buildAgentMessages(conversationId) {\n const historyRows = statements.listMessages.all(conversationId).slice(-14);\n return [{ role: 'system', content: systemPrompt }, ...normalizeMessages(historyRows)];\n}\n\nfunction agentUsagePayload(usage, user, budget) {\n const promptTokens = usage.promptTokens || usage.inputTokens || 0;\n const completionTokens = usage.completionTokens || usage.outputTokens || 0;\n const totalTokens = usage.totalTokens || promptTokens + completionTokens;\n const latestUsage = usageFor(user.userId);\n return {\n inputTokens: promptTokens,\n outputTokens: completionTokens,\n totalTokens,\n monthlyLimit: budget.limit,\n remainingTokens: Math.max(0, budget.limit - latestUsage.used),\n };\n}\n\napp.post(\n '/codex-deepseek/v1/responses',\n asyncHandler(async (req, res) => {\n const bearer = bearerToken(req);\n if (!deepseekApiKey || bearer !== deepseekApiKey) {\n res.status(401).json({\n error: { message: 'Unauthorized Codex DeepSeek adapter request' },\n });\n return;\n }\n\n const prompt = codexResponsesPrompt(req.body);\n if (!prompt) {\n res.status(400).json({ error: { message: 'Responses input is required' } });\n return;\n }\n\n const model = String(req.body?.model || deepseekModel);\n const responseId = `resp_${crypto.randomUUID().replaceAll('-', '')}`;\n const itemId = `msg_${crypto.randomUUID().replaceAll('-', '')}`;\n const wantsStream =\n req.body?.stream !== false ||\n String(req.get('accept') || '').includes('text/event-stream');\n const messages = [{ role: 'user', content: prompt }];\n\n if (!wantsStream) {\n const result = await callDeepSeek(messages);\n res.json(\n responsesPayload({\n responseId,\n itemId,\n status: 'completed',\n model,\n text: result.message,\n usage: result.usage,\n }),\n );\n return;\n }\n\n res.status(200);\n res.set({\n 'Content-Type': 'text/event-stream; charset=utf-8',\n 'Cache-Control': 'no-cache, no-transform',\n Connection: 'keep-alive',\n 'X-Accel-Buffering': 'no',\n });\n res.flushHeaders?.();\n\n writeResponsesEvent(res, 'response.created', {\n type: 'response.created',\n response: responsesPayload({\n responseId,\n itemId,\n status: 'in_progress',\n model,\n }),\n });\n writeResponsesEvent(res, 'response.output_item.added', {\n type: 'response.output_item.added',\n output_index: 0,\n item: {\n id: itemId,\n type: 'message',\n role: 'assistant',\n status: 'in_progress',\n content: [],\n },\n });\n writeResponsesEvent(res, 'response.content_part.added', {\n type: 'response.content_part.added',\n item_id: itemId,\n output_index: 0,\n content_index: 0,\n part: { type: 'output_text', text: '' },\n });\n\n let text = '';\n try {\n const result = await callDeepSeekStream(messages, {\n onToken: (delta) => {\n text += delta;\n writeResponsesEvent(res, 'response.output_text.delta', {\n type: 'response.output_text.delta',\n item_id: itemId,\n output_index: 0,\n content_index: 0,\n delta,\n });\n },\n });\n text = result.message || text;\n writeResponsesEvent(res, 'response.output_text.done', {\n type: 'response.output_text.done',\n item_id: itemId,\n output_index: 0,\n content_index: 0,\n text,\n });\n writeResponsesEvent(res, 'response.content_part.done', {\n type: 'response.content_part.done',\n item_id: itemId,\n output_index: 0,\n content_index: 0,\n part: { type: 'output_text', text },\n });\n writeResponsesEvent(res, 'response.output_item.done', {\n type: 'response.output_item.done',\n output_index: 0,\n item: {\n id: itemId,\n type: 'message',\n role: 'assistant',\n status: 'completed',\n content: [{ type: 'output_text', text }],\n },\n });\n writeResponsesEvent(res, 'response.completed', {\n type: 'response.completed',\n response: responsesPayload({\n responseId,\n itemId,\n status: 'completed',\n model,\n text,\n usage: result.usage,\n }),\n });\n res.end();\n } catch (error) {\n writeResponsesEvent(res, 'response.failed', {\n type: 'response.failed',\n response: responsesPayload({\n responseId,\n itemId,\n status: 'failed',\n model,\n text,\n error,\n }),\n });\n res.end();\n }\n }),\n);\n\napp.get('/health', (_req, res) => {\n const codexRuntime = createCodexDeepSeekRuntime();\n const libreChatRuntime = createLibreChatAgentRuntime();\n const openClawRuntime = createOpenClawRuntime();\n jsonResponse(res, 200, {\n status: 'ok',\n service: 'dacheng-ai-backend',\n provider: libreChatRuntime.enabled ? libreChatRuntime.provider : 'deepseek',\n model: libreChatRuntime.enabled ? libreChatRuntime.model : deepseekModel,\n libreChatAgentChatEnabled: libreChatRuntime.enabled,\n libreChatAgentProvider: libreChatRuntime.provider,\n libreChatAgentReason: libreChatRuntime.enabled ? undefined : libreChatRuntime.reason,\n codexSdkAvailable: true,\n codexSdkChatEnabled: codexRuntime.enabled,\n codexSdkProvider: codexRuntime.provider,\n openClawAgentChatEnabled: openClawRuntime.enabled,\n openClawAgentProvider: openClawRuntime.provider,\n openClawAgentReason: openClawRuntime.enabled ? undefined : openClawRuntime.reason,\n cbetaApiRoot,\n timestamp: nowIso(),\n });\n});\n\napp.post(\n '/api/agent/chat',\n asyncHandler(async (req, res) => {\n const message = safeUserText(req.body?.message);\n if (!message) {\n return jsonResponse(res, 400, { success: false, message: 'message is required' });\n }\n\n const user = await resolveUser(req, req.body || {});\n const estimated = estimateTokens(message) + 600;\n const budget = enforceTokenBudget(user, estimated);\n const createdAt = nowIso();\n const conversationId = safeUserText(req.body?.conversationId) || crypto.randomUUID();\n const mode = normalizeAgentMode(req.body?.mode);\n const stream = req.body?.stream === true;\n let conversation = statements.getConversation.get(conversationId, user.userId);\n const isNewConversation = !conversation;\n\n if (isNewConversation) {\n conversation = { id: conversationId, user_id: user.userId, title: titleFromMessage(message) };\n statements.insertConversation.run({\n id: conversationId,\n userId: user.userId,\n title: conversation.title,\n provider: 'agent',\n model: openClawAgentId,\n createdAt,\n updatedAt: createdAt,\n });\n }\n\n const messageId = safeUserText(req.body?.messageId) || crypto.randomUUID();\n statements.insertMessage.run({\n id: messageId,\n conversationId,\n role: 'user',\n content: message,\n promptTokens: 0,\n completionTokens: 0,\n totalTokens: 0,\n createdAt,\n });\n\n const runId = `run_${crypto.randomUUID().replaceAll('-', '')}`;\n const sessionKey = openClawSessionKey(user, conversationId);\n const openClawRuntime = createOpenClawRuntime();\n statements.insertAgentRun.run({\n id: runId,\n userId: user.userId,\n conversationId,\n messageId,\n openClawRunId: null,\n openClawSessionKey: sessionKey,\n status: stream ? 'queued' : 'running',\n mode,\n provider: openClawRuntime.enabled ? openClawRuntime.provider : 'dacheng-ai-fallback',\n model: openClawRuntime.enabled ? openClawAgentId : deepseekModel,\n startedAt: createdAt,\n });\n\n if (stream) {\n return jsonResponse(res, 200, {\n success: true,\n runId,\n conversationId,\n streamUrl: `/api/agent/runs/${runId}/events`,\n });\n }\n\n const controller = new AbortController();\n activeAgentRuns.set(runId, controller);\n try {\n const modelMessages = buildAgentMessages(conversationId);\n const aiResult = await runAgentModel({\n messages: modelMessages,\n user,\n conversationId,\n mode,\n signal: controller.signal,\n });\n const usage = {\n promptTokens: aiResult.usage?.promptTokens || estimateTokens(JSON.stringify(modelMessages)),\n completionTokens: aiResult.usage?.completionTokens || estimateTokens(aiResult.message),\n totalTokens:\n aiResult.usage?.totalTokens ||\n estimateTokens(JSON.stringify(modelMessages)) + estimateTokens(aiResult.message),\n };\n recordUsage(user.userId, usage.totalTokens);\n const answeredAt = nowIso();\n const assistantMessageId = crypto.randomUUID();\n statements.insertMessage.run({\n id: assistantMessageId,\n conversationId,\n role: 'assistant',\n content: aiResult.message,\n promptTokens: usage.promptTokens,\n completionTokens: usage.completionTokens,\n totalTokens: usage.totalTokens,\n createdAt: answeredAt,\n });\n statements.updateConversation.run({\n id: conversationId,\n userId: user.userId,\n title: isNewConversation ? titleFromMessage(message) : conversation.title,\n updatedAt: answeredAt,\n });\n updateAgentRunStatus({ id: runId, user_id: user.userId }, 'completed', {\n openClawRunId: aiResult.runId,\n inputTokens: usage.promptTokens,\n outputTokens: usage.completionTokens,\n });\n return jsonResponse(res, 200, {\n success: true,\n runId,\n conversationId,\n messageId: assistantMessageId,\n provider: aiResult.provider,\n model: aiResult.model,\n message: aiResult.message,\n usage: agentUsagePayload(usage, user, budget),\n });\n } catch (error) {\n const status = error.name === 'AbortError' ? 'cancelled' : 'failed';\n updateAgentRunStatus({ id: runId, user_id: user.userId }, status, {\n errorCode: error.name || 'AGENT_RUN_FAILED',\n errorMessage: publicAiErrorMessage(error),\n });\n if (status === 'cancelled') {\n return jsonResponse(res, 499, { success: false, runId, status: 'cancelled' });\n }\n throw error;\n } finally {\n activeAgentRuns.delete(runId);\n }\n }),\n);\n\napp.get(\n '/api/agent/runs/:runId/events',\n asyncHandler(async (req, res) => {\n const user = await resolveUser(req, {});\n const run = statements.getAgentRun.get(req.params.runId, user.userId);\n if (!run) {\n return jsonResponse(res, 404, { success: false, message: 'run not found' });\n }\n if (run.status === 'completed' || run.status === 'failed' || run.status === 'cancelled') {\n return jsonResponse(res, 409, { success: false, message: `run is already ${run.status}` });\n }\n\n sseHeaders(res);\n const controller = new AbortController();\n activeAgentRuns.set(run.id, controller);\n updateAgentRunStatus(run, 'running');\n writeSse(res, 'run.started', { runId: run.id, conversationId: run.conversation_id });\n\n try {\n const estimated = 600;\n const budget = enforceTokenBudget(user, estimated);\n let streamedText = '';\n const modelMessages = buildAgentMessages(run.conversation_id);\n const aiResult = await runAgentModel({\n messages: modelMessages,\n user,\n conversationId: run.conversation_id,\n mode: run.mode,\n signal: controller.signal,\n callbacks: {\n onStep: (step) => writeSse(res, 'tool.call.completed', {\n tool: 'agent.step',\n displayName: step.title || 'Agent step',\n summary: step.message || '',\n }),\n onToken: (delta) => {\n streamedText += delta;\n writeSse(res, 'assistant.delta', { text: delta });\n },\n },\n });\n const message = aiResult.message || streamedText.trim();\n const usage = {\n promptTokens: aiResult.usage?.promptTokens || estimateTokens(JSON.stringify(modelMessages)),\n completionTokens: aiResult.usage?.completionTokens || estimateTokens(message),\n totalTokens:\n aiResult.usage?.totalTokens || estimateTokens(JSON.stringify(modelMessages)) + estimateTokens(message),\n };\n recordUsage(user.userId, usage.totalTokens);\n const assistantMessageId = crypto.randomUUID();\n const answeredAt = nowIso();\n statements.insertMessage.run({\n id: assistantMessageId,\n conversationId: run.conversation_id,\n role: 'assistant',\n content: message,\n promptTokens: usage.promptTokens,\n completionTokens: usage.completionTokens,\n totalTokens: usage.totalTokens,\n createdAt: answeredAt,\n });\n statements.updateConversation.run({\n id: run.conversation_id,\n userId: user.userId,\n title: titleFromMessage(message),\n updatedAt: answeredAt,\n });\n updateAgentRunStatus(run, 'completed', {\n openClawRunId: aiResult.runId,\n inputTokens: usage.promptTokens,\n outputTokens: usage.completionTokens,\n });\n writeSse(res, 'assistant.message', { messageId: assistantMessageId, content: message });\n writeSse(res, 'run.completed', { usage: agentUsagePayload(usage, user, budget) });\n res.end();\n } catch (error) {\n const status = error.name === 'AbortError' ? 'cancelled' : 'failed';\n updateAgentRunStatus(run, status, {\n errorCode: error.name || 'AGENT_RUN_FAILED',\n errorMessage: publicAiErrorMessage(error),\n });\n writeSse(res, status === 'cancelled' ? 'run.cancelled' : 'run.failed', {\n runId: run.id,\n message: publicAiErrorMessage(error),\n });\n res.end();\n } finally {\n activeAgentRuns.delete(run.id);\n }\n }),\n);\n\napp.post(\n '/api/agent/runs/:runId/cancel',\n asyncHandler(async (req, res) => {\n const user = await resolveUser(req, req.body || {});\n const run = statements.getAgentRun.get(req.params.runId, user.userId);\n if (!run) {\n return jsonResponse(res, 404, { success: false, message: 'run not found' });\n }\n const controller = activeAgentRuns.get(run.id);\n if (controller) controller.abort();\n updateAgentRunStatus(run, 'cancelled');\n return jsonResponse(res, 200, { success: true, runId: run.id, status: 'cancelled' });\n }),\n);\n\napp.post(\n '/api/agent/messages/:messageId/feedback',\n asyncHandler(async (req, res) => {\n const user = await resolveUser(req, req.body || {});\n const rating = safeUserText(req.body?.rating);\n if (!['up', 'down'].includes(rating)) {\n return jsonResponse(res, 400, { success: false, message: 'rating must be up or down' });\n }\n statements.insertAgentFeedback.run({\n id: crypto.randomUUID(),\n messageId: req.params.messageId,\n userId: user.userId,\n rating,\n reason: safeUserText(req.body?.reason),\n comment: safeUserText(req.body?.comment),\n createdAt: nowIso(),\n });\n return jsonResponse(res, 200, { success: true });\n }),\n);\n\napp.post(\n '/api/ai/chat',\n asyncHandler(async (req, res) => {\n const message = safeUserText(req.body?.message);\n if (!message) {\n return jsonResponse(res, 400, { success: false, message: 'message is required' });\n }\n\n const user = await resolveUser(req, req.body || {});\n const estimated = estimateTokens(message) + 600;\n const budget = enforceTokenBudget(user, estimated);\n const createdAt = nowIso();\n const conversationId = safeUserText(req.body?.conversationId) || crypto.randomUUID();\n let conversation = statements.getConversation.get(conversationId, user.userId);\n const isNewConversation = !conversation;\n\n if (isNewConversation) {\n conversation = {\n id: conversationId,\n user_id: user.userId,\n title: titleFromMessage(message),\n };\n statements.insertConversation.run({\n id: conversationId,\n userId: user.userId,\n title: conversation.title,\n provider: 'deepseek',\n model: deepseekModel,\n createdAt,\n updatedAt: createdAt,\n });\n }\n\n statements.insertMessage.run({\n id: crypto.randomUUID(),\n conversationId,\n role: 'user',\n content: message,\n promptTokens: 0,\n completionTokens: 0,\n totalTokens: 0,\n createdAt,\n });\n\n const libreChatRuntime = createLibreChatAgentRuntime();\n const skillResult = await runResourceFinderSkill({ message, user });\n const skillContext = resourceContextMessage(skillResult);\n const historyRows = statements.listMessages.all(conversationId).slice(-14);\n const modelMessages = [\n { role: 'system', content: systemPrompt },\n ...(skillContext ? [{ role: 'system', content: skillContext }] : []),\n ...normalizeMessages(historyRows),\n ];\n\n const codexRuntime = createCodexDeepSeekRuntime();\n let provider = libreChatRuntime.enabled ? libreChatRuntime.provider : 'deepseek';\n let responseModel = libreChatRuntime.enabled ? libreChatRuntime.model : deepseekModel;\n let aiResult;\n try {\n if (libreChatRuntime.enabled) {\n aiResult = await callLibreChatAgent(modelMessages);\n } else if (codexRuntime.enabled) {\n provider = codexRuntime.provider;\n aiResult = await callCodexSdkDeepSeek(modelMessages);\n } else {\n aiResult = await callDeepSeek(modelMessages);\n }\n } catch (error) {\n if (libreChatRuntime.enabled) {\n logger.warn({ error: String(error) }, 'LibreChat Agent chat failed, falling back to Codex SDK or DeepSeek direct');\n provider = codexRuntime.enabled ? codexRuntime.provider : 'deepseek';\n responseModel = deepseekModel;\n if (codexRuntime.enabled) {\n try {\n aiResult = await callCodexSdkDeepSeek(modelMessages);\n } catch (codexError) {\n if (isDeepSeekQuotaError(codexError)) throw codexError;\n logger.warn({ error: String(codexError) }, 'Codex SDK chat failed, falling back to DeepSeek direct');\n provider = 'deepseek';\n aiResult = await callDeepSeek(modelMessages);\n }\n } else {\n aiResult = await callDeepSeek(modelMessages);\n }\n } else {\n if (!codexRuntime.enabled || isDeepSeekQuotaError(error)) throw error;\n logger.warn({ error: String(error) }, 'Codex SDK chat failed, falling back to DeepSeek direct');\n provider = 'deepseek';\n aiResult = await callDeepSeek(modelMessages);\n }\n }\n const usage = {\n promptTokens: aiResult.usage.promptTokens || estimateTokens(JSON.stringify(modelMessages)),\n completionTokens: aiResult.usage.completionTokens || estimateTokens(aiResult.message),\n totalTokens:\n aiResult.usage.totalTokens ||\n estimateTokens(JSON.stringify(modelMessages)) + estimateTokens(aiResult.message),\n };\n recordUsage(user.userId, usage.totalTokens);\n\n const answeredAt = nowIso();\n statements.insertMessage.run({\n id: crypto.randomUUID(),\n conversationId,\n role: 'assistant',\n content: aiResult.message,\n promptTokens: usage.promptTokens,\n completionTokens: usage.completionTokens,\n totalTokens: usage.totalTokens,\n createdAt: answeredAt,\n });\n statements.updateConversation.run({\n id: conversationId,\n userId: user.userId,\n title: isNewConversation ? titleFromMessage(message) : conversation.title,\n updatedAt: answeredAt,\n });\n\n const latestUsage = usageFor(user.userId);\n const monthlyLimit = budget.limit;\n jsonResponse(res, 200, {\n success: true,\n conversationId,\n provider,\n model: responseModel,\n message: aiResult.message,\n usage: {\n ...usage,\n monthlyLimit,\n remainingTokens: Math.max(0, monthlyLimit - latestUsage.used),\n },\n });\n }),\n);\n\napp.post('/api/ai/chat/stream', async (req, res) => {\n sseHeaders(res);\n\n try {\n const message = safeUserText(req.body?.message);\n if (!message) {\n writeSse(res, 'error', { message: 'message is required' });\n res.end();\n return;\n }\n\n const user = await resolveUser(req, req.body || {});\n const estimated = estimateTokens(message) + 600;\n const budget = enforceTokenBudget(user, estimated);\n const createdAt = nowIso();\n const conversationId = safeUserText(req.body?.conversationId) || crypto.randomUUID();\n let conversation = statements.getConversation.get(conversationId, user.userId);\n const isNewConversation = !conversation;\n\n if (isNewConversation) {\n conversation = {\n id: conversationId,\n user_id: user.userId,\n title: titleFromMessage(message),\n };\n statements.insertConversation.run({\n id: conversationId,\n userId: user.userId,\n title: conversation.title,\n provider: 'deepseek',\n model: deepseekModel,\n createdAt,\n updatedAt: createdAt,\n });\n }\n\n statements.insertMessage.run({\n id: crypto.randomUUID(),\n conversationId,\n role: 'user',\n content: message,\n promptTokens: 0,\n completionTokens: 0,\n totalTokens: 0,\n createdAt,\n });\n\n const codexRuntime = createCodexDeepSeekRuntime();\n const libreChatRuntime = createLibreChatAgentRuntime();\n let provider = libreChatRuntime.enabled\n ? libreChatRuntime.provider\n : codexRuntime.enabled\n ? codexRuntime.provider\n : 'deepseek';\n let responseModel = libreChatRuntime.enabled ? libreChatRuntime.model : deepseekModel;\n writeSse(res, 'meta', {\n conversationId,\n provider,\n model: responseModel,\n });\n const skillResult = await runResourceFinderSkill({\n message,\n user,\n onStep: (step) => writeSse(res, 'step', step),\n });\n const skillContext = resourceContextMessage(skillResult);\n const historyRows = statements.listMessages.all(conversationId).slice(-14);\n const modelMessages = [\n { role: 'system', content: systemPrompt },\n ...(skillContext ? [{ role: 'system', content: skillContext }] : []),\n ...normalizeMessages(historyRows),\n ];\n\n let aiResult;\n try {\n if (libreChatRuntime.enabled) {\n aiResult = await callLibreChatAgentStream(modelMessages, {\n onToken: (token) => writeSse(res, 'delta', { text: token }),\n });\n } else if (codexRuntime.enabled) {\n aiResult = await callCodexSdkDeepSeek(modelMessages, {\n onToken: (token) => writeSse(res, 'delta', { text: token }),\n onStep: (step) => writeSse(res, 'step', step),\n });\n } else {\n aiResult = await callDeepSeekStream(modelMessages, {\n onToken: (token) => writeSse(res, 'delta', { text: token }),\n });\n }\n } catch (error) {\n if (libreChatRuntime.enabled) {\n logger.warn({ error: String(error) }, 'LibreChat Agent streaming chat failed, falling back to Codex SDK or DeepSeek direct');\n provider = codexRuntime.enabled ? codexRuntime.provider : 'deepseek';\n responseModel = deepseekModel;\n writeSse(res, 'meta', {\n conversationId,\n provider,\n model: responseModel,\n });\n if (codexRuntime.enabled) {\n try {\n aiResult = await callCodexSdkDeepSeek(modelMessages, {\n onToken: (token) => writeSse(res, 'delta', { text: token }),\n onStep: (step) => writeSse(res, 'step', step),\n });\n } catch (codexError) {\n if (isDeepSeekQuotaError(codexError)) throw codexError;\n logger.warn({ error: String(codexError) }, 'Codex SDK streaming chat failed, falling back to DeepSeek direct');\n provider = 'deepseek';\n responseModel = deepseekModel;\n writeSse(res, 'meta', {\n conversationId,\n provider,\n model: responseModel,\n });\n aiResult = await callDeepSeekStream(modelMessages, {\n onToken: (token) => writeSse(res, 'delta', { text: token }),\n });\n }\n } else {\n aiResult = await callDeepSeekStream(modelMessages, {\n onToken: (token) => writeSse(res, 'delta', { text: token }),\n });\n }\n } else {\n if (!codexRuntime.enabled || isDeepSeekQuotaError(error)) throw error;\n logger.warn({ error: String(error) }, 'Codex SDK streaming chat failed, falling back to DeepSeek direct');\n provider = 'deepseek';\n responseModel = deepseekModel;\n writeSse(res, 'meta', {\n conversationId,\n provider,\n model: responseModel,\n });\n aiResult = await callDeepSeekStream(modelMessages, {\n onToken: (token) => writeSse(res, 'delta', { text: token }),\n });\n }\n }\n\n const usage = {\n promptTokens: aiResult.usage.promptTokens || estimateTokens(JSON.stringify(modelMessages)),\n completionTokens: aiResult.usage.completionTokens || estimateTokens(aiResult.message),\n totalTokens:\n aiResult.usage.totalTokens ||\n estimateTokens(JSON.stringify(modelMessages)) + estimateTokens(aiResult.message),\n };\n recordUsage(user.userId, usage.totalTokens);\n\n const answeredAt = nowIso();\n statements.insertMessage.run({\n id: crypto.randomUUID(),\n conversationId,\n role: 'assistant',\n content: aiResult.message,\n promptTokens: usage.promptTokens,\n completionTokens: usage.completionTokens,\n totalTokens: usage.totalTokens,\n createdAt: answeredAt,\n });\n statements.updateConversation.run({\n id: conversationId,\n userId: user.userId,\n title: isNewConversation ? titleFromMessage(message) : conversation.title,\n updatedAt: answeredAt,\n });\n\n const latestUsage = usageFor(user.userId);\n const monthlyLimit = budget.limit;\n writeSse(res, 'done', {\n conversationId,\n provider,\n model: responseModel,\n message: aiResult.message,\n usage: {\n ...usage,\n monthlyLimit,\n remainingTokens: Math.max(0, monthlyLimit - latestUsage.used),\n },\n });\n res.end();\n } catch (error) {\n logger.error({ error: error.stack || String(error) }, 'streaming chat failed');\n writeSse(res, 'error', {\n message: publicAiErrorMessage(error),\n details: error.details,\n });\n res.end();\n }\n});\n\napp.get(\n '/api/ai/conversations',\n asyncHandler(async (req, res) => {\n const user = await resolveUser(req, { username: req.query.username });\n const items = statements.listConversations.all(user.userId).map((item) => ({\n id: item.id,\n title: item.title,\n updatedAt: item.updatedAt,\n }));\n jsonResponse(res, 200, { success: true, items });\n }),\n);\n\napp.get(\n '/api/ai/conversations/:id',\n asyncHandler(async (req, res) => {\n const user = await resolveUser(req);\n const conversation = statements.getConversation.get(req.params.id, user.userId);\n if (!conversation) {\n return jsonResponse(res, 404, { success: false, message: 'conversation not found' });\n }\n const messages = statements.listMessages.all(req.params.id).map((item) => ({\n role: item.role,\n content: item.content,\n createdAt: item.createdAt,\n }));\n jsonResponse(res, 200, {\n success: true,\n conversation: {\n id: conversation.id,\n title: conversation.title,\n updatedAt: conversation.updated_at,\n },\n messages,\n });\n }),\n);\n\nfunction buildUrl(base, endpoint, params = {}) {\n const url = new URL(endpoint, `${base}/`);\n for (const [key, value] of Object.entries(params)) {\n if (Array.isArray(value)) {\n for (const item of value) {\n if (item !== undefined && item !== null && item !== '') {\n url.searchParams.append(key, String(item));\n }\n }\n } else if (value !== undefined && value !== null && value !== '') {\n url.searchParams.set(key, String(value));\n }\n }\n return url;\n}\n\nasync function fetchJson(url) {\n const response = await fetch(url, {\n headers: { Accept: 'application/json' },\n signal: AbortSignal.timeout(15_000),\n });\n if (!response.ok) {\n throw new Error(`Request failed: ${response.status}`);\n }\n return response.json();\n}\n\nfunction stripHtml(value) {\n return String(value || '')\n .replace(/<script[\\s\\S]*?<\\/script>/gi, ' ')\n .replace(/<style[\\s\\S]*?<\\/style>/gi, ' ')\n .replace(/<[^>]+>/g, ' ')\n .replace(/ /g, ' ')\n .replace(/</g, '<')\n .replace(/>/g, '>')\n .replace(/&/g, '&')\n .replace(/&#(\\d+);/g, (_match, code) => String.fromCharCode(Number(code)))\n .replace(/\\s+/g, ' ')\n .trim();\n}\n\nfunction cleanExtractedText(value) {\n let text = String(value || '')\n .replace(/\\b[A-Z]{1,3}\\d{2}n\\d{4,5}[a-z]?_p?\\d{4}[a-z]\\d{2}\\b/g, ' ')\n .replace(/\\uFFFD/g, '')\n .replace(/\\s+([,。;:!?、」』)》】])/g, '$1')\n .replace(/([「『《(【])\\s+/g, '$1');\n for (let i = 0; i < 3; i += 1) {\n text = text.replace(/([\\u3400-\\u9fff])\\s+([\\u3400-\\u9fff])/g, '$1$2');\n }\n return text.replace(/[ \\t]{2,}/g, ' ').trim();\n}\n\nfunction compactText(value, limit = 140) {\n const text = stripHtml(value);\n return text.length > limit ? `${text.slice(0, limit)}...` : text;\n}\n\nfunction collectSearchResults(data) {\n if (!data || typeof data !== 'object') return [];\n if (Array.isArray(data.results)) return data.results;\n if (Array.isArray(data?.results?.docs)) return data.results.docs;\n if (Array.isArray(data?.response?.docs)) return data.response.docs;\n if (Array.isArray(data.docs)) return data.docs;\n return [];\n}\n\nfunction firstText(item, keys) {\n for (const key of keys) {\n const value = item?.[key];\n if (Array.isArray(value)) {\n const first = value.map((entry) => safeUserText(String(entry))).find(Boolean);\n if (first) return first;\n }\n if (typeof value === 'string' && value.trim()) return value.trim();\n if (typeof value === 'number') return String(value);\n }\n return '';\n}\n\nfunction firstNumber(item, keys, fallback = undefined) {\n for (const key of keys) {\n const value = item?.[key];\n if (typeof value === 'number') return value;\n if (typeof value === 'string' && value.trim() && Number.isFinite(Number(value))) {\n return Number(value);\n }\n }\n return fallback;\n}\n\nasync function searchCbetaTitle(query, limit) {\n const url = buildUrl(cbetaApiRoot, 'search/title', { q: query, start: 0, rows: limit });\n const data = await fetchJson(url);\n return collectSearchResults(data).flatMap((item) => {\n const work = firstText(item, ['work', 'work_id', 'workId']);\n const title = firstText(item, ['content', 'title', 'work_title', 'workTitle']);\n if (!work || !title) return [];\n const juan = firstNumber(item, ['juan', 'juan_num', 'juanNum'], 1);\n return [\n {\n id: `cbeta-title-${work}-${juan}`,\n title,\n sourceName: 'CBETA 佛典',\n url: `cbeta:${work}:${juan}`,\n snippet: firstText(item, ['byline', 'creators', 'time_dynasty']) || '可下载经文正文并用于全球法布施。',\n resourceType: 'scripture',\n work,\n juan,\n },\n ];\n });\n}\n\nasync function searchCbetaContent(query, limit) {\n const endpoints = ['search/fulltext', 'search/content', 'search/all_in_one'];\n const settled = await Promise.allSettled(\n endpoints.map((endpoint) =>\n fetchJson(buildUrl(cbetaApiRoot, endpoint, { q: query, start: 0, rows: limit, field: 'content' })),\n ),\n );\n const results = [];\n for (const response of settled) {\n if (response.status !== 'fulfilled') continue;\n for (const item of collectSearchResults(response.value)) {\n const work = firstText(item, ['work', 'work_id', 'workId', 'sutra', 'id']);\n const snippet = compactText(firstText(item, ['kwic', 'highlight', 'snippet', 'content', 'text', 'body', 'p']));\n if (!work || !snippet) continue;\n const juan = firstNumber(item, ['juan', 'juan_num', 'juanNum', '卷'], 1);\n const title = firstText(item, ['title', 'work_title', 'workTitle', 'sutra_name', 'book', 'name']) || work;\n results.push({\n id: `cbeta-content-${work}-${juan}-${sha256(snippet).slice(0, 8)}`,\n title,\n sourceName: 'CBETA 佛典',\n url: `cbeta:${work}:${juan}`,\n snippet,\n resourceType: 'scripture',\n work,\n juan,\n });\n if (results.length >= limit) return results;\n }\n }\n return results;\n}\n\nfunction directUrlResource(query) {\n const url = safeUserText(query);\n let parsed;\n try {\n parsed = new URL(url);\n } catch {\n return null;\n }\n if (!['http:', 'https:'].includes(parsed.protocol)) return null;\n return {\n id: `url-${sha256(url).slice(0, 12)}`,\n title: parsed.hostname,\n sourceName: '网页资源',\n url,\n snippet: '直接下载并提取这个链接的可读正文。',\n resourceType: 'web',\n };\n}\n\nfunction looksLikeAudioQuery(query) {\n return /音乐|音樂|音频|音訊|mp3|audio|念诵|念誦|诵经|誦經|唱诵|唱誦|读诵|讀誦/.test(\n safeUserText(query).toLowerCase(),\n );\n}\n\nfunction archiveSearchTerm(query) {\n return (\n safeUserText(query)\n .replace(/下载|查找|寻找|找一?个|资源|音频|音訊|音乐|音樂|mp3|audio|念诵|念誦|诵经|誦經|唱诵|唱誦|读诵|讀誦/gi, ' ')\n .replace(/\\s+/g, ' ')\n .trim() || safeUserText(query)\n );\n}\n\nfunction solrPhrase(value) {\n return String(value).replace(/\\\\/g, '\\\\\\\\').replace(/\"/g, '\\\\\"');\n}\n\nasync function searchInternetArchiveAudio(query, limit) {\n if (!looksLikeAudioQuery(query)) return [];\n const term = archiveSearchTerm(query);\n if (!term) return [];\n const variants = Array.from(\n new Set([\n term,\n term.replaceAll('金刚经', '金剛經'),\n term.replaceAll('金剛經', '金刚经'),\n ].filter(Boolean)),\n );\n const phraseQuery = variants\n .flatMap((item) => [`title:\"${solrPhrase(item)}\"`, `description:\"${solrPhrase(item)}\"`])\n .join(' OR ');\n const url = buildUrl('https://archive.org', 'advancedsearch.php', {\n q: `(${phraseQuery}) AND mediatype:audio`,\n rows: Math.min(Math.max(limit, 1), 10),\n output: 'json',\n 'fl[]': ['identifier', 'title', 'description'],\n });\n const data = await fetchJson(url);\n const docs = Array.isArray(data?.response?.docs) ? data.response.docs : [];\n return docs\n .map((item) => {\n const identifier = firstText(item, ['identifier']);\n const title = firstText(item, ['title']) || identifier;\n if (!identifier || !title) return null;\n return {\n id: `ia-audio-${identifier}`,\n title,\n sourceName: 'Internet Archive 音频',\n url: `ia:${identifier}`,\n snippet: compactText(firstText(item, ['description'])) || '公开可访问的音频资源,将下载第一个可用 MP3 文件。',\n resourceType: 'audio',\n identifier,\n };\n })\n .filter(Boolean);\n}\n\nconst curatedCbetaWorks = [\n {\n work: 'T0235',\n juan: 1,\n title: '金剛般若波羅蜜經',\n keywords: ['金剛般若波羅蜜經', '金刚般若波罗蜜经', '金剛經', '金刚经'],\n snippet: '姚秦 鳩摩羅什譯。常用于读诵、共修与法布施的般若经典。',\n },\n {\n work: 'T0251',\n juan: 1,\n title: '般若波羅蜜多心經',\n keywords: ['般若波羅蜜多心經', '般若波罗蜜多心经', '心經', '心经'],\n snippet: '唐 玄奘譯。篇幅短,适合加入禅室功课本与日常持诵。',\n },\n {\n work: 'T0262',\n juan: 1,\n title: '妙法蓮華經',\n keywords: ['妙法蓮華經', '妙法莲华经', '法華經', '法华经'],\n snippet: '姚秦 鳩摩羅什譯。可按卷下载正文,用于长期功课与分享。',\n },\n {\n work: 'T0279',\n juan: 1,\n title: '大方廣佛華嚴經',\n keywords: ['大方廣佛華嚴經', '大方广佛华严经', '華嚴經', '华严经'],\n snippet: '唐 實叉難陀譯。可按卷下载正文,用于系统研读与法布施。',\n },\n {\n work: 'T0360',\n juan: 1,\n title: '佛說無量壽經',\n keywords: ['佛說無量壽經', '佛说无量寿经', '無量壽經', '无量寿经'],\n snippet: '净土经典。可下载正文,用于净土功课本与全球法布施。',\n },\n {\n work: 'T0365',\n juan: 1,\n title: '佛說觀無量壽佛經',\n keywords: ['佛說觀無量壽佛經', '佛说观无量寿佛经', '觀無量壽經', '观无量寿经'],\n snippet: '净土三经之一。适合查找下载后加入净土功课。',\n },\n {\n work: 'T0366',\n juan: 1,\n title: '佛說阿彌陀經',\n keywords: ['佛說阿彌陀經', '佛说阿弥陀经', '阿彌陀經', '阿弥陀经'],\n snippet: '姚秦 鳩摩羅什譯。篇幅适中,适合共修、持诵与分享。',\n },\n {\n work: 'T0475',\n juan: 1,\n title: '維摩詰所說經',\n keywords: ['維摩詰所說經', '维摩诘所说经', '維摩詰經', '维摩诘经'],\n snippet: '姚秦 鳩摩羅什譯。大乘经典,可按卷下载用于研读。',\n },\n {\n work: 'T0676',\n juan: 1,\n title: '解深密經',\n keywords: ['解深密經', '解深密经'],\n snippet: '唯识相关大乘经典。适合研读、整理笔记与法布施。',\n },\n {\n work: 'T1666',\n juan: 1,\n title: '大乘起信論',\n keywords: ['大乘起信論', '大乘起信论', '起信論', '起信论'],\n snippet: '大乘论典。适合下载后作为研习功课资料。',\n },\n];\n\nfunction localCbetaResults(query, limit) {\n const normalizedQuery = safeUserText(query).toLowerCase();\n if (!normalizedQuery) return [];\n return curatedCbetaWorks\n .filter((item) =>\n item.keywords.some((keyword) => {\n const normalizedKeyword = keyword.toLowerCase();\n return normalizedKeyword.includes(normalizedQuery) || normalizedQuery.includes(normalizedKeyword);\n }),\n )\n .slice(0, limit)\n .map((item) => ({\n id: `cbeta-curated-${item.work}-${item.juan}`,\n title: item.title,\n sourceName: 'CBETA 佛典',\n url: `cbeta:${item.work}:${item.juan}`,\n snippet: item.snippet,\n resourceType: 'scripture',\n work: item.work,\n juan: item.juan,\n }));\n}\n\nfunction uniqueResources(items, limit) {\n const seen = new Set();\n const results = [];\n for (const item of items) {\n const key = `${item.url}:${item.title}`;\n if (seen.has(key)) continue;\n seen.add(key);\n results.push(item);\n if (results.length >= limit) break;\n }\n return results;\n}\n\nasync function findResourceCandidates(query, limit) {\n const direct = directUrlResource(query);\n const results = [];\n if (direct) results.push(direct);\n try {\n results.push(...(await searchInternetArchiveAudio(query, limit)));\n } catch (error) {\n logger.warn({ error: String(error), query }, 'Internet Archive audio search skipped');\n }\n results.push(...localCbetaResults(query, limit));\n\n const [titleResults, contentResults] = await Promise.allSettled([\n searchCbetaTitle(query, limit),\n searchCbetaContent(query, limit),\n ]);\n if (titleResults.status === 'fulfilled') results.push(...titleResults.value);\n if (contentResults.status === 'fulfilled') results.push(...contentResults.value);\n\n return uniqueResources(results, limit);\n}\n\nfunction looksLikeResourceTask(message) {\n const text = safeUserText(message).toLowerCase();\n if (!text) return false;\n return /下载|查找|寻找|找一?个|资源|經文|经文|佛经|佛經|仪轨|儀軌|音乐|音樂|mp3|audio|念诵|念誦|诵经|誦經/.test(text);\n}\n\nfunction extractResourceQuery(message) {\n const text = safeUserText(message)\n .replace(/请|請|帮我|幫我|麻烦|麻煩|可以|能不能|需要|我要|想要/g, ' ')\n .replace(/下载|下載|查找|寻找|尋找|找一个|找一個|找|资源|資源|并|並|然后|然後|用于|用於|全球法布施|加入功课本|加入功課本/g, ' ')\n .replace(/\\s+/g, ' ')\n .trim();\n return text || safeUserText(message);\n}\n\nfunction resourceContextMessage(skillResult) {\n if (!skillResult) return '';\n const lines = [\n '本轮资源检索/下载已经由大乘 App 后端完成。',\n '不要再次调用 search_dharma_resources、download_dharma_resource 或其他资源 MCP 工具;请直接基于以下结果回答用户。',\n `后端已调用 skill: ${skillResult.skillName}`,\n `执行目标: ${skillResult.query}`,\n ];\n if (skillResult.selected) {\n lines.push(`候选资源: ${skillResult.selected.title}`);\n lines.push(`来源: ${skillResult.selected.sourceName}`);\n lines.push(`资源地址: ${skillResult.selected.url}`);\n }\n if (skillResult.downloaded) {\n lines.push(`下载标题: ${skillResult.downloaded.title}`);\n lines.push(`下载文件: ${skillResult.downloaded.fileName}`);\n const text = safeUserText(skillResult.downloaded.contentText);\n if (text) {\n lines.push('下载正文摘录:');\n lines.push(text.slice(0, 8000));\n }\n }\n return lines.join('\\n');\n}\n\nasync function runResourceFinderSkill({ message, user, onStep }) {\n if (!looksLikeResourceTask(message)) return null;\n\n const skillName = 'resource-finder-downloader';\n const query = extractResourceQuery(message);\n const audioTask = looksLikeAudioQuery(message) || looksLikeAudioQuery(query);\n onStep?.({\n type: 'skill.step',\n skillName,\n stage: 'start',\n title: '调用资源查找下载 skill',\n message: `识别到资源任务: ${query}`,\n });\n\n onStep?.({\n type: 'skill.step',\n skillName,\n stage: 'search',\n title: '搜索可分享资源',\n message: audioTask\n ? '优先检索公开可访问音频资源,失败时再回落到佛典正文资源。'\n : '优先检索 CBETA 佛典正文接口,搜索索引不可用时使用内置经典映射。',\n });\n const candidates = await findResourceCandidates(query, 8);\n if (candidates.length === 0) {\n onStep?.({\n type: 'skill.step',\n skillName,\n stage: 'empty',\n title: '没有找到可下载资源',\n message: '后端没有找到明确可下载且可分享的资源。',\n });\n return {\n skillName,\n query,\n candidates: [],\n selected: null,\n downloaded: null,\n };\n }\n\n const selected = candidates[0];\n onStep?.({\n type: 'skill.step',\n skillName,\n stage: 'select',\n title: '选择可信候选资源',\n message: `${selected.title} / ${selected.sourceName}`,\n });\n\n onStep?.({\n type: 'skill.step',\n skillName,\n stage: 'download',\n title: '下载并提取资源',\n message: selected.url,\n });\n const cbeta = parseCbetaResource(selected);\n const internetArchive = parseInternetArchiveResource(selected);\n const downloaded = cbeta\n ? await downloadCbetaResource({ ...selected, ...cbeta })\n : internetArchive\n ? await downloadInternetArchiveAudio({ ...selected, ...internetArchive })\n : await downloadWebResource(selected);\n const persisted = persistDownloadedResource(user, downloaded);\n\n onStep?.({\n type: 'skill.step',\n skillName,\n stage: 'done',\n title: '资源已准备完成',\n message: downloaded.fileName,\n });\n\n return {\n skillName,\n query,\n candidates,\n selected,\n downloaded: {\n ...downloaded,\n downloadId: persisted.id,\n },\n };\n}\n\napp.post(\n '/api/resources/search',\n asyncHandler(async (req, res) => {\n const query = safeUserText(req.body?.query);\n const limit = Math.min(Math.max(Number(req.body?.limit || 12), 1), 20);\n if (query.length < 2) {\n return jsonResponse(res, 400, { success: false, message: 'query is required' });\n }\n\n const items = await findResourceCandidates(query, limit);\n\n jsonResponse(res, 200, {\n success: true,\n source: 'resource-index',\n items,\n });\n }),\n);\n\nfunction parseCbetaResource(input) {\n const work = safeUserText(input.work);\n const juan = Number(input.juan || 1);\n if (work) return { work, juan: Number.isFinite(juan) ? juan : 1 };\n\n const url = safeUserText(input.url);\n const match = /^cbeta:([^:]+):(\\d+)$/i.exec(url);\n if (!match) return null;\n return {\n work: match[1].toUpperCase(),\n juan: Number(match[2] || 1),\n };\n}\n\nfunction parseInternetArchiveResource(input) {\n const url = safeUserText(input.url);\n const match = /^ia:([^:]+)$/i.exec(url);\n const identifier = match?.[1] || safeUserText(input.identifier);\n if (!identifier) return null;\n return { identifier };\n}\n\nfunction safeFileName(value) {\n const normalized = String(value || 'dharma-resource')\n .replace(/[\\\\/:*?\"<>|]+/g, '-')\n .replace(/\\s+/g, '-')\n .replace(/-+/g, '-')\n .replace(/^-|-$/g, '')\n .slice(0, 80);\n return normalized || 'dharma-resource';\n}\n\nfunction extractCbetaHtml(data) {\n const first = Array.isArray(data?.results) ? data.results[0] : null;\n if (typeof first === 'string') return first;\n if (first && typeof first === 'object' && typeof first.html === 'string') return first.html;\n if (typeof data?.html === 'string') return data.html;\n return '';\n}\n\nasync function downloadCbetaResource(resource) {\n const url = buildUrl(cbetaApiRoot, 'juans', {\n work: resource.work,\n juan: resource.juan,\n work_info: 1,\n toc: 1,\n });\n const data = await fetchJson(url);\n const workInfo = data.work_info || {};\n const title = safeUserText(workInfo.title) || safeUserText(resource.title) || resource.work;\n const html = extractCbetaHtml(data);\n const text = cleanExtractedText(stripHtml(html));\n if (!text) throw new Error('CBETA resource returned empty content');\n return {\n title,\n sourceName: 'CBETA 佛典',\n url: `cbeta:${resource.work}:${resource.juan}`,\n contentText: text.slice(0, maxResourceTextChars),\n fileName: `${resource.work}_${resource.juan}_${safeFileName(title)}.txt`,\n };\n}\n\nasync function downloadInternetArchiveAudio(resource) {\n const identifier = safeUserText(resource.identifier);\n if (!identifier) throw new Error('Internet Archive identifier is required');\n const metadata = await fetchJson(`https://archive.org/metadata/${encodeURIComponent(identifier)}`);\n const title = safeUserText(metadata?.metadata?.title) || safeUserText(resource.title) || identifier;\n const files = Array.isArray(metadata?.files) ? metadata.files : [];\n const audioFile = files.find((file) => {\n const name = safeUserText(file?.name);\n const format = safeUserText(file?.format).toLowerCase();\n return /\\.(mp3|m4a|ogg)$/i.test(name) || format.includes('mp3');\n });\n if (!audioFile?.name) {\n throw new Error('Internet Archive audio item has no downloadable audio file');\n }\n const sourceFileName = audioFile.name;\n const fileUrl = `https://archive.org/download/${encodeURIComponent(identifier)}/${sourceFileName\n .split('/')\n .map((part) => encodeURIComponent(part))\n .join('/')}`;\n const response = await fetch(fileUrl, {\n headers: {\n Accept: 'audio/mpeg, audio/*;q=0.9, */*;q=0.5',\n 'User-Agent': 'DachengResourceDownloader/1.0 (+https://ombhrum.com)',\n },\n signal: AbortSignal.timeout(60_000),\n });\n if (!response.ok) {\n throw new Error(`Internet Archive audio download failed: ${response.status}`);\n }\n const binaryContent = Buffer.from(await response.arrayBuffer());\n if (!binaryContent.length) throw new Error('Internet Archive audio returned empty content');\n const sizeMb = (binaryContent.length / 1024 / 1024).toFixed(1);\n return {\n title,\n sourceName: 'Internet Archive 音频',\n url: fileUrl,\n contentText: `已下载音频资源: ${title}\\n文件名: ${sourceFileName}\\n大小: ${sizeMb} MB\\n来源: ${fileUrl}`,\n binaryContent,\n fileName: safeFileName(sourceFileName),\n };\n}\n\nasync function downloadWebResource(input) {\n const url = safeUserText(input.url);\n const parsed = new URL(url);\n if (!['http:', 'https:'].includes(parsed.protocol)) {\n throw new Error('Only http/https resources can be downloaded');\n }\n\n const response = await fetch(parsed, {\n headers: {\n Accept: 'text/html, text/plain, application/xhtml+xml, application/xml;q=0.8, */*;q=0.5',\n 'User-Agent': 'DachengResourceDownloader/1.0 (+https://ombhrum.com)',\n },\n signal: AbortSignal.timeout(25_000),\n });\n if (!response.ok) throw new Error(`Resource download failed: ${response.status}`);\n const contentType = response.headers.get('content-type') || '';\n const body = await response.text();\n const title = safeUserText(input.title) || parsed.hostname;\n const contentText = contentType.includes('html') ? cleanExtractedText(stripHtml(body)) : body.trim();\n if (!contentText) throw new Error('Resource returned empty content');\n return {\n title,\n sourceName: safeUserText(input.sourceName) || parsed.hostname,\n url,\n contentText: contentText.slice(0, maxResourceTextChars),\n fileName: `${safeFileName(title)}.txt`,\n };\n}\n\nfunction persistDownloadedResource(user, content) {\n const id = crypto.randomUUID();\n const fileName = `${id}-${safeFileName(content.fileName)}`;\n const filePath = path.join(resourcesDir, fileName);\n if (content.binaryContent) {\n fs.writeFileSync(filePath, content.binaryContent);\n } else {\n fs.writeFileSync(filePath, content.contentText, 'utf8');\n }\n statements.insertResourceDownload.run({\n id,\n userId: user.userId,\n title: content.title,\n sourceName: content.sourceName,\n sourceUrl: content.url,\n fileName: content.fileName,\n filePath,\n createdAt: nowIso(),\n });\n return { id, fileName, filePath };\n}\n\nfunction createLibreChatMcpServer() {\n const server = new McpServer({\n name: 'dacheng-ai-tools',\n version: '0.1.0',\n });\n\n server.registerTool(\n 'search_dharma_resources',\n {\n title: 'Search dharma resources',\n description:\n 'Search for legally shareable Buddhist scripture, text, web, or audio resources that can be prepared for Fabushi sharing.',\n inputSchema: {\n query: z.string().min(2).max(200),\n limit: z.number().int().min(1).max(12).optional(),\n },\n outputSchema: {\n query: z.string(),\n items: z.array(\n z.object({\n id: z.string(),\n title: z.string(),\n sourceName: z.string(),\n url: z.string(),\n snippet: z.string(),\n resourceType: z.string(),\n work: z.string().optional(),\n juan: z.number().optional(),\n }),\n ),\n },\n annotations: {\n readOnlyHint: true,\n destructiveHint: false,\n openWorldHint: true,\n },\n },\n async ({ query, limit }) => {\n const items = await findResourceCandidates(query, limit ?? 8);\n return textToolResult(`Found ${items.length} candidate resources for \"${query}\".`, {\n query,\n items,\n });\n },\n );\n\n server.registerTool(\n 'download_dharma_resource',\n {\n title: 'Download dharma resource',\n description:\n 'Download and extract a selected resource. Use the url/work/juan returned by search_dharma_resources whenever possible.',\n inputSchema: {\n url: z.string().min(1).max(1000),\n title: z.string().max(300).optional(),\n sourceName: z.string().max(120).optional(),\n work: z.string().max(40).optional(),\n juan: z.number().int().min(1).optional(),\n identifier: z.string().max(300).optional(),\n },\n outputSchema: {\n title: z.string(),\n sourceName: z.string(),\n url: z.string(),\n fileName: z.string(),\n downloadId: z.string(),\n contentText: z.string(),\n },\n annotations: {\n readOnlyHint: false,\n destructiveHint: false,\n openWorldHint: true,\n },\n },\n async (input) => {\n const user = { userId: 'librechat:mcp' };\n const cbeta = parseCbetaResource(input);\n const internetArchive = parseInternetArchiveResource(input);\n const content = cbeta\n ? await downloadCbetaResource({ ...input, ...cbeta })\n : internetArchive\n ? await downloadInternetArchiveAudio({ ...input, ...internetArchive })\n : await downloadWebResource(input);\n const persisted = persistDownloadedResource(user, content);\n const { binaryContent: _binaryContent, ...publicContent } = content;\n return textToolResult(`Downloaded ${publicContent.title} as ${publicContent.fileName}.`, {\n ...publicContent,\n downloadId: persisted.id,\n });\n },\n );\n\n server.registerTool(\n 'prepare_dharma_share_text',\n {\n title: 'Prepare dharma sharing text',\n description:\n 'Prepare a text payload for the Fabushi app to confirm and add to global dharma sharing. This returns a clientAction; it does not start sending.',\n inputSchema: {\n title: z.string().min(1).max(120),\n text: z.string().min(1).max(20000),\n },\n outputSchema: {\n clientAction: z.object({\n type: z.literal('prepare_dharma_share_text'),\n title: z.string(),\n text: z.string(),\n }),\n },\n annotations: {\n readOnlyHint: false,\n destructiveHint: false,\n openWorldHint: false,\n },\n },\n async ({ title, text }) =>\n textToolResult(`Prepared \"${title}\" for user confirmation in the Fabushi app.`, {\n clientAction: {\n type: 'prepare_dharma_share_text',\n title: safeUserText(title).slice(0, 120),\n text: String(text || '').trim().slice(0, 20000),\n },\n }),\n );\n\n server.registerTool(\n 'prepare_practice_book_item',\n {\n title: 'Prepare practice book item',\n description:\n 'Prepare a scripture, chant, or practice item for the Fabushi app practice book. This returns a clientAction; it does not mutate the app by itself.',\n inputSchema: {\n title: z.string().min(1).max(120),\n sourceName: z.string().max(120).optional(),\n text: z.string().min(1).max(20000),\n },\n outputSchema: {\n clientAction: z.object({\n type: z.literal('prepare_practice_book_item'),\n title: z.string(),\n sourceName: z.string(),\n text: z.string(),\n }),\n },\n annotations: {\n readOnlyHint: false,\n destructiveHint: false,\n openWorldHint: false,\n },\n },\n async ({ title, sourceName, text }) =>\n textToolResult(`Prepared \"${title}\" for the Fabushi practice book confirmation flow.`, {\n clientAction: {\n type: 'prepare_practice_book_item',\n title: safeUserText(title).slice(0, 120),\n sourceName: safeUserText(sourceName || '大乘 AI').slice(0, 120),\n text: String(text || '').trim().slice(0, 20000),\n },\n }),\n );\n\n return server;\n}\n\nasync function handleMcpRequest(req, res) {\n const server = createLibreChatMcpServer();\n const transport = new StreamableHTTPServerTransport({\n sessionIdGenerator: undefined,\n enableJsonResponse: true,\n });\n\n res.on('close', () => {\n transport.close();\n server.close();\n });\n\n await server.connect(transport);\n await transport.handleRequest(req, res, req.body);\n}\n\napp.all('/mcp', async (req, res) => {\n try {\n await handleMcpRequest(req, res);\n } catch (error) {\n logger.error({ error: error.stack || String(error) }, 'MCP request failed');\n if (!res.headersSent) {\n res.status(500).json({\n error: 'MCP request failed',\n message: publicAiErrorMessage(error),\n });\n }\n }\n});\n\napp.post(\n '/api/resources/download',\n asyncHandler(async (req, res) => {\n const user = await resolveUser(req, req.body || {});\n const cbeta = parseCbetaResource(req.body || {});\n const internetArchive = parseInternetArchiveResource(req.body || {});\n const content = cbeta\n ? await downloadCbetaResource({ ...req.body, ...cbeta })\n : internetArchive\n ? await downloadInternetArchiveAudio({ ...req.body, ...internetArchive })\n : await downloadWebResource(req.body || {});\n const persisted = persistDownloadedResource(user, content);\n const { binaryContent: _binaryContent, ...publicContent } = content;\n jsonResponse(res, 200, {\n success: true,\n downloadId: persisted.id,\n ...publicContent,\n });\n }),\n);\n\napp.post(\n '/api/codex/resource-task',\n asyncHandler(async (req, res) => {\n if (process.env.ENABLE_CODEX_SDK !== 'true') {\n return jsonResponse(res, 403, {\n success: false,\n message: 'Codex SDK task runner is disabled. Set ENABLE_CODEX_SDK=true on the server to enable it.',\n });\n }\n const prompt = safeUserText(req.body?.prompt);\n if (!prompt) {\n return jsonResponse(res, 400, { success: false, message: 'prompt is required' });\n }\n const { Codex } = await import('@openai/codex-sdk');\n const codexRuntime = createCodexDeepSeekRuntime();\n const codex = new Codex(codexRuntime.enabled ? codexRuntime.options : {});\n const thread = codex.startThread(\n codexRuntime.enabled\n ? codexRuntime.threadOptions\n : { skipGitRepoCheck: true, networkAccessEnabled: true },\n );\n const result = await thread.run(prompt);\n jsonResponse(res, 200, {\n success: true,\n threadId: thread.id,\n provider: codexRuntime.provider,\n model: deepseekModel,\n result,\n });\n }),\n);\n\napp.use((error, _req, res, _next) => {\n const status = Number(error.statusCode || error.status || 500);\n const safeStatus = status >= 400 && status < 600 ? status : 500;\n logger.error({ error: error.stack || String(error) }, 'request failed');\n jsonResponse(res, safeStatus, {\n success: false,\n message: error.message || 'Internal Server Error',\n details: error.details,\n });\n});\n\nconst port = Number(process.env.PORT || 8788);\napp.listen(port, '0.0.0.0', () => {\n logger.info(\n {\n port,\n dbPath,\n dataDir,\n deepseekModel,\n cbetaApiRoot,\n },\n 'Dacheng AI backend started',\n );\n});\n"}, "55d1aa57788a777f1e5c74cba1e5038cc2cce330": {"t": 1785757993.9850123, "n": 73367, "h": "929f37690ef8e47e", "c": "import crypto from 'node:crypto';\nimport fs from 'node:fs';\nimport path from 'node:path';\nimport process from 'node:process';\nimport { fileURLToPath } from 'node:url';\n\nimport { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';\nimport { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';\nimport Database from 'better-sqlite3';\nimport cors from 'cors';\nimport dotenv from 'dotenv';\nimport express from 'express';\nimport rateLimit from 'express-rate-limit';\nimport helmet from 'helmet';\nimport pino from 'pino';\nimport { z } from 'zod';\n\ndotenv.config();\n\nconst __dirname = path.dirname(fileURLToPath(import.meta.url));\nconst rootDir = path.resolve(__dirname, '..');\nconst dataDir = process.env.DATA_DIR || path.join(rootDir, 'data');\nconst resourcesDir = path.join(dataDir, 'resources');\nconst codexHomeDir = process.env.CODEX_HOME || path.join(dataDir, 'codex-home');\nconst codexTempDir = process.env.CODEX_TMPDIR || path.join(dataDir, 'codex-tmp');\nconst codexRuntimeDir = process.env.XDG_RUNTIME_DIR || path.join(dataDir, 'codex-runtime');\nconst dbPath = process.env.SQLITE_PATH || path.join(dataDir, 'dacheng-ai.sqlite');\n\nfs.mkdirSync(resourcesDir, { recursive: true });\nfs.mkdirSync(codexHomeDir, { recursive: true });\nfs.mkdirSync(codexTempDir, { recursive: true });\nfs.mkdirSync(codexRuntimeDir, { recursive: true });\n\nconst logger = pino({\n level: process.env.LOG_LEVEL || 'info',\n});\n\nconst app = express();\napp.disable('x-powered-by');\napp.set('trust proxy', Number(process.env.TRUST_PROXY_HOPS || 1));\napp.use(helmet());\nconst corsOrigin =\n !process.env.CORS_ORIGIN || process.env.CORS_ORIGIN.trim() === '*'\n ? true\n : process.env.CORS_ORIGIN.split(',').map((item) => item.trim()).filter(Boolean);\napp.use(cors({ origin: corsOrigin }));\napp.use(express.json({ limit: '1mb' }));\napp.use(\n rateLimit({\n windowMs: 60_000,\n limit: Number(process.env.RATE_LIMIT_PER_MINUTE || 90),\n standardHeaders: true,\n legacyHeaders: false,\n }),\n);\n\nconst db = new Database(dbPath);\ndb.pragma('journal_mode = WAL');\ndb.pragma('foreign_keys = ON');\n\ndb.exec(`\n CREATE TABLE IF NOT EXISTS conversations (\n id TEXT PRIMARY KEY,\n user_id TEXT NOT NULL,\n title TEXT NOT NULL,\n provider TEXT NOT NULL,\n model TEXT NOT NULL,\n created_at TEXT NOT NULL,\n updated_at TEXT NOT NULL\n );\n\n CREATE INDEX IF NOT EXISTS idx_conversations_user_updated\n ON conversations (user_id, updated_at DESC);\n\n CREATE TABLE IF NOT EXISTS messages (\n id TEXT PRIMARY KEY,\n conversation_id TEXT NOT NULL,\n role TEXT NOT NULL,\n content TEXT NOT NULL,\n prompt_tokens INTEGER NOT NULL DEFAULT 0,\n completion_tokens INTEGER NOT NULL DEFAULT 0,\n total_tokens INTEGER NOT NULL DEFAULT 0,\n created_at TEXT NOT NULL,\n FOREIGN KEY (conversation_id) REFERENCES conversations(id) ON DELETE CASCADE\n );\n\n CREATE INDEX IF NOT EXISTS idx_messages_conversation_created\n ON messages (conversation_id, created_at ASC);\n\n CREATE TABLE IF NOT EXISTS usage_months (\n user_id TEXT NOT NULL,\n month TEXT NOT NULL,\n total_tokens INTEGER NOT NULL DEFAULT 0,\n updated_at TEXT NOT NULL,\n PRIMARY KEY (user_id, month)\n );\n\n CREATE TABLE IF NOT EXISTS resource_downloads (\n id TEXT PRIMARY KEY,\n user_id TEXT NOT NULL,\n title TEXT NOT NULL,\n source_name TEXT NOT NULL,\n source_url TEXT NOT NULL,\n file_name TEXT NOT NULL,\n file_path TEXT NOT NULL,\n created_at TEXT NOT NULL\n );\n`);\n\nconst statements = {\n insertConversation: db.prepare(`\n INSERT INTO conversations (id, user_id, title, provider, model, created_at, updated_at)\n VALUES (@id, @userId, @title, @provider, @model, @createdAt, @updatedAt)\n `),\n updateConversation: db.prepare(`\n UPDATE conversations\n SET title = @title, updated_at = @updatedAt\n WHERE id = @id AND user_id = @userId\n `),\n getConversation: db.prepare(`\n SELECT * FROM conversations WHERE id = ? AND user_id = ? LIMIT 1\n `),\n listConversations: db.prepare(`\n SELECT id, title, updated_at AS updatedAt\n FROM conversations\n WHERE user_id = ?\n ORDER BY updated_at DESC\n LIMIT 30\n `),\n insertMessage: db.prepare(`\n INSERT INTO messages (\n id, conversation_id, role, content,\n prompt_tokens, completion_tokens, total_tokens, created_at\n )\n VALUES (\n @id, @conversationId, @role, @content,\n @promptTokens, @completionTokens, @totalTokens, @createdAt\n )\n `),\n listMessages: db.prepare(`\n SELECT role, content, prompt_tokens AS promptTokens,\n completion_tokens AS completionTokens, total_tokens AS totalTokens,\n created_at AS createdAt\n FROM messages\n WHERE conversation_id = ?\n ORDER BY created_at ASC\n `),\n getUsage: db.prepare(`\n SELECT total_tokens AS totalTokens FROM usage_months WHERE user_id = ? AND month = ?\n `),\n upsertUsage: db.prepare(`\n INSERT INTO usage_months (user_id, month, total_tokens, updated_at)\n VALUES (@userId, @month, @totalTokens, @updatedAt)\n ON CONFLICT(user_id, month)\n DO UPDATE SET total_tokens = usage_months.total_tokens + excluded.total_tokens,\n updated_at = excluded.updated_at\n `),\n insertResourceDownload: db.prepare(`\n INSERT INTO resource_downloads (\n id, user_id, title, source_name, source_url, file_name, file_path, created_at\n )\n VALUES (\n @id, @userId, @title, @sourceName, @sourceUrl, @fileName, @filePath, @createdAt\n )\n `),\n};\n\nconst deepseekApiKey = process.env.DEEPSEEK_API_KEY || '';\nconst deepseekBaseUrl = (process.env.DEEPSEEK_BASE_URL || 'https://api.deepseek.com').replace(/\\/+$/, '');\nconst deepseekModel = process.env.DEEPSEEK_MODEL || 'deepseek-chat';\nconst cbetaApiRoot = (process.env.CBETA_API_ROOT || 'https://144.24.17.21.sslip.io').replace(/\\/+$/, '');\nconst fabushiApiBaseUrl = (process.env.FABUSHI_API_BASE_URL || 'http://141.148.140.39').replace(/\\/+$/, '');\nconst memberMonthlyLimit = Number(process.env.MEMBER_MONTHLY_TOKEN_LIMIT || 1_000_000);\nconst freeMonthlyLimit = Number(process.env.FREE_MONTHLY_TOKEN_LIMIT || 50_000);\nconst maxResourceTextChars = Number(process.env.MAX_RESOURCE_TEXT_CHARS || 80_000);\nconst enableLibreChatAgentChat = process.env.ENABLE_LIBRECHAT_AGENT_CHAT === 'true';\nconst libreChatAgentsBaseUrl = (\n process.env.LIBRECHAT_AGENTS_BASE_URL || 'http://127.0.0.1:3080/api/agents/v1'\n).replace(/\\/+$/, '');\nconst libreChatAgentApiKey = process.env.LIBRECHAT_AGENT_API_KEY || '';\nconst libreChatAgentId = process.env.LIBRECHAT_AGENT_ID || '';\nconst enableCodexSdkChat = process.env.ENABLE_CODEX_SDK_CHAT === 'true';\nconst codexDeepSeekProviderId = 'deepseek-chat-completions';\nconst codexResponsesBaseUrl = (\n process.env.CODEX_DEEPSEEK_RESPONSES_BASE_URL ||\n `http://127.0.0.1:${process.env.PORT || 8788}/codex-deepseek/v1`\n).replace(/\\/+$/, '');\n\nconst systemPrompt = [\n '你是“大乘”App 的 AI 助手。',\n '你的核心任务是帮助用户查找、整理、理解并全球法布施合法可分享的佛法资源。',\n '回答要庄重、简洁、可执行;涉及经典、仪轨或资源时,提醒用户尊重版权、来源和当地法规。',\n '如果用户想找资源、下载经文、寻找音频或准备可分享资料,你必须先在后端自动执行搜索、验证和下载步骤;不要要求用户去前端 + 菜单手动查找。',\n '你可以把执行进度总结给用户,但不要输出隐藏推理原文。',\n].join('\\n');\n\nfunction nowIso() {\n return new Date().toISOString();\n}\n\nfunction monthKey(date = new Date()) {\n return `${date.getUTCFullYear()}-${String(date.getUTCMonth() + 1).padStart(2, '0')}`;\n}\n\nfunction jsonResponse(res, status, payload) {\n res.status(status).json(payload);\n}\n\nfunction textToolResult(text, structuredContent = {}) {\n return {\n structuredContent,\n content: [{ type: 'text', text }],\n };\n}\n\nfunction sseHeaders(res) {\n res.status(200);\n res.setHeader('Content-Type', 'text/event-stream; charset=utf-8');\n res.setHeader('Cache-Control', 'no-cache, no-transform');\n res.setHeader('Connection', 'keep-alive');\n res.setHeader('X-Accel-Buffering', 'no');\n if (typeof res.flushHeaders === 'function') res.flushHeaders();\n}\n\nfunction writeSse(res, event, payload) {\n res.write(`event: ${event}\\n`);\n res.write(`data: ${JSON.stringify(payload)}\\n\\n`);\n}\n\nfunction asyncHandler(fn) {\n return (req, res, next) => {\n Promise.resolve(fn(req, res, next)).catch(next);\n };\n}\n\nfunction bearerToken(req) {\n const header = req.get('Authorization') || '';\n if (!header.startsWith('Bearer ')) return '';\n return header.slice('Bearer '.length).trim();\n}\n\nfunction sha256(value) {\n return crypto.createHash('sha256').update(value).digest('hex');\n}\n\nfunction safeUserText(value, fallback = '') {\n if (typeof value !== 'string') return fallback;\n return value.replace(/\\u0000/g, '').trim();\n}\n\nfunction estimateTokens(text) {\n const normalized = String(text || '').trim();\n if (!normalized) return 0;\n return Math.max(1, Math.ceil(normalized.length / 3));\n}\n\nfunction normalizeMembershipPayload(payload) {\n if (!payload || typeof payload !== 'object') return false;\n const data = payload.data && typeof payload.data === 'object' ? payload.data : payload;\n return Boolean(\n data.isActive ||\n data.active ||\n data.hasPremiumAccess ||\n data.hasPremiumMembership ||\n data.isPremium ||\n data.status === 'active' ||\n data.type === 'paid' ||\n data.membershipType === 'paid',\n );\n}\n\nasync function checkRemoteMembership(token) {\n if (!token || !fabushiApiBaseUrl) return false;\n const endpoints = [\n '/api/stripe/membership-status',\n '/api/alipay/check-membership',\n '/api/auth/user-info',\n ];\n\n for (const endpoint of endpoints) {\n try {\n const response = await fetch(`${fabushiApiBaseUrl}${endpoint}`, {\n headers: {\n Accept: 'application/json',\n Authorization: `Bearer ${token}`,\n },\n signal: AbortSignal.timeout(8_000),\n });\n if (!response.ok) continue;\n const payload = await response.json();\n if (normalizeMembershipPayload(payload)) return true;\n } catch (error) {\n logger.debug({ endpoint, error: String(error) }, 'membership lookup skipped');\n }\n }\n\n return false;\n}\n\nasync function resolveUser(req, body = {}) {\n const token = bearerToken(req);\n const username = safeUserText(body.username || req.query.username);\n const tokenHash = token ? sha256(token).slice(0, 24) : '';\n const anonymousHash = sha256(`${req.ip}|${req.get('User-Agent') || ''}`).slice(0, 24);\n const userId = username\n ? `user:${username}`\n : tokenHash\n ? `token:${tokenHash}`\n : `anon:${anonymousHash}`;\n const remoteMember = await checkRemoteMembership(token);\n const memberHint = body.clientMembershipHint === true;\n return {\n userId,\n username,\n tokenHash,\n isMember: remoteMember || memberHint,\n };\n}\n\nfunction usageFor(userId) {\n const month = monthKey();\n const row = statements.getUsage.get(userId, month);\n return {\n month,\n used: Number(row?.totalTokens || 0),\n };\n}\n\nfunction enforceTokenBudget(user, estimatedTokensForRequest) {\n const limit = user.isMember ? memberMonthlyLimit : freeMonthlyLimit;\n const usage = usageFor(user.userId);\n if (usage.used + estimatedTokensForRequest > limit) {\n const error = new Error('本月 AI token 额度已不足');\n error.statusCode = 429;\n error.details = {\n monthlyLimit: limit,\n usedTokens: usage.used,\n remainingTokens: Math.max(0, limit - usage.used),\n };\n throw error;\n }\n return { limit, usage };\n}\n\nfunction recordUsage(userId, totalTokens) {\n if (!totalTokens || totalTokens < 1) return;\n statements.upsertUsage.run({\n userId,\n month: monthKey(),\n totalTokens,\n updatedAt: nowIso(),\n });\n}\n\nfunction titleFromMessage(message) {\n const compact = message.replace(/\\s+/g, ' ').trim();\n if (!compact) return '新对话';\n return compact.length > 22 ? `${compact.slice(0, 22)}...` : compact;\n}\n\nfunction normalizeMessages(rows) {\n return rows.map((row) => ({\n role: row.role,\n content: row.content,\n }));\n}\n\nasync function callDeepSeek(messages) {\n if (!deepseekApiKey) {\n const error = new Error('DeepSeek API key is not configured');\n error.statusCode = 503;\n throw error;\n }\n\n const response = await fetch(`${deepseekBaseUrl}/chat/completions`, {\n method: 'POST',\n headers: {\n Accept: 'application/json',\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${deepseekApiKey}`,\n },\n body: JSON.stringify({\n model: deepseekModel,\n messages,\n temperature: 0.4,\n max_tokens: 1400,\n stream: false,\n }),\n signal: AbortSignal.timeout(60_000),\n });\n\n const bodyText = await response.text();\n let payload;\n try {\n payload = bodyText ? JSON.parse(bodyText) : {};\n } catch {\n payload = { error: bodyText };\n }\n\n if (!response.ok) {\n const upstreamMessage = payload?.error?.message || payload?.message || '';\n const friendlyMessage =\n response.status === 402 || /insufficient\\s+balance/i.test(upstreamMessage)\n ? '大乘 AI 服务额度暂不可用,请联系管理员充值 DeepSeek 账户后重试。'\n : upstreamMessage || `DeepSeek request failed: ${response.status}`;\n const error = new Error(friendlyMessage);\n error.statusCode = response.status;\n throw error;\n }\n\n const message = payload?.choices?.[0]?.message?.content?.trim();\n if (!message) {\n const error = new Error('DeepSeek returned an empty response');\n error.statusCode = 502;\n throw error;\n }\n\n return {\n message,\n usage: {\n promptTokens: Number(payload?.usage?.prompt_tokens || 0),\n completionTokens: Number(payload?.usage?.completion_tokens || 0),\n totalTokens: Number(payload?.usage?.total_tokens || 0),\n },\n };\n}\n\nasync function callDeepSeekStream(messages, callbacks = {}) {\n if (!deepseekApiKey) {\n const error = new Error('DeepSeek API key is not configured');\n error.statusCode = 503;\n throw error;\n }\n\n const response = await fetch(`${deepseekBaseUrl}/chat/completions`, {\n method: 'POST',\n headers: {\n Accept: 'text/event-stream',\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${deepseekApiKey}`,\n },\n body: JSON.stringify({\n model: deepseekModel,\n messages,\n temperature: 0.4,\n max_tokens: 1400,\n stream: true,\n stream_options: { include_usage: true },\n }),\n signal: callbacks.signal || AbortSignal.timeout(90_000),\n });\n\n if (!response.ok) {\n const bodyText = await response.text();\n let payload;\n try {\n payload = bodyText ? JSON.parse(bodyText) : {};\n } catch {\n payload = { error: bodyText };\n }\n const upstreamMessage = payload?.error?.message || payload?.message || '';\n const friendlyMessage =\n response.status === 402 || /insufficient\\s+balance/i.test(upstreamMessage)\n ? '大乘 AI 服务额度暂不可用,请联系管理员充值 DeepSeek 账户后重试。'\n : upstreamMessage || `DeepSeek request failed: ${response.status}`;\n const error = new Error(friendlyMessage);\n error.statusCode = response.status;\n throw error;\n }\n\n let buffer = '';\n let message = '';\n let usage = {\n promptTokens: 0,\n completionTokens: 0,\n totalTokens: 0,\n };\n const decoder = new TextDecoder();\n\n for await (const chunk of response.body) {\n buffer += decoder.decode(chunk, { stream: true });\n const lines = buffer.split(/\\r?\\n/);\n buffer = lines.pop() || '';\n\n for (const rawLine of lines) {\n const line = rawLine.trim();\n if (!line.startsWith('data:')) continue;\n const data = line.slice('data:'.length).trim();\n if (!data || data === '[DONE]') continue;\n\n let payload;\n try {\n payload = JSON.parse(data);\n } catch {\n continue;\n }\n\n const delta = payload?.choices?.[0]?.delta?.content || '';\n if (delta) {\n message += delta;\n callbacks.onToken?.(delta);\n }\n\n if (payload?.usage) {\n usage = {\n promptTokens: Number(payload.usage.prompt_tokens || usage.promptTokens || 0),\n completionTokens: Number(payload.usage.completion_tokens || usage.completionTokens || 0),\n totalTokens: Number(payload.usage.total_tokens || usage.totalTokens || 0),\n };\n }\n }\n }\n\n message = message.trim();\n if (!message) {\n const error = new Error('DeepSeek returned an empty response');\n error.statusCode = 502;\n throw error;\n }\n\n return { message, usage };\n}\n\nfunction createCodexDeepSeekRuntime() {\n if (!enableCodexSdkChat || !deepseekApiKey) {\n return {\n enabled: false,\n provider: 'deepseek-direct',\n reason: enableCodexSdkChat ? 'DeepSeek API key is not configured' : 'ENABLE_CODEX_SDK_CHAT is not true',\n };\n }\n\n return {\n enabled: true,\n provider: 'codex-sdk-deepseek',\n options: {\n apiKey: deepseekApiKey,\n baseUrl: deepseekBaseUrl,\n env: {\n ...process.env,\n DEEPSEEK_API_KEY: deepseekApiKey,\n HOME: codexHomeDir,\n CODEX_HOME: codexHomeDir,\n TMPDIR: codexTempDir,\n XDG_RUNTIME_DIR: codexRuntimeDir,\n XDG_CACHE_HOME: path.join(codexHomeDir, '.cache'),\n XDG_CONFIG_HOME: path.join(codexHomeDir, '.config'),\n XDG_DATA_HOME: path.join(codexHomeDir, '.local', 'share'),\n PATH: process.env.PATH || '/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin',\n },\n config: {\n model_provider: codexDeepSeekProviderId,\n model_providers: {\n [codexDeepSeekProviderId]: {\n name: 'DeepSeek Chat Completions',\n base_url: codexResponsesBaseUrl,\n env_key: 'DEEPSEEK_API_KEY',\n wire_api: 'responses',\n query_params: {},\n },\n },\n },\n },\n threadOptions: {\n model: deepseekModel,\n approvalPolicy: 'never',\n sandboxMode: 'read-only',\n skipGitRepoCheck: true,\n networkAccessEnabled: true,\n },\n };\n}\n\nfunction codexPromptFromMessages(messages) {\n return messages\n .map((message) => {\n const role = String(message.role || 'user').toUpperCase();\n return `${role}:\\n${message.content}`;\n })\n .join('\\n\\n');\n}\n\nfunction responseContentText(content) {\n if (typeof content === 'string') return content;\n if (!Array.isArray(content)) return '';\n return content\n .map((part) => {\n if (typeof part === 'string') return part;\n if (!part || typeof part !== 'object') return '';\n return String(part.text || part.input_text || part.output_text || '');\n })\n .filter(Boolean)\n .join('\\n');\n}\n\nfunction codexResponsesPrompt(body) {\n const input = Array.isArray(body?.input) ? body.input : [];\n const userItems = input.filter((item) => item?.role === 'user');\n const source = userItems[userItems.length - 1] || input[input.length - 1];\n const prompt = responseContentText(source?.content).trim();\n if (prompt) return prompt;\n if (typeof body?.input === 'string') return body.input.trim();\n return '';\n}\n\nfunction writeResponsesEvent(res, event, data) {\n res.write(`event: ${event}\\n`);\n res.write(`data: ${JSON.stringify(data)}\\n\\n`);\n}\n\nfunction responseUsage(usage = {}) {\n return {\n input_tokens: Number(usage.promptTokens || usage.input_tokens || 0),\n cached_input_tokens: 0,\n output_tokens: Number(usage.completionTokens || usage.output_tokens || 0),\n reasoning_output_tokens: 0,\n total_tokens: Number(usage.totalTokens || usage.total_tokens || 0),\n };\n}\n\nfunction createLibreChatAgentRuntime() {\n if (!enableLibreChatAgentChat || !libreChatAgentApiKey || !libreChatAgentId) {\n return {\n enabled: false,\n provider: 'librechat-agent',\n reason: !enableLibreChatAgentChat\n ? 'ENABLE_LIBRECHAT_AGENT_CHAT is not true'\n : !libreChatAgentApiKey\n ? 'LIBRECHAT_AGENT_API_KEY is not configured'\n : 'LIBRECHAT_AGENT_ID is not configured',\n };\n }\n\n return {\n enabled: true,\n provider: 'librechat-agent',\n model: libreChatAgentId,\n chatCompletionsUrl: `${libreChatAgentsBaseUrl}/chat/completions`,\n };\n}\n\nfunction libreChatUsage(usage = {}) {\n return {\n promptTokens: Number(usage.prompt_tokens || usage.input_tokens || usage.promptTokens || 0),\n completionTokens: Number(usage.completion_tokens || usage.output_tokens || usage.completionTokens || 0),\n totalTokens: Number(usage.total_tokens || usage.totalTokens || 0),\n };\n}\n\nasync function callLibreChatAgent(messages) {\n const runtime = createLibreChatAgentRuntime();\n if (!runtime.enabled) {\n const error = new Error(runtime.reason || 'LibreChat Agent chat is not enabled');\n error.statusCode = 503;\n throw error;\n }\n\n const response = await fetch(runtime.chatCompletionsUrl, {\n method: 'POST',\n headers: {\n Accept: 'application/json',\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${libreChatAgentApiKey}`,\n },\n body: JSON.stringify({\n model: runtime.model,\n messages,\n temperature: 0.4,\n stream: false,\n }),\n signal: AbortSignal.timeout(120_000),\n });\n\n const bodyText = await response.text();\n let payload;\n try {\n payload = bodyText ? JSON.parse(bodyText) : {};\n } catch {\n payload = { error: bodyText };\n }\n\n if (!response.ok) {\n const upstreamMessage = payload?.error?.message || payload?.message || bodyText || '';\n const error = new Error(upstreamMessage || `LibreChat Agent request failed: ${response.status}`);\n error.statusCode = response.status;\n throw error;\n }\n\n const message = payload?.choices?.[0]?.message?.content?.trim();\n if (!message) {\n const error = new Error('LibreChat Agent returned an empty response');\n error.statusCode = 502;\n throw error;\n }\n\n return {\n message,\n usage: libreChatUsage(payload?.usage),\n };\n}\n\nasync function callLibreChatAgentStream(messages, callbacks = {}) {\n const runtime = createLibreChatAgentRuntime();\n if (!runtime.enabled) {\n const error = new Error(runtime.reason || 'LibreChat Agent chat is not enabled');\n error.statusCode = 503;\n throw error;\n }\n\n const response = await fetch(runtime.chatCompletionsUrl, {\n method: 'POST',\n headers: {\n Accept: 'text/event-stream',\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${libreChatAgentApiKey}`,\n },\n body: JSON.stringify({\n model: runtime.model,\n messages,\n temperature: 0.4,\n stream: true,\n }),\n signal: callbacks.signal || AbortSignal.timeout(180_000),\n });\n\n if (!response.ok) {\n const bodyText = await response.text();\n let payload;\n try {\n payload = bodyText ? JSON.parse(bodyText) : {};\n } catch {\n payload = { error: bodyText };\n }\n const upstreamMessage = payload?.error?.message || payload?.message || bodyText || '';\n const error = new Error(upstreamMessage || `LibreChat Agent request failed: ${response.status}`);\n error.statusCode = response.status;\n throw error;\n }\n\n let buffer = '';\n let message = '';\n let usage = { promptTokens: 0, completionTokens: 0, totalTokens: 0 };\n const decoder = new TextDecoder();\n\n for await (const chunk of response.body) {\n buffer += decoder.decode(chunk, { stream: true });\n const lines = buffer.split(/\\r?\\n/);\n buffer = lines.pop() || '';\n\n for (const rawLine of lines) {\n const line = rawLine.trim();\n if (!line.startsWith('data:')) continue;\n const data = line.slice('data:'.length).trim();\n if (!data || data === '[DONE]') continue;\n\n let payload;\n try {\n payload = JSON.parse(data);\n } catch {\n continue;\n }\n\n const delta = payload?.choices?.[0]?.delta?.content || '';\n if (delta) {\n message += delta;\n callbacks.onToken?.(delta);\n }\n\n if (payload?.usage) {\n usage = libreChatUsage(payload.usage);\n }\n }\n }\n\n message = message.trim();\n if (!message) {\n const error = new Error('LibreChat Agent returned an empty response');\n error.statusCode = 502;\n throw error;\n }\n\n return { message, usage };\n}\n\nfunction isDeepSeekQuotaError(error) {\n const message = String(error?.message || error || '');\n return /额度|insufficient\\s+balance|balance/i.test(message);\n}\n\nfunction publicAiErrorMessage(error) {\n if (isDeepSeekQuotaError(error)) {\n return '大乘 AI 服务额度暂不可用,请联系管理员充值 DeepSeek 账户后重试。';\n }\n return error?.message || 'Internal Server Error';\n}\n\nfunction responsesPayload({\n responseId,\n itemId,\n status,\n model,\n text = '',\n usage = null,\n error = null,\n}) {\n return {\n id: responseId,\n object: 'response',\n status,\n model,\n output:\n status === 'completed'\n ? [\n {\n id: itemId,\n type: 'message',\n role: 'assistant',\n status: 'completed',\n content: [{ type: 'output_text', text }],\n },\n ]\n : [],\n ...(usage ? { usage: responseUsage(usage) } : {}),\n ...(error\n ? {\n error: {\n message: error.message || String(error),\n type: 'server_error',\n },\n }\n : {}),\n };\n}\n\nasync function callCodexSdkDeepSeek(messages, callbacks = {}) {\n const codexRuntime = createCodexDeepSeekRuntime();\n if (!codexRuntime.enabled) {\n const error = new Error(codexRuntime.reason || 'Codex SDK chat is not enabled');\n error.statusCode = 503;\n throw error;\n }\n\n const { Codex } = await import('@openai/codex-sdk');\n const codex = new Codex(codexRuntime.options);\n const thread = codex.startThread(codexRuntime.threadOptions);\n const prompt = codexPromptFromMessages(messages);\n\n if (callbacks.onToken || callbacks.onStep) {\n const { events } = await thread.runStreamed(prompt, { signal: callbacks.signal });\n let finalResponse = '';\n let usage = null;\n\n for await (const event of events) {\n if (event.type === 'item.completed') {\n const item = event.item;\n if (item.type === 'agent_message') {\n finalResponse = item.text || finalResponse;\n callbacks.onToken?.(item.text || '');\n } else if (item.type === 'todo_list') {\n callbacks.onStep?.({\n title: 'Codex SDK 执行计划',\n message: item.items.map((todo) => `${todo.completed ? '✓' : '•'} ${todo.text}`).join('\\n'),\n });\n } else if (item.type === 'command_execution') {\n callbacks.onStep?.({\n title: 'Codex SDK 执行命令',\n message: item.command,\n });\n } else if (item.type === 'mcp_tool_call') {\n callbacks.onStep?.({\n title: 'Codex SDK 调用工具',\n message: `${item.server}.${item.tool}`,\n });\n }\n } else if (event.type === 'turn.completed') {\n usage = event.usage;\n } else if (event.type === 'turn.failed') {\n throw new Error(event.error?.message || 'Codex SDK turn failed');\n } else if (event.type === 'error') {\n throw new Error(event.message || 'Codex SDK stream failed');\n }\n }\n\n if (!finalResponse.trim()) {\n const error = new Error('Codex SDK returned an empty response');\n error.statusCode = 502;\n throw error;\n }\n\n return {\n message: finalResponse.trim(),\n usage: {\n promptTokens: Number(usage?.input_tokens || 0),\n completionTokens: Number(usage?.output_tokens || 0),\n totalTokens: Number((usage?.input_tokens || 0) + (usage?.output_tokens || 0)),\n },\n };\n }\n\n const turn = await thread.run(prompt);\n if (!turn.finalResponse?.trim()) {\n const error = new Error('Codex SDK returned an empty response');\n error.statusCode = 502;\n throw error;\n }\n\n return {\n message: turn.finalResponse.trim(),\n usage: {\n promptTokens: Number(turn.usage?.input_tokens || 0),\n completionTokens: Number(turn.usage?.output_tokens || 0),\n totalTokens: Number((turn.usage?.input_tokens || 0) + (turn.usage?.output_tokens || 0)),\n },\n };\n}\n\napp.post(\n '/codex-deepseek/v1/responses',\n asyncHandler(async (req, res) => {\n const bearer = bearerToken(req);\n if (!deepseekApiKey || bearer !== deepseekApiKey) {\n res.status(401).json({\n error: { message: 'Unauthorized Codex DeepSeek adapter request' },\n });\n return;\n }\n\n const prompt = codexResponsesPrompt(req.body);\n if (!prompt) {\n res.status(400).json({ error: { message: 'Responses input is required' } });\n return;\n }\n\n const model = String(req.body?.model || deepseekModel);\n const responseId = `resp_${crypto.randomUUID().replaceAll('-', '')}`;\n const itemId = `msg_${crypto.randomUUID().replaceAll('-', '')}`;\n const wantsStream =\n req.body?.stream !== false ||\n String(req.get('accept') || '').includes('text/event-stream');\n const messages = [{ role: 'user', content: prompt }];\n\n if (!wantsStream) {\n const result = await callDeepSeek(messages);\n res.json(\n responsesPayload({\n responseId,\n itemId,\n status: 'completed',\n model,\n text: result.message,\n usage: result.usage,\n }),\n );\n return;\n }\n\n res.status(200);\n res.set({\n 'Content-Type': 'text/event-stream; charset=utf-8',\n 'Cache-Control': 'no-cache, no-transform',\n Connection: 'keep-alive',\n 'X-Accel-Buffering': 'no',\n });\n res.flushHeaders?.();\n\n writeResponsesEvent(res, 'response.created', {\n type: 'response.created',\n response: responsesPayload({\n responseId,\n itemId,\n status: 'in_progress',\n model,\n }),\n });\n writeResponsesEvent(res, 'response.output_item.added', {\n type: 'response.output_item.added',\n output_index: 0,\n item: {\n id: itemId,\n type: 'message',\n role: 'assistant',\n status: 'in_progress',\n content: [],\n },\n });\n writeResponsesEvent(res, 'response.content_part.added', {\n type: 'response.content_part.added',\n item_id: itemId,\n output_index: 0,\n content_index: 0,\n part: { type: 'output_text', text: '' },\n });\n\n let text = '';\n try {\n const result = await callDeepSeekStream(messages, {\n onToken: (delta) => {\n text += delta;\n writeResponsesEvent(res, 'response.output_text.delta', {\n type: 'response.output_text.delta',\n item_id: itemId,\n output_index: 0,\n content_index: 0,\n delta,\n });\n },\n });\n text = result.message || text;\n writeResponsesEvent(res, 'response.output_text.done', {\n type: 'response.output_text.done',\n item_id: itemId,\n output_index: 0,\n content_index: 0,\n text,\n });\n writeResponsesEvent(res, 'response.content_part.done', {\n type: 'response.content_part.done',\n item_id: itemId,\n output_index: 0,\n content_index: 0,\n part: { type: 'output_text', text },\n });\n writeResponsesEvent(res, 'response.output_item.done', {\n type: 'response.output_item.done',\n output_index: 0,\n item: {\n id: itemId,\n type: 'message',\n role: 'assistant',\n status: 'completed',\n content: [{ type: 'output_text', text }],\n },\n });\n writeResponsesEvent(res, 'response.completed', {\n type: 'response.completed',\n response: responsesPayload({\n responseId,\n itemId,\n status: 'completed',\n model,\n text,\n usage: result.usage,\n }),\n });\n res.end();\n } catch (error) {\n writeResponsesEvent(res, 'response.failed', {\n type: 'response.failed',\n response: responsesPayload({\n responseId,\n itemId,\n status: 'failed',\n model,\n text,\n error,\n }),\n });\n res.end();\n }\n }),\n);\n\napp.get('/health', (_req, res) => {\n const codexRuntime = createCodexDeepSeekRuntime();\n const libreChatRuntime = createLibreChatAgentRuntime();\n jsonResponse(res, 200, {\n status: 'ok',\n service: 'dacheng-ai-backend',\n provider: libreChatRuntime.enabled ? libreChatRuntime.provider : 'deepseek',\n model: libreChatRuntime.enabled ? libreChatRuntime.model : deepseekModel,\n libreChatAgentChatEnabled: libreChatRuntime.enabled,\n libreChatAgentProvider: libreChatRuntime.provider,\n libreChatAgentReason: libreChatRuntime.enabled ? undefined : libreChatRuntime.reason,\n codexSdkAvailable: true,\n codexSdkChatEnabled: codexRuntime.enabled,\n codexSdkProvider: codexRuntime.provider,\n cbetaApiRoot,\n timestamp: nowIso(),\n });\n});\n\napp.post(\n '/api/ai/chat',\n asyncHandler(async (req, res) => {\n const message = safeUserText(req.body?.message);\n if (!message) {\n return jsonResponse(res, 400, { success: false, message: 'message is required' });\n }\n\n const user = await resolveUser(req, req.body || {});\n const estimated = estimateTokens(message) + 600;\n const budget = enforceTokenBudget(user, estimated);\n const createdAt = nowIso();\n const conversationId = safeUserText(req.body?.conversationId) || crypto.randomUUID();\n let conversation = statements.getConversation.get(conversationId, user.userId);\n const isNewConversation = !conversation;\n\n if (isNewConversation) {\n conversation = {\n id: conversationId,\n user_id: user.userId,\n title: titleFromMessage(message),\n };\n statements.insertConversation.run({\n id: conversationId,\n userId: user.userId,\n title: conversation.title,\n provider: 'deepseek',\n model: deepseekModel,\n createdAt,\n updatedAt: createdAt,\n });\n }\n\n statements.insertMessage.run({\n id: crypto.randomUUID(),\n conversationId,\n role: 'user',\n content: message,\n promptTokens: 0,\n completionTokens: 0,\n totalTokens: 0,\n createdAt,\n });\n\n const libreChatRuntime = createLibreChatAgentRuntime();\n const skillResult = await runResourceFinderSkill({ message, user });\n const skillContext = resourceContextMessage(skillResult);\n const historyRows = statements.listMessages.all(conversationId).slice(-14);\n const modelMessages = [\n { role: 'system', content: systemPrompt },\n ...(skillContext ? [{ role: 'system', content: skillContext }] : []),\n ...normalizeMessages(historyRows),\n ];\n\n const codexRuntime = createCodexDeepSeekRuntime();\n let provider = libreChatRuntime.enabled ? libreChatRuntime.provider : 'deepseek';\n let responseModel = libreChatRuntime.enabled ? libreChatRuntime.model : deepseekModel;\n let aiResult;\n try {\n if (libreChatRuntime.enabled) {\n aiResult = await callLibreChatAgent(modelMessages);\n } else if (codexRuntime.enabled) {\n provider = codexRuntime.provider;\n aiResult = await callCodexSdkDeepSeek(modelMessages);\n } else {\n aiResult = await callDeepSeek(modelMessages);\n }\n } catch (error) {\n if (libreChatRuntime.enabled) {\n logger.warn({ error: String(error) }, 'LibreChat Agent chat failed, falling back to Codex SDK or DeepSeek direct');\n provider = codexRuntime.enabled ? codexRuntime.provider : 'deepseek';\n responseModel = deepseekModel;\n if (codexRuntime.enabled) {\n try {\n aiResult = await callCodexSdkDeepSeek(modelMessages);\n } catch (codexError) {\n if (isDeepSeekQuotaError(codexError)) throw codexError;\n logger.warn({ error: String(codexError) }, 'Codex SDK chat failed, falling back to DeepSeek direct');\n provider = 'deepseek';\n aiResult = await callDeepSeek(modelMessages);\n }\n } else {\n aiResult = await callDeepSeek(modelMessages);\n }\n } else {\n if (!codexRuntime.enabled || isDeepSeekQuotaError(error)) throw error;\n logger.warn({ error: String(error) }, 'Codex SDK chat failed, falling back to DeepSeek direct');\n provider = 'deepseek';\n aiResult = await callDeepSeek(modelMessages);\n }\n }\n const usage = {\n promptTokens: aiResult.usage.promptTokens || estimateTokens(JSON.stringify(modelMessages)),\n completionTokens: aiResult.usage.completionTokens || estimateTokens(aiResult.message),\n totalTokens:\n aiResult.usage.totalTokens ||\n estimateTokens(JSON.stringify(modelMessages)) + estimateTokens(aiResult.message),\n };\n recordUsage(user.userId, usage.totalTokens);\n\n const answeredAt = nowIso();\n statements.insertMessage.run({\n id: crypto.randomUUID(),\n conversationId,\n role: 'assistant',\n content: aiResult.message,\n promptTokens: usage.promptTokens,\n completionTokens: usage.completionTokens,\n totalTokens: usage.totalTokens,\n createdAt: answeredAt,\n });\n statements.updateConversation.run({\n id: conversationId,\n userId: user.userId,\n title: isNewConversation ? titleFromMessage(message) : conversation.title,\n updatedAt: answeredAt,\n });\n\n const latestUsage = usageFor(user.userId);\n const monthlyLimit = budget.limit;\n jsonResponse(res, 200, {\n success: true,\n conversationId,\n provider,\n model: responseModel,\n message: aiResult.message,\n usage: {\n ...usage,\n monthlyLimit,\n remainingTokens: Math.max(0, monthlyLimit - latestUsage.used),\n },\n });\n }),\n);\n\napp.post('/api/ai/chat/stream', async (req, res) => {\n sseHeaders(res);\n\n try {\n const message = safeUserText(req.body?.message);\n if (!message) {\n writeSse(res, 'error', { message: 'message is required' });\n res.end();\n return;\n }\n\n const user = await resolveUser(req, req.body || {});\n const estimated = estimateTokens(message) + 600;\n const budget = enforceTokenBudget(user, estimated);\n const createdAt = nowIso();\n const conversationId = safeUserText(req.body?.conversationId) || crypto.randomUUID();\n let conversation = statements.getConversation.get(conversationId, user.userId);\n const isNewConversation = !conversation;\n\n if (isNewConversation) {\n conversation = {\n id: conversationId,\n user_id: user.userId,\n title: titleFromMessage(message),\n };\n statements.insertConversation.run({\n id: conversationId,\n userId: user.userId,\n title: conversation.title,\n provider: 'deepseek',\n model: deepseekModel,\n createdAt,\n updatedAt: createdAt,\n });\n }\n\n statements.insertMessage.run({\n id: crypto.randomUUID(),\n conversationId,\n role: 'user',\n content: message,\n promptTokens: 0,\n completionTokens: 0,\n totalTokens: 0,\n createdAt,\n });\n\n const codexRuntime = createCodexDeepSeekRuntime();\n const libreChatRuntime = createLibreChatAgentRuntime();\n let provider = libreChatRuntime.enabled\n ? libreChatRuntime.provider\n : codexRuntime.enabled\n ? codexRuntime.provider\n : 'deepseek';\n let responseModel = libreChatRuntime.enabled ? libreChatRuntime.model : deepseekModel;\n writeSse(res, 'meta', {\n conversationId,\n provider,\n model: responseModel,\n });\n const skillResult = await runResourceFinderSkill({\n message,\n user,\n onStep: (step) => writeSse(res, 'step', step),\n });\n const skillContext = resourceContextMessage(skillResult);\n const historyRows = statements.listMessages.all(conversationId).slice(-14);\n const modelMessages = [\n { role: 'system', content: systemPrompt },\n ...(skillContext ? [{ role: 'system', content: skillContext }] : []),\n ...normalizeMessages(historyRows),\n ];\n\n let aiResult;\n try {\n if (libreChatRuntime.enabled) {\n aiResult = await callLibreChatAgentStream(modelMessages, {\n onToken: (token) => writeSse(res, 'delta', { text: token }),\n });\n } else if (codexRuntime.enabled) {\n aiResult = await callCodexSdkDeepSeek(modelMessages, {\n onToken: (token) => writeSse(res, 'delta', { text: token }),\n onStep: (step) => writeSse(res, 'step', step),\n });\n } else {\n aiResult = await callDeepSeekStream(modelMessages, {\n onToken: (token) => writeSse(res, 'delta', { text: token }),\n });\n }\n } catch (error) {\n if (libreChatRuntime.enabled) {\n logger.warn({ error: String(error) }, 'LibreChat Agent streaming chat failed, falling back to Codex SDK or DeepSeek direct');\n provider = codexRuntime.enabled ? codexRuntime.provider : 'deepseek';\n responseModel = deepseekModel;\n writeSse(res, 'meta', {\n conversationId,\n provider,\n model: responseModel,\n });\n if (codexRuntime.enabled) {\n try {\n aiResult = await callCodexSdkDeepSeek(modelMessages, {\n onToken: (token) => writeSse(res, 'delta', { text: token }),\n onStep: (step) => writeSse(res, 'step', step),\n });\n } catch (codexError) {\n if (isDeepSeekQuotaError(codexError)) throw codexError;\n logger.warn({ error: String(codexError) }, 'Codex SDK streaming chat failed, falling back to DeepSeek direct');\n provider = 'deepseek';\n responseModel = deepseekModel;\n writeSse(res, 'meta', {\n conversationId,\n provider,\n model: responseModel,\n });\n aiResult = await callDeepSeekStream(modelMessages, {\n onToken: (token) => writeSse(res, 'delta', { text: token }),\n });\n }\n } else {\n aiResult = await callDeepSeekStream(modelMessages, {\n onToken: (token) => writeSse(res, 'delta', { text: token }),\n });\n }\n } else {\n if (!codexRuntime.enabled || isDeepSeekQuotaError(error)) throw error;\n logger.warn({ error: String(error) }, 'Codex SDK streaming chat failed, falling back to DeepSeek direct');\n provider = 'deepseek';\n responseModel = deepseekModel;\n writeSse(res, 'meta', {\n conversationId,\n provider,\n model: responseModel,\n });\n aiResult = await callDeepSeekStream(modelMessages, {\n onToken: (token) => writeSse(res, 'delta', { text: token }),\n });\n }\n }\n\n const usage = {\n promptTokens: aiResult.usage.promptTokens || estimateTokens(JSON.stringify(modelMessages)),\n completionTokens: aiResult.usage.completionTokens || estimateTokens(aiResult.message),\n totalTokens:\n aiResult.usage.totalTokens ||\n estimateTokens(JSON.stringify(modelMessages)) + estimateTokens(aiResult.message),\n };\n recordUsage(user.userId, usage.totalTokens);\n\n const answeredAt = nowIso();\n statements.insertMessage.run({\n id: crypto.randomUUID(),\n conversationId,\n role: 'assistant',\n content: aiResult.message,\n promptTokens: usage.promptTokens,\n completionTokens: usage.completionTokens,\n totalTokens: usage.totalTokens,\n createdAt: answeredAt,\n });\n statements.updateConversation.run({\n id: conversationId,\n userId: user.userId,\n title: isNewConversation ? titleFromMessage(message) : conversation.title,\n updatedAt: answeredAt,\n });\n\n const latestUsage = usageFor(user.userId);\n const monthlyLimit = budget.limit;\n writeSse(res, 'done', {\n conversationId,\n provider,\n model: responseModel,\n message: aiResult.message,\n usage: {\n ...usage,\n monthlyLimit,\n remainingTokens: Math.max(0, monthlyLimit - latestUsage.used),\n },\n });\n res.end();\n } catch (error) {\n logger.error({ error: error.stack || String(error) }, 'streaming chat failed');\n writeSse(res, 'error', {\n message: publicAiErrorMessage(error),\n details: error.details,\n });\n res.end();\n }\n});\n\napp.get(\n '/api/ai/conversations',\n asyncHandler(async (req, res) => {\n const user = await resolveUser(req, { username: req.query.username });\n const items = statements.listConversations.all(user.userId).map((item) => ({\n id: item.id,\n title: item.title,\n updatedAt: item.updatedAt,\n }));\n jsonResponse(res, 200, { success: true, items });\n }),\n);\n\napp.get(\n '/api/ai/conversations/:id',\n asyncHandler(async (req, res) => {\n const user = await resolveUser(req);\n const conversation = statements.getConversation.get(req.params.id, user.userId);\n if (!conversation) {\n return jsonResponse(res, 404, { success: false, message: 'conversation not found' });\n }\n const messages = statements.listMessages.all(req.params.id).map((item) => ({\n role: item.role,\n content: item.content,\n createdAt: item.createdAt,\n }));\n jsonResponse(res, 200, {\n success: true,\n conversation: {\n id: conversation.id,\n title: conversation.title,\n updatedAt: conversation.updated_at,\n },\n messages,\n });\n }),\n);\n\nfunction buildUrl(base, endpoint, params = {}) {\n const url = new URL(endpoint, `${base}/`);\n for (const [key, value] of Object.entries(params)) {\n if (Array.isArray(value)) {\n for (const item of value) {\n if (item !== undefined && item !== null && item !== '') {\n url.searchParams.append(key, String(item));\n }\n }\n } else if (value !== undefined && value !== null && value !== '') {\n url.searchParams.set(key, String(value));\n }\n }\n return url;\n}\n\nasync function fetchJson(url) {\n const response = await fetch(url, {\n headers: { Accept: 'application/json' },\n signal: AbortSignal.timeout(15_000),\n });\n if (!response.ok) {\n throw new Error(`Request failed: ${response.status}`);\n }\n return response.json();\n}\n\nfunction stripHtml(value) {\n return String(value || '')\n .replace(/<script[\\s\\S]*?<\\/script>/gi, ' ')\n .replace(/<style[\\s\\S]*?<\\/style>/gi, ' ')\n .replace(/<[^>]+>/g, ' ')\n .replace(/ /g, ' ')\n .replace(/</g, '<')\n .replace(/>/g, '>')\n .replace(/&/g, '&')\n .replace(/&#(\\d+);/g, (_match, code) => String.fromCharCode(Number(code)))\n .replace(/\\s+/g, ' ')\n .trim();\n}\n\nfunction cleanExtractedText(value) {\n let text = String(value || '')\n .replace(/\\b[A-Z]{1,3}\\d{2}n\\d{4,5}[a-z]?_p?\\d{4}[a-z]\\d{2}\\b/g, ' ')\n .replace(/\\uFFFD/g, '')\n .replace(/\\s+([,。;:!?、」』)》】])/g, '$1')\n .replace(/([「『《(【])\\s+/g, '$1');\n for (let i = 0; i < 3; i += 1) {\n text = text.replace(/([\\u3400-\\u9fff])\\s+([\\u3400-\\u9fff])/g, '$1$2');\n }\n return text.replace(/[ \\t]{2,}/g, ' ').trim();\n}\n\nfunction compactText(value, limit = 140) {\n const text = stripHtml(value);\n return text.length > limit ? `${text.slice(0, limit)}...` : text;\n}\n\nfunction collectSearchResults(data) {\n if (!data || typeof data !== 'object') return [];\n if (Array.isArray(data.results)) return data.results;\n if (Array.isArray(data?.results?.docs)) return data.results.docs;\n if (Array.isArray(data?.response?.docs)) return data.response.docs;\n if (Array.isArray(data.docs)) return data.docs;\n return [];\n}\n\nfunction firstText(item, keys) {\n for (const key of keys) {\n const value = item?.[key];\n if (Array.isArray(value)) {\n const first = value.map((entry) => safeUserText(String(entry))).find(Boolean);\n if (first) return first;\n }\n if (typeof value === 'string' && value.trim()) return value.trim();\n if (typeof value === 'number') return String(value);\n }\n return '';\n}\n\nfunction firstNumber(item, keys, fallback = undefined) {\n for (const key of keys) {\n const value = item?.[key];\n if (typeof value === 'number') return value;\n if (typeof value === 'string' && value.trim() && Number.isFinite(Number(value))) {\n return Number(value);\n }\n }\n return fallback;\n}\n\nasync function searchCbetaTitle(query, limit) {\n const url = buildUrl(cbetaApiRoot, 'search/title', { q: query, start: 0, rows: limit });\n const data = await fetchJson(url);\n return collectSearchResults(data).flatMap((item) => {\n const work = firstText(item, ['work', 'work_id', 'workId']);\n const title = firstText(item, ['content', 'title', 'work_title', 'workTitle']);\n if (!work || !title) return [];\n const juan = firstNumber(item, ['juan', 'juan_num', 'juanNum'], 1);\n return [\n {\n id: `cbeta-title-${work}-${juan}`,\n title,\n sourceName: 'CBETA 佛典',\n url: `cbeta:${work}:${juan}`,\n snippet: firstText(item, ['byline', 'creators', 'time_dynasty']) || '可下载经文正文并用于全球法布施。',\n resourceType: 'scripture',\n work,\n juan,\n },\n ];\n });\n}\n\nasync function searchCbetaContent(query, limit) {\n const endpoints = ['search/fulltext', 'search/content', 'search/all_in_one'];\n const settled = await Promise.allSettled(\n endpoints.map((endpoint) =>\n fetchJson(buildUrl(cbetaApiRoot, endpoint, { q: query, start: 0, rows: limit, field: 'content' })),\n ),\n );\n const results = [];\n for (const response of settled) {\n if (response.status !== 'fulfilled') continue;\n for (const item of collectSearchResults(response.value)) {\n const work = firstText(item, ['work', 'work_id', 'workId', 'sutra', 'id']);\n const snippet = compactText(firstText(item, ['kwic', 'highlight', 'snippet', 'content', 'text', 'body', 'p']));\n if (!work || !snippet) continue;\n const juan = firstNumber(item, ['juan', 'juan_num', 'juanNum', '卷'], 1);\n const title = firstText(item, ['title', 'work_title', 'workTitle', 'sutra_name', 'book', 'name']) || work;\n results.push({\n id: `cbeta-content-${work}-${juan}-${sha256(snippet).slice(0, 8)}`,\n title,\n sourceName: 'CBETA 佛典',\n url: `cbeta:${work}:${juan}`,\n snippet,\n resourceType: 'scripture',\n work,\n juan,\n });\n if (results.length >= limit) return results;\n }\n }\n return results;\n}\n\nfunction directUrlResource(query) {\n const url = safeUserText(query);\n let parsed;\n try {\n parsed = new URL(url);\n } catch {\n return null;\n }\n if (!['http:', 'https:'].includes(parsed.protocol)) return null;\n return {\n id: `url-${sha256(url).slice(0, 12)}`,\n title: parsed.hostname,\n sourceName: '网页资源',\n url,\n snippet: '直接下载并提取这个链接的可读正文。',\n resourceType: 'web',\n };\n}\n\nfunction looksLikeAudioQuery(query) {\n return /音乐|音樂|音频|音訊|mp3|audio|念诵|念誦|诵经|誦經|唱诵|唱誦|读诵|讀誦/.test(\n safeUserText(query).toLowerCase(),\n );\n}\n\nfunction archiveSearchTerm(query) {\n return (\n safeUserText(query)\n .replace(/下载|查找|寻找|找一?个|资源|音频|音訊|音乐|音樂|mp3|audio|念诵|念誦|诵经|誦經|唱诵|唱誦|读诵|讀誦/gi, ' ')\n .replace(/\\s+/g, ' ')\n .trim() || safeUserText(query)\n );\n}\n\nfunction solrPhrase(value) {\n return String(value).replace(/\\\\/g, '\\\\\\\\').replace(/\"/g, '\\\\\"');\n}\n\nasync function searchInternetArchiveAudio(query, limit) {\n if (!looksLikeAudioQuery(query)) return [];\n const term = archiveSearchTerm(query);\n if (!term) return [];\n const variants = Array.from(\n new Set([\n term,\n term.replaceAll('金刚经', '金剛經'),\n term.replaceAll('金剛經', '金刚经'),\n ].filter(Boolean)),\n );\n const phraseQuery = variants\n .flatMap((item) => [`title:\"${solrPhrase(item)}\"`, `description:\"${solrPhrase(item)}\"`])\n .join(' OR ');\n const url = buildUrl('https://archive.org', 'advancedsearch.php', {\n q: `(${phraseQuery}) AND mediatype:audio`,\n rows: Math.min(Math.max(limit, 1), 10),\n output: 'json',\n 'fl[]': ['identifier', 'title', 'description'],\n });\n const data = await fetchJson(url);\n const docs = Array.isArray(data?.response?.docs) ? data.response.docs : [];\n return docs\n .map((item) => {\n const identifier = firstText(item, ['identifier']);\n const title = firstText(item, ['title']) || identifier;\n if (!identifier || !title) return null;\n return {\n id: `ia-audio-${identifier}`,\n title,\n sourceName: 'Internet Archive 音频',\n url: `ia:${identifier}`,\n snippet: compactText(firstText(item, ['description'])) || '公开可访问的音频资源,将下载第一个可用 MP3 文件。',\n resourceType: 'audio',\n identifier,\n };\n })\n .filter(Boolean);\n}\n\nconst curatedCbetaWorks = [\n {\n work: 'T0235',\n juan: 1,\n title: '金剛般若波羅蜜經',\n keywords: ['金剛般若波羅蜜經', '金刚般若波罗蜜经', '金剛經', '金刚经'],\n snippet: '姚秦 鳩摩羅什譯。常用于读诵、共修与法布施的般若经典。',\n },\n {\n work: 'T0251',\n juan: 1,\n title: '般若波羅蜜多心經',\n keywords: ['般若波羅蜜多心經', '般若波罗蜜多心经', '心經', '心经'],\n snippet: '唐 玄奘譯。篇幅短,适合加入禅室功课本与日常持诵。',\n },\n {\n work: 'T0262',\n juan: 1,\n title: '妙法蓮華經',\n keywords: ['妙法蓮華經', '妙法莲华经', '法華經', '法华经'],\n snippet: '姚秦 鳩摩羅什譯。可按卷下载正文,用于长期功课与分享。',\n },\n {\n work: 'T0279',\n juan: 1,\n title: '大方廣佛華嚴經',\n keywords: ['大方廣佛華嚴經', '大方广佛华严经', '華嚴經', '华严经'],\n snippet: '唐 實叉難陀譯。可按卷下载正文,用于系统研读与法布施。',\n },\n {\n work: 'T0360',\n juan: 1,\n title: '佛說無量壽經',\n keywords: ['佛說無量壽經', '佛说无量寿经', '無量壽經', '无量寿经'],\n snippet: '净土经典。可下载正文,用于净土功课本与全球法布施。',\n },\n {\n work: 'T0365',\n juan: 1,\n title: '佛說觀無量壽佛經',\n keywords: ['佛說觀無量壽佛經', '佛说观无量寿佛经', '觀無量壽經', '观无量寿经'],\n snippet: '净土三经之一。适合查找下载后加入净土功课。',\n },\n {\n work: 'T0366',\n juan: 1,\n title: '佛說阿彌陀經',\n keywords: ['佛說阿彌陀經', '佛说阿弥陀经', '阿彌陀經', '阿弥陀经'],\n snippet: '姚秦 鳩摩羅什譯。篇幅适中,适合共修、持诵与分享。',\n },\n {\n work: 'T0475',\n juan: 1,\n title: '維摩詰所說經',\n keywords: ['維摩詰所說經', '维摩诘所说经', '維摩詰經', '维摩诘经'],\n snippet: '姚秦 鳩摩羅什譯。大乘经典,可按卷下载用于研读。',\n },\n {\n work: 'T0676',\n juan: 1,\n title: '解深密經',\n keywords: ['解深密經', '解深密经'],\n snippet: '唯识相关大乘经典。适合研读、整理笔记与法布施。',\n },\n {\n work: 'T1666',\n juan: 1,\n title: '大乘起信論',\n keywords: ['大乘起信論', '大乘起信论', '起信論', '起信论'],\n snippet: '大乘论典。适合下载后作为研习功课资料。',\n },\n];\n\nfunction localCbetaResults(query, limit) {\n const normalizedQuery = safeUserText(query).toLowerCase();\n if (!normalizedQuery) return [];\n return curatedCbetaWorks\n .filter((item) =>\n item.keywords.some((keyword) => {\n const normalizedKeyword = keyword.toLowerCase();\n return normalizedKeyword.includes(normalizedQuery) || normalizedQuery.includes(normalizedKeyword);\n }),\n )\n .slice(0, limit)\n .map((item) => ({\n id: `cbeta-curated-${item.work}-${item.juan}`,\n title: item.title,\n sourceName: 'CBETA 佛典',\n url: `cbeta:${item.work}:${item.juan}`,\n snippet: item.snippet,\n resourceType: 'scripture',\n work: item.work,\n juan: item.juan,\n }));\n}\n\nfunction uniqueResources(items, limit) {\n const seen = new Set();\n const results = [];\n for (const item of items) {\n const key = `${item.url}:${item.title}`;\n if (seen.has(key)) continue;\n seen.add(key);\n results.push(item);\n if (results.length >= limit) break;\n }\n return results;\n}\n\nasync function findResourceCandidates(query, limit) {\n const direct = directUrlResource(query);\n const results = [];\n if (direct) results.push(direct);\n try {\n results.push(...(await searchInternetArchiveAudio(query, limit)));\n } catch (error) {\n logger.warn({ error: String(error), query }, 'Internet Archive audio search skipped');\n }\n results.push(...localCbetaResults(query, limit));\n\n const [titleResults, contentResults] = await Promise.allSettled([\n searchCbetaTitle(query, limit),\n searchCbetaContent(query, limit),\n ]);\n if (titleResults.status === 'fulfilled') results.push(...titleResults.value);\n if (contentResults.status === 'fulfilled') results.push(...contentResults.value);\n\n return uniqueResources(results, limit);\n}\n\nfunction looksLikeResourceTask(message) {\n const text = safeUserText(message).toLowerCase();\n if (!text) return false;\n return /下载|查找|寻找|找一?个|资源|經文|经文|佛经|佛經|仪轨|儀軌|音乐|音樂|mp3|audio|念诵|念誦|诵经|誦經/.test(text);\n}\n\nfunction extractResourceQuery(message) {\n const text = safeUserText(message)\n .replace(/请|請|帮我|幫我|麻烦|麻煩|可以|能不能|需要|我要|想要/g, ' ')\n .replace(/下载|下載|查找|寻找|尋找|找一个|找一個|找|资源|資源|并|並|然后|然後|用于|用於|全球法布施|加入功课本|加入功課本/g, ' ')\n .replace(/\\s+/g, ' ')\n .trim();\n return text || safeUserText(message);\n}\n\nfunction resourceContextMessage(skillResult) {\n if (!skillResult) return '';\n const lines = [\n '本轮资源检索/下载已经由大乘 App 后端完成。',\n '不要再次调用 search_dharma_resources、download_dharma_resource 或其他资源 MCP 工具;请直接基于以下结果回答用户。',\n `后端已调用 skill: ${skillResult.skillName}`,\n `执行目标: ${skillResult.query}`,\n ];\n if (skillResult.selected) {\n lines.push(`候选资源: ${skillResult.selected.title}`);\n lines.push(`来源: ${skillResult.selected.sourceName}`);\n lines.push(`资源地址: ${skillResult.selected.url}`);\n }\n if (skillResult.downloaded) {\n lines.push(`下载标题: ${skillResult.downloaded.title}`);\n lines.push(`下载文件: ${skillResult.downloaded.fileName}`);\n const text = safeUserText(skillResult.downloaded.contentText);\n if (text) {\n lines.push('下载正文摘录:');\n lines.push(text.slice(0, 8000));\n }\n }\n return lines.join('\\n');\n}\n\nasync function runResourceFinderSkill({ message, user, onStep }) {\n if (!looksLikeResourceTask(message)) return null;\n\n const skillName = 'resource-finder-downloader';\n const query = extractResourceQuery(message);\n const audioTask = looksLikeAudioQuery(message) || looksLikeAudioQuery(query);\n onStep?.({\n type: 'skill.step',\n skillName,\n stage: 'start',\n title: '调用资源查找下载 skill',\n message: `识别到资源任务: ${query}`,\n });\n\n onStep?.({\n type: 'skill.step',\n skillName,\n stage: 'search',\n title: '搜索可分享资源',\n message: audioTask\n ? '优先检索公开可访问音频资源,失败时再回落到佛典正文资源。'\n : '优先检索 CBETA 佛典正文接口,搜索索引不可用时使用内置经典映射。',\n });\n const candidates = await findResourceCandidates(query, 8);\n if (candidates.length === 0) {\n onStep?.({\n type: 'skill.step',\n skillName,\n stage: 'empty',\n title: '没有找到可下载资源',\n message: '后端没有找到明确可下载且可分享的资源。',\n });\n return {\n skillName,\n query,\n candidates: [],\n selected: null,\n downloaded: null,\n };\n }\n\n const selected = candidates[0];\n onStep?.({\n type: 'skill.step',\n skillName,\n stage: 'select',\n title: '选择可信候选资源',\n message: `${selected.title} / ${selected.sourceName}`,\n });\n\n onStep?.({\n type: 'skill.step',\n skillName,\n stage: 'download',\n title: '下载并提取资源',\n message: selected.url,\n });\n const cbeta = parseCbetaResource(selected);\n const internetArchive = parseInternetArchiveResource(selected);\n const downloaded = cbeta\n ? await downloadCbetaResource({ ...selected, ...cbeta })\n : internetArchive\n ? await downloadInternetArchiveAudio({ ...selected, ...internetArchive })\n : await downloadWebResource(selected);\n const persisted = persistDownloadedResource(user, downloaded);\n\n onStep?.({\n type: 'skill.step',\n skillName,\n stage: 'done',\n title: '资源已准备完成',\n message: downloaded.fileName,\n });\n\n return {\n skillName,\n query,\n candidates,\n selected,\n downloaded: {\n ...downloaded,\n downloadId: persisted.id,\n },\n };\n}\n\napp.post(\n '/api/resources/search',\n asyncHandler(async (req, res) => {\n const query = safeUserText(req.body?.query);\n const limit = Math.min(Math.max(Number(req.body?.limit || 12), 1), 20);\n if (query.length < 2) {\n return jsonResponse(res, 400, { success: false, message: 'query is required' });\n }\n\n const items = await findResourceCandidates(query, limit);\n\n jsonResponse(res, 200, {\n success: true,\n source: 'resource-index',\n items,\n });\n }),\n);\n\nfunction parseCbetaResource(input) {\n const work = safeUserText(input.work);\n const juan = Number(input.juan || 1);\n if (work) return { work, juan: Number.isFinite(juan) ? juan : 1 };\n\n const url = safeUserText(input.url);\n const match = /^cbeta:([^:]+):(\\d+)$/i.exec(url);\n if (!match) return null;\n return {\n work: match[1].toUpperCase(),\n juan: Number(match[2] || 1),\n };\n}\n\nfunction parseInternetArchiveResource(input) {\n const url = safeUserText(input.url);\n const match = /^ia:([^:]+)$/i.exec(url);\n const identifier = match?.[1] || safeUserText(input.identifier);\n if (!identifier) return null;\n return { identifier };\n}\n\nfunction safeFileName(value) {\n const normalized = String(value || 'dharma-resource')\n .replace(/[\\\\/:*?\"<>|]+/g, '-')\n .replace(/\\s+/g, '-')\n .replace(/-+/g, '-')\n .replace(/^-|-$/g, '')\n .slice(0, 80);\n return normalized || 'dharma-resource';\n}\n\nfunction extractCbetaHtml(data) {\n const first = Array.isArray(data?.results) ? data.results[0] : null;\n if (typeof first === 'string') return first;\n if (first && typeof first === 'object' && typeof first.html === 'string') return first.html;\n if (typeof data?.html === 'string') return data.html;\n return '';\n}\n\nasync function downloadCbetaResource(resource) {\n const url = buildUrl(cbetaApiRoot, 'juans', {\n work: resource.work,\n juan: resource.juan,\n work_info: 1,\n toc: 1,\n });\n const data = await fetchJson(url);\n const workInfo = data.work_info || {};\n const title = safeUserText(workInfo.title) || safeUserText(resource.title) || resource.work;\n const html = extractCbetaHtml(data);\n const text = cleanExtractedText(stripHtml(html));\n if (!text) throw new Error('CBETA resource returned empty content');\n return {\n title,\n sourceName: 'CBETA 佛典',\n url: `cbeta:${resource.work}:${resource.juan}`,\n contentText: text.slice(0, maxResourceTextChars),\n fileName: `${resource.work}_${resource.juan}_${safeFileName(title)}.txt`,\n };\n}\n\nasync function downloadInternetArchiveAudio(resource) {\n const identifier = safeUserText(resource.identifier);\n if (!identifier) throw new Error('Internet Archive identifier is required');\n const metadata = await fetchJson(`https://archive.org/metadata/${encodeURIComponent(identifier)}`);\n const title = safeUserText(metadata?.metadata?.title) || safeUserText(resource.title) || identifier;\n const files = Array.isArray(metadata?.files) ? metadata.files : [];\n const audioFile = files.find((file) => {\n const name = safeUserText(file?.name);\n const format = safeUserText(file?.format).toLowerCase();\n return /\\.(mp3|m4a|ogg)$/i.test(name) || format.includes('mp3');\n });\n if (!audioFile?.name) {\n throw new Error('Internet Archive audio item has no downloadable audio file');\n }\n const sourceFileName = audioFile.name;\n const fileUrl = `https://archive.org/download/${encodeURIComponent(identifier)}/${sourceFileName\n .split('/')\n .map((part) => encodeURIComponent(part))\n .join('/')}`;\n const response = await fetch(fileUrl, {\n headers: {\n Accept: 'audio/mpeg, audio/*;q=0.9, */*;q=0.5',\n 'User-Agent': 'DachengResourceDownloader/1.0 (+https://ombhrum.com)',\n },\n signal: AbortSignal.timeout(60_000),\n });\n if (!response.ok) {\n throw new Error(`Internet Archive audio download failed: ${response.status}`);\n }\n const binaryContent = Buffer.from(await response.arrayBuffer());\n if (!binaryContent.length) throw new Error('Internet Archive audio returned empty content');\n const sizeMb = (binaryContent.length / 1024 / 1024).toFixed(1);\n return {\n title,\n sourceName: 'Internet Archive 音频',\n url: fileUrl,\n contentText: `已下载音频资源: ${title}\\n文件名: ${sourceFileName}\\n大小: ${sizeMb} MB\\n来源: ${fileUrl}`,\n binaryContent,\n fileName: safeFileName(sourceFileName),\n };\n}\n\nasync function downloadWebResource(input) {\n const url = safeUserText(input.url);\n const parsed = new URL(url);\n if (!['http:', 'https:'].includes(parsed.protocol)) {\n throw new Error('Only http/https resources can be downloaded');\n }\n\n const response = await fetch(parsed, {\n headers: {\n Accept: 'text/html, text/plain, application/xhtml+xml, application/xml;q=0.8, */*;q=0.5',\n 'User-Agent': 'DachengResourceDownloader/1.0 (+https://ombhrum.com)',\n },\n signal: AbortSignal.timeout(25_000),\n });\n if (!response.ok) throw new Error(`Resource download failed: ${response.status}`);\n const contentType = response.headers.get('content-type') || '';\n const body = await response.text();\n const title = safeUserText(input.title) || parsed.hostname;\n const contentText = contentType.includes('html') ? cleanExtractedText(stripHtml(body)) : body.trim();\n if (!contentText) throw new Error('Resource returned empty content');\n return {\n title,\n sourceName: safeUserText(input.sourceName) || parsed.hostname,\n url,\n contentText: contentText.slice(0, maxResourceTextChars),\n fileName: `${safeFileName(title)}.txt`,\n };\n}\n\nfunction persistDownloadedResource(user, content) {\n const id = crypto.randomUUID();\n const fileName = `${id}-${safeFileName(content.fileName)}`;\n const filePath = path.join(resourcesDir, fileName);\n if (content.binaryContent) {\n fs.writeFileSync(filePath, content.binaryContent);\n } else {\n fs.writeFileSync(filePath, content.contentText, 'utf8');\n }\n statements.insertResourceDownload.run({\n id,\n userId: user.userId,\n title: content.title,\n sourceName: content.sourceName,\n sourceUrl: content.url,\n fileName: content.fileName,\n filePath,\n createdAt: nowIso(),\n });\n return { id, fileName, filePath };\n}\n\nfunction createLibreChatMcpServer() {\n const server = new McpServer({\n name: 'dacheng-ai-tools',\n version: '0.1.0',\n });\n\n server.registerTool(\n 'search_dharma_resources',\n {\n title: 'Search dharma resources',\n description:\n 'Search for legally shareable Buddhist scripture, text, web, or audio resources that can be prepared for Fabushi sharing.',\n inputSchema: {\n query: z.string().min(2).max(200),\n limit: z.number().int().min(1).max(12).optional(),\n },\n outputSchema: {\n query: z.string(),\n items: z.array(\n z.object({\n id: z.string(),\n title: z.string(),\n sourceName: z.string(),\n url: z.string(),\n snippet: z.string(),\n resourceType: z.string(),\n work: z.string().optional(),\n juan: z.number().optional(),\n }),\n ),\n },\n annotations: {\n readOnlyHint: true,\n destructiveHint: false,\n openWorldHint: true,\n },\n },\n async ({ query, limit }) => {\n const items = await findResourceCandidates(query, limit ?? 8);\n return textToolResult(`Found ${items.length} candidate resources for \"${query}\".`, {\n query,\n items,\n });\n },\n );\n\n server.registerTool(\n 'download_dharma_resource',\n {\n title: 'Download dharma resource',\n description:\n 'Download and extract a selected resource. Use the url/work/juan returned by search_dharma_resources whenever possible.',\n inputSchema: {\n url: z.string().min(1).max(1000),\n title: z.string().max(300).optional(),\n sourceName: z.string().max(120).optional(),\n work: z.string().max(40).optional(),\n juan: z.number().int().min(1).optional(),\n identifier: z.string().max(300).optional(),\n },\n outputSchema: {\n title: z.string(),\n sourceName: z.string(),\n url: z.string(),\n fileName: z.string(),\n downloadId: z.string(),\n contentText: z.string(),\n },\n annotations: {\n readOnlyHint: false,\n destructiveHint: false,\n openWorldHint: true,\n },\n },\n async (input) => {\n const user = { userId: 'librechat:mcp' };\n const cbeta = parseCbetaResource(input);\n const internetArchive = parseInternetArchiveResource(input);\n const content = cbeta\n ? await downloadCbetaResource({ ...input, ...cbeta })\n : internetArchive\n ? await downloadInternetArchiveAudio({ ...input, ...internetArchive })\n : await downloadWebResource(input);\n const persisted = persistDownloadedResource(user, content);\n const { binaryContent: _binaryContent, ...publicContent } = content;\n return textToolResult(`Downloaded ${publicContent.title} as ${publicContent.fileName}.`, {\n ...publicContent,\n downloadId: persisted.id,\n });\n },\n );\n\n server.registerTool(\n 'prepare_dharma_share_text',\n {\n title: 'Prepare dharma sharing text',\n description:\n 'Prepare a text payload for the Fabushi app to confirm and add to global dharma sharing. This returns a clientAction; it does not start sending.',\n inputSchema: {\n title: z.string().min(1).max(120),\n text: z.string().min(1).max(20000),\n },\n outputSchema: {\n clientAction: z.object({\n type: z.literal('prepare_dharma_share_text'),\n title: z.string(),\n text: z.string(),\n }),\n },\n annotations: {\n readOnlyHint: false,\n destructiveHint: false,\n openWorldHint: false,\n },\n },\n async ({ title, text }) =>\n textToolResult(`Prepared \"${title}\" for user confirmation in the Fabushi app.`, {\n clientAction: {\n type: 'prepare_dharma_share_text',\n title: safeUserText(title).slice(0, 120),\n text: String(text || '').trim().slice(0, 20000),\n },\n }),\n );\n\n server.registerTool(\n 'prepare_practice_book_item',\n {\n title: 'Prepare practice book item',\n description:\n 'Prepare a scripture, chant, or practice item for the Fabushi app practice book. This returns a clientAction; it does not mutate the app by itself.',\n inputSchema: {\n title: z.string().min(1).max(120),\n sourceName: z.string().max(120).optional(),\n text: z.string().min(1).max(20000),\n },\n outputSchema: {\n clientAction: z.object({\n type: z.literal('prepare_practice_book_item'),\n title: z.string(),\n sourceName: z.string(),\n text: z.string(),\n }),\n },\n annotations: {\n readOnlyHint: false,\n destructiveHint: false,\n openWorldHint: false,\n },\n },\n async ({ title, sourceName, text }) =>\n textToolResult(`Prepared \"${title}\" for the Fabushi practice book confirmation flow.`, {\n clientAction: {\n type: 'prepare_practice_book_item',\n title: safeUserText(title).slice(0, 120),\n sourceName: safeUserText(sourceName || '大乘 AI').slice(0, 120),\n text: String(text || '').trim().slice(0, 20000),\n },\n }),\n );\n\n return server;\n}\n\nasync function handleMcpRequest(req, res) {\n const server = createLibreChatMcpServer();\n const transport = new StreamableHTTPServerTransport({\n sessionIdGenerator: undefined,\n enableJsonResponse: true,\n });\n\n res.on('close', () => {\n transport.close();\n server.close();\n });\n\n await server.connect(transport);\n await transport.handleRequest(req, res, req.body);\n}\n\napp.all('/mcp', async (req, res) => {\n try {\n await handleMcpRequest(req, res);\n } catch (error) {\n logger.error({ error: error.stack || String(error) }, 'MCP request failed');\n if (!res.headersSent) {\n res.status(500).json({\n error: 'MCP request failed',\n message: publicAiErrorMessage(error),\n });\n }\n }\n});\n\napp.post(\n '/api/resources/download',\n asyncHandler(async (req, res) => {\n const user = await resolveUser(req, req.body || {});\n const cbeta = parseCbetaResource(req.body || {});\n const internetArchive = parseInternetArchiveResource(req.body || {});\n const content = cbeta\n ? await downloadCbetaResource({ ...req.body, ...cbeta })\n : internetArchive\n ? await downloadInternetArchiveAudio({ ...req.body, ...internetArchive })\n : await downloadWebResource(req.body || {});\n const persisted = persistDownloadedResource(user, content);\n const { binaryContent: _binaryContent, ...publicContent } = content;\n jsonResponse(res, 200, {\n success: true,\n downloadId: persisted.id,\n ...publicContent,\n });\n }),\n);\n\napp.post(\n '/api/codex/resource-task',\n asyncHandler(async (req, res) => {\n if (process.env.ENABLE_CODEX_SDK !== 'true') {\n return jsonResponse(res, 403, {\n success: false,\n message: 'Codex SDK task runner is disabled. Set ENABLE_CODEX_SDK=true on the server to enable it.',\n });\n }\n const prompt = safeUserText(req.body?.prompt);\n if (!prompt) {\n return jsonResponse(res, 400, { success: false, message: 'prompt is required' });\n }\n const { Codex } = await import('@openai/codex-sdk');\n const codexRuntime = createCodexDeepSeekRuntime();\n const codex = new Codex(codexRuntime.enabled ? codexRuntime.options : {});\n const thread = codex.startThread(\n codexRuntime.enabled\n ? codexRuntime.threadOptions\n : { skipGitRepoCheck: true, networkAccessEnabled: true },\n );\n const result = await thread.run(prompt);\n jsonResponse(res, 200, {\n success: true,\n threadId: thread.id,\n provider: codexRuntime.provider,\n model: deepseekModel,\n result,\n });\n }),\n);\n\napp.use((error, _req, res, _next) => {\n const status = Number(error.statusCode || error.status || 500);\n const safeStatus = status >= 400 && status < 600 ? status : 500;\n logger.error({ error: error.stack || String(error) }, 'request failed');\n jsonResponse(res, safeStatus, {\n success: false,\n message: error.message || 'Internal Server Error',\n details: error.details,\n });\n});\n\nconst port = Number(process.env.PORT || 8788);\napp.listen(port, '0.0.0.0', () => {\n logger.info(\n {\n port,\n dbPath,\n dataDir,\n deepseekModel,\n cbetaApiRoot,\n },\n 'Dacheng AI backend started',\n );\n});\n"}, "ec534cf1c276d6da84968ab8d4eb53f160f9257d": {"t": 1785757996.1953502, "n": 64921, "h": "a6f4ef4edc335524", "c": "import crypto from 'node:crypto';\nimport fs from 'node:fs';\nimport path from 'node:path';\nimport process from 'node:process';\nimport { fileURLToPath } from 'node:url';\n\nimport { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';\nimport { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';\nimport Database from 'better-sqlite3';\nimport cors from 'cors';\nimport dotenv from 'dotenv';\nimport express from 'express';\nimport rateLimit from 'express-rate-limit';\nimport helmet from 'helmet';\nimport pino from 'pino';\nimport { z } from 'zod';\n\ndotenv.config();\n\nconst __dirname = path.dirname(fileURLToPath(import.meta.url));\nconst rootDir = path.resolve(__dirname, '..');\nconst dataDir = process.env.DATA_DIR || path.join(rootDir, 'data');\nconst resourcesDir = path.join(dataDir, 'resources');\nconst codexHomeDir = process.env.CODEX_HOME || path.join(dataDir, 'codex-home');\nconst codexTempDir = process.env.CODEX_TMPDIR || path.join(dataDir, 'codex-tmp');\nconst codexRuntimeDir = process.env.XDG_RUNTIME_DIR || path.join(dataDir, 'codex-runtime');\nconst dbPath = process.env.SQLITE_PATH || path.join(dataDir, 'dacheng-ai.sqlite');\n\nfs.mkdirSync(resourcesDir, { recursive: true });\nfs.mkdirSync(codexHomeDir, { recursive: true });\nfs.mkdirSync(codexTempDir, { recursive: true });\nfs.mkdirSync(codexRuntimeDir, { recursive: true });\n\nconst logger = pino({\n level: process.env.LOG_LEVEL || 'info',\n});\n\nconst app = express();\napp.disable('x-powered-by');\napp.set('trust proxy', Number(process.env.TRUST_PROXY_HOPS || 1));\napp.use(helmet());\nconst corsOrigin =\n !process.env.CORS_ORIGIN || process.env.CORS_ORIGIN.trim() === '*'\n ? true\n : process.env.CORS_ORIGIN.split(',').map((item) => item.trim()).filter(Boolean);\napp.use(cors({ origin: corsOrigin }));\napp.use(express.json({ limit: '1mb' }));\napp.use(\n rateLimit({\n windowMs: 60_000,\n limit: Number(process.env.RATE_LIMIT_PER_MINUTE || 90),\n standardHeaders: true,\n legacyHeaders: false,\n }),\n);\n\nconst db = new Database(dbPath);\ndb.pragma('journal_mode = WAL');\ndb.pragma('foreign_keys = ON');\n\ndb.exec(`\n CREATE TABLE IF NOT EXISTS conversations (\n id TEXT PRIMARY KEY,\n user_id TEXT NOT NULL,\n title TEXT NOT NULL,\n provider TEXT NOT NULL,\n model TEXT NOT NULL,\n created_at TEXT NOT NULL,\n updated_at TEXT NOT NULL\n );\n\n CREATE INDEX IF NOT EXISTS idx_conversations_user_updated\n ON conversations (user_id, updated_at DESC);\n\n CREATE TABLE IF NOT EXISTS messages (\n id TEXT PRIMARY KEY,\n conversation_id TEXT NOT NULL,\n role TEXT NOT NULL,\n content TEXT NOT NULL,\n prompt_tokens INTEGER NOT NULL DEFAULT 0,\n completion_tokens INTEGER NOT NULL DEFAULT 0,\n total_tokens INTEGER NOT NULL DEFAULT 0,\n created_at TEXT NOT NULL,\n FOREIGN KEY (conversation_id) REFERENCES conversations(id) ON DELETE CASCADE\n );\n\n CREATE INDEX IF NOT EXISTS idx_messages_conversation_created\n ON messages (conversation_id, created_at ASC);\n\n CREATE TABLE IF NOT EXISTS usage_months (\n user_id TEXT NOT NULL,\n month TEXT NOT NULL,\n total_tokens INTEGER NOT NULL DEFAULT 0,\n updated_at TEXT NOT NULL,\n PRIMARY KEY (user_id, month)\n );\n\n CREATE TABLE IF NOT EXISTS resource_downloads (\n id TEXT PRIMARY KEY,\n user_id TEXT NOT NULL,\n title TEXT NOT NULL,\n source_name TEXT NOT NULL,\n source_url TEXT NOT NULL,\n file_name TEXT NOT NULL,\n file_path TEXT NOT NULL,\n created_at TEXT NOT NULL\n );\n`);\n\nconst statements = {\n insertConversation: db.prepare(`\n INSERT INTO conversations (id, user_id, title, provider, model, created_at, updated_at)\n VALUES (@id, @userId, @title, @provider, @model, @createdAt, @updatedAt)\n `),\n updateConversation: db.prepare(`\n UPDATE conversations\n SET title = @title, updated_at = @updatedAt\n WHERE id = @id AND user_id = @userId\n `),\n getConversation: db.prepare(`\n SELECT * FROM conversations WHERE id = ? AND user_id = ? LIMIT 1\n `),\n listConversations: db.prepare(`\n SELECT id, title, updated_at AS updatedAt\n FROM conversations\n WHERE user_id = ?\n ORDER BY updated_at DESC\n LIMIT 30\n `),\n insertMessage: db.prepare(`\n INSERT INTO messages (\n id, conversation_id, role, content,\n prompt_tokens, completion_tokens, total_tokens, created_at\n )\n VALUES (\n @id, @conversationId, @role, @content,\n @promptTokens, @completionTokens, @totalTokens, @createdAt\n )\n `),\n listMessages: db.prepare(`\n SELECT role, content, prompt_tokens AS promptTokens,\n completion_tokens AS completionTokens, total_tokens AS totalTokens,\n created_at AS createdAt\n FROM messages\n WHERE conversation_id = ?\n ORDER BY created_at ASC\n `),\n getUsage: db.prepare(`\n SELECT total_tokens AS totalTokens FROM usage_months WHERE user_id = ? AND month = ?\n `),\n upsertUsage: db.prepare(`\n INSERT INTO usage_months (user_id, month, total_tokens, updated_at)\n VALUES (@userId, @month, @totalTokens, @updatedAt)\n ON CONFLICT(user_id, month)\n DO UPDATE SET total_tokens = usage_months.total_tokens + excluded.total_tokens,\n updated_at = excluded.updated_at\n `),\n insertResourceDownload: db.prepare(`\n INSERT INTO resource_downloads (\n id, user_id, title, source_name, source_url, file_name, file_path, created_at\n )\n VALUES (\n @id, @userId, @title, @sourceName, @sourceUrl, @fileName, @filePath, @createdAt\n )\n `),\n};\n\nconst deepseekApiKey = process.env.DEEPSEEK_API_KEY || '';\nconst deepseekBaseUrl = (process.env.DEEPSEEK_BASE_URL || 'https://api.deepseek.com').replace(/\\/+$/, '');\nconst deepseekModel = process.env.DEEPSEEK_MODEL || 'deepseek-chat';\nconst cbetaApiRoot = (process.env.CBETA_API_ROOT || 'https://144.24.17.21.sslip.io').replace(/\\/+$/, '');\nconst fabushiApiBaseUrl = (process.env.FABUSHI_API_BASE_URL || 'http://141.148.140.39').replace(/\\/+$/, '');\nconst memberMonthlyLimit = Number(process.env.MEMBER_MONTHLY_TOKEN_LIMIT || 1_000_000);\nconst freeMonthlyLimit = Number(process.env.FREE_MONTHLY_TOKEN_LIMIT || 50_000);\nconst maxResourceTextChars = Number(process.env.MAX_RESOURCE_TEXT_CHARS || 80_000);\nconst enableCodexSdkChat = process.env.ENABLE_CODEX_SDK_CHAT === 'true';\nconst codexDeepSeekProviderId = 'deepseek-chat-completions';\nconst codexResponsesBaseUrl = (\n process.env.CODEX_DEEPSEEK_RESPONSES_BASE_URL ||\n `http://127.0.0.1:${process.env.PORT || 8788}/codex-deepseek/v1`\n).replace(/\\/+$/, '');\n\nconst systemPrompt = [\n '你是“大乘”App 的 AI 助手。',\n '你的核心任务是帮助用户查找、整理、理解并全球法布施合法可分享的佛法资源。',\n '回答要庄重、简洁、可执行;涉及经典、仪轨或资源时,提醒用户尊重版权、来源和当地法规。',\n '如果用户想找资源、下载经文、寻找音频或准备可分享资料,你必须先在后端自动执行搜索、验证和下载步骤;不要要求用户去前端 + 菜单手动查找。',\n '你可以把执行进度总结给用户,但不要输出隐藏推理原文。',\n].join('\\n');\n\nfunction nowIso() {\n return new Date().toISOString();\n}\n\nfunction monthKey(date = new Date()) {\n return `${date.getUTCFullYear()}-${String(date.getUTCMonth() + 1).padStart(2, '0')}`;\n}\n\nfunction jsonResponse(res, status, payload) {\n res.status(status).json(payload);\n}\n\nfunction textToolResult(text, structuredContent = {}) {\n return {\n structuredContent,\n content: [{ type: 'text', text }],\n };\n}\n\nfunction sseHeaders(res) {\n res.status(200);\n res.setHeader('Content-Type', 'text/event-stream; charset=utf-8');\n res.setHeader('Cache-Control', 'no-cache, no-transform');\n res.setHeader('Connection', 'keep-alive');\n res.setHeader('X-Accel-Buffering', 'no');\n if (typeof res.flushHeaders === 'function') res.flushHeaders();\n}\n\nfunction writeSse(res, event, payload) {\n res.write(`event: ${event}\\n`);\n res.write(`data: ${JSON.stringify(payload)}\\n\\n`);\n}\n\nfunction asyncHandler(fn) {\n return (req, res, next) => {\n Promise.resolve(fn(req, res, next)).catch(next);\n };\n}\n\nfunction bearerToken(req) {\n const header = req.get('Authorization') || '';\n if (!header.startsWith('Bearer ')) return '';\n return header.slice('Bearer '.length).trim();\n}\n\nfunction sha256(value) {\n return crypto.createHash('sha256').update(value).digest('hex');\n}\n\nfunction safeUserText(value, fallback = '') {\n if (typeof value !== 'string') return fallback;\n return value.replace(/\\u0000/g, '').trim();\n}\n\nfunction estimateTokens(text) {\n const normalized = String(text || '').trim();\n if (!normalized) return 0;\n return Math.max(1, Math.ceil(normalized.length / 3));\n}\n\nfunction normalizeMembershipPayload(payload) {\n if (!payload || typeof payload !== 'object') return false;\n const data = payload.data && typeof payload.data === 'object' ? payload.data : payload;\n return Boolean(\n data.isActive ||\n data.active ||\n data.hasPremiumAccess ||\n data.hasPremiumMembership ||\n data.isPremium ||\n data.status === 'active' ||\n data.type === 'paid' ||\n data.membershipType === 'paid',\n );\n}\n\nasync function checkRemoteMembership(token) {\n if (!token || !fabushiApiBaseUrl) return false;\n const endpoints = [\n '/api/stripe/membership-status',\n '/api/alipay/check-membership',\n '/api/auth/user-info',\n ];\n\n for (const endpoint of endpoints) {\n try {\n const response = await fetch(`${fabushiApiBaseUrl}${endpoint}`, {\n headers: {\n Accept: 'application/json',\n Authorization: `Bearer ${token}`,\n },\n signal: AbortSignal.timeout(8_000),\n });\n if (!response.ok) continue;\n const payload = await response.json();\n if (normalizeMembershipPayload(payload)) return true;\n } catch (error) {\n logger.debug({ endpoint, error: String(error) }, 'membership lookup skipped');\n }\n }\n\n return false;\n}\n\nasync function resolveUser(req, body = {}) {\n const token = bearerToken(req);\n const username = safeUserText(body.username || req.query.username);\n const tokenHash = token ? sha256(token).slice(0, 24) : '';\n const anonymousHash = sha256(`${req.ip}|${req.get('User-Agent') || ''}`).slice(0, 24);\n const userId = username\n ? `user:${username}`\n : tokenHash\n ? `token:${tokenHash}`\n : `anon:${anonymousHash}`;\n const remoteMember = await checkRemoteMembership(token);\n const memberHint = body.clientMembershipHint === true;\n return {\n userId,\n username,\n tokenHash,\n isMember: remoteMember || memberHint,\n };\n}\n\nfunction usageFor(userId) {\n const month = monthKey();\n const row = statements.getUsage.get(userId, month);\n return {\n month,\n used: Number(row?.totalTokens || 0),\n };\n}\n\nfunction enforceTokenBudget(user, estimatedTokensForRequest) {\n const limit = user.isMember ? memberMonthlyLimit : freeMonthlyLimit;\n const usage = usageFor(user.userId);\n if (usage.used + estimatedTokensForRequest > limit) {\n const error = new Error('本月 AI token 额度已不足');\n error.statusCode = 429;\n error.details = {\n monthlyLimit: limit,\n usedTokens: usage.used,\n remainingTokens: Math.max(0, limit - usage.used),\n };\n throw error;\n }\n return { limit, usage };\n}\n\nfunction recordUsage(userId, totalTokens) {\n if (!totalTokens || totalTokens < 1) return;\n statements.upsertUsage.run({\n userId,\n month: monthKey(),\n totalTokens,\n updatedAt: nowIso(),\n });\n}\n\nfunction titleFromMessage(message) {\n const compact = message.replace(/\\s+/g, ' ').trim();\n if (!compact) return '新对话';\n return compact.length > 22 ? `${compact.slice(0, 22)}...` : compact;\n}\n\nfunction normalizeMessages(rows) {\n return rows.map((row) => ({\n role: row.role,\n content: row.content,\n }));\n}\n\nasync function callDeepSeek(messages) {\n if (!deepseekApiKey) {\n const error = new Error('DeepSeek API key is not configured');\n error.statusCode = 503;\n throw error;\n }\n\n const response = await fetch(`${deepseekBaseUrl}/chat/completions`, {\n method: 'POST',\n headers: {\n Accept: 'application/json',\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${deepseekApiKey}`,\n },\n body: JSON.stringify({\n model: deepseekModel,\n messages,\n temperature: 0.4,\n max_tokens: 1400,\n stream: false,\n }),\n signal: AbortSignal.timeout(60_000),\n });\n\n const bodyText = await response.text();\n let payload;\n try {\n payload = bodyText ? JSON.parse(bodyText) : {};\n } catch {\n payload = { error: bodyText };\n }\n\n if (!response.ok) {\n const upstreamMessage = payload?.error?.message || payload?.message || '';\n const friendlyMessage =\n response.status === 402 || /insufficient\\s+balance/i.test(upstreamMessage)\n ? '大乘 AI 服务额度暂不可用,请联系管理员充值 DeepSeek 账户后重试。'\n : upstreamMessage || `DeepSeek request failed: ${response.status}`;\n const error = new Error(friendlyMessage);\n error.statusCode = response.status;\n throw error;\n }\n\n const message = payload?.choices?.[0]?.message?.content?.trim();\n if (!message) {\n const error = new Error('DeepSeek returned an empty response');\n error.statusCode = 502;\n throw error;\n }\n\n return {\n message,\n usage: {\n promptTokens: Number(payload?.usage?.prompt_tokens || 0),\n completionTokens: Number(payload?.usage?.completion_tokens || 0),\n totalTokens: Number(payload?.usage?.total_tokens || 0),\n },\n };\n}\n\nasync function callDeepSeekStream(messages, callbacks = {}) {\n if (!deepseekApiKey) {\n const error = new Error('DeepSeek API key is not configured');\n error.statusCode = 503;\n throw error;\n }\n\n const response = await fetch(`${deepseekBaseUrl}/chat/completions`, {\n method: 'POST',\n headers: {\n Accept: 'text/event-stream',\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${deepseekApiKey}`,\n },\n body: JSON.stringify({\n model: deepseekModel,\n messages,\n temperature: 0.4,\n max_tokens: 1400,\n stream: true,\n stream_options: { include_usage: true },\n }),\n signal: callbacks.signal || AbortSignal.timeout(90_000),\n });\n\n if (!response.ok) {\n const bodyText = await response.text();\n let payload;\n try {\n payload = bodyText ? JSON.parse(bodyText) : {};\n } catch {\n payload = { error: bodyText };\n }\n const upstreamMessage = payload?.error?.message || payload?.message || '';\n const friendlyMessage =\n response.status === 402 || /insufficient\\s+balance/i.test(upstreamMessage)\n ? '大乘 AI 服务额度暂不可用,请联系管理员充值 DeepSeek 账户后重试。'\n : upstreamMessage || `DeepSeek request failed: ${response.status}`;\n const error = new Error(friendlyMessage);\n error.statusCode = response.status;\n throw error;\n }\n\n let buffer = '';\n let message = '';\n let usage = {\n promptTokens: 0,\n completionTokens: 0,\n totalTokens: 0,\n };\n const decoder = new TextDecoder();\n\n for await (const chunk of response.body) {\n buffer += decoder.decode(chunk, { stream: true });\n const lines = buffer.split(/\\r?\\n/);\n buffer = lines.pop() || '';\n\n for (const rawLine of lines) {\n const line = rawLine.trim();\n if (!line.startsWith('data:')) continue;\n const data = line.slice('data:'.length).trim();\n if (!data || data === '[DONE]') continue;\n\n let payload;\n try {\n payload = JSON.parse(data);\n } catch {\n continue;\n }\n\n const delta = payload?.choices?.[0]?.delta?.content || '';\n if (delta) {\n message += delta;\n callbacks.onToken?.(delta);\n }\n\n if (payload?.usage) {\n usage = {\n promptTokens: Number(payload.usage.prompt_tokens || usage.promptTokens || 0),\n completionTokens: Number(payload.usage.completion_tokens || usage.completionTokens || 0),\n totalTokens: Number(payload.usage.total_tokens || usage.totalTokens || 0),\n };\n }\n }\n }\n\n message = message.trim();\n if (!message) {\n const error = new Error('DeepSeek returned an empty response');\n error.statusCode = 502;\n throw error;\n }\n\n return { message, usage };\n}\n\nfunction createCodexDeepSeekRuntime() {\n if (!enableCodexSdkChat || !deepseekApiKey) {\n return {\n enabled: false,\n provider: 'deepseek-direct',\n reason: enableCodexSdkChat ? 'DeepSeek API key is not configured' : 'ENABLE_CODEX_SDK_CHAT is not true',\n };\n }\n\n return {\n enabled: true,\n provider: 'codex-sdk-deepseek',\n options: {\n apiKey: deepseekApiKey,\n baseUrl: deepseekBaseUrl,\n env: {\n ...process.env,\n DEEPSEEK_API_KEY: deepseekApiKey,\n HOME: codexHomeDir,\n CODEX_HOME: codexHomeDir,\n TMPDIR: codexTempDir,\n XDG_RUNTIME_DIR: codexRuntimeDir,\n XDG_CACHE_HOME: path.join(codexHomeDir, '.cache'),\n XDG_CONFIG_HOME: path.join(codexHomeDir, '.config'),\n XDG_DATA_HOME: path.join(codexHomeDir, '.local', 'share'),\n PATH: process.env.PATH || '/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin',\n },\n config: {\n model_provider: codexDeepSeekProviderId,\n model_providers: {\n [codexDeepSeekProviderId]: {\n name: 'DeepSeek Chat Completions',\n base_url: codexResponsesBaseUrl,\n env_key: 'DEEPSEEK_API_KEY',\n wire_api: 'responses',\n query_params: {},\n },\n },\n },\n },\n threadOptions: {\n model: deepseekModel,\n approvalPolicy: 'never',\n sandboxMode: 'read-only',\n skipGitRepoCheck: true,\n networkAccessEnabled: true,\n },\n };\n}\n\nfunction codexPromptFromMessages(messages) {\n return messages\n .map((message) => {\n const role = String(message.role || 'user').toUpperCase();\n return `${role}:\\n${message.content}`;\n })\n .join('\\n\\n');\n}\n\nfunction responseContentText(content) {\n if (typeof content === 'string') return content;\n if (!Array.isArray(content)) return '';\n return content\n .map((part) => {\n if (typeof part === 'string') return part;\n if (!part || typeof part !== 'object') return '';\n return String(part.text || part.input_text || part.output_text || '');\n })\n .filter(Boolean)\n .join('\\n');\n}\n\nfunction codexResponsesPrompt(body) {\n const input = Array.isArray(body?.input) ? body.input : [];\n const userItems = input.filter((item) => item?.role === 'user');\n const source = userItems[userItems.length - 1] || input[input.length - 1];\n const prompt = responseContentText(source?.content).trim();\n if (prompt) return prompt;\n if (typeof body?.input === 'string') return body.input.trim();\n return '';\n}\n\nfunction writeResponsesEvent(res, event, data) {\n res.write(`event: ${event}\\n`);\n res.write(`data: ${JSON.stringify(data)}\\n\\n`);\n}\n\nfunction responseUsage(usage = {}) {\n return {\n input_tokens: Number(usage.promptTokens || usage.input_tokens || 0),\n cached_input_tokens: 0,\n output_tokens: Number(usage.completionTokens || usage.output_tokens || 0),\n reasoning_output_tokens: 0,\n total_tokens: Number(usage.totalTokens || usage.total_tokens || 0),\n };\n}\n\nfunction isDeepSeekQuotaError(error) {\n const message = String(error?.message || error || '');\n return /额度|insufficient\\s+balance|balance/i.test(message);\n}\n\nfunction publicAiErrorMessage(error) {\n if (isDeepSeekQuotaError(error)) {\n return '大乘 AI 服务额度暂不可用,请联系管理员充值 DeepSeek 账户后重试。';\n }\n return error?.message || 'Internal Server Error';\n}\n\nfunction responsesPayload({\n responseId,\n itemId,\n status,\n model,\n text = '',\n usage = null,\n error = null,\n}) {\n return {\n id: responseId,\n object: 'response',\n status,\n model,\n output:\n status === 'completed'\n ? [\n {\n id: itemId,\n type: 'message',\n role: 'assistant',\n status: 'completed',\n content: [{ type: 'output_text', text }],\n },\n ]\n : [],\n ...(usage ? { usage: responseUsage(usage) } : {}),\n ...(error\n ? {\n error: {\n message: error.message || String(error),\n type: 'server_error',\n },\n }\n : {}),\n };\n}\n\nasync function callCodexSdkDeepSeek(messages, callbacks = {}) {\n const codexRuntime = createCodexDeepSeekRuntime();\n if (!codexRuntime.enabled) {\n const error = new Error(codexRuntime.reason || 'Codex SDK chat is not enabled');\n error.statusCode = 503;\n throw error;\n }\n\n const { Codex } = await import('@openai/codex-sdk');\n const codex = new Codex(codexRuntime.options);\n const thread = codex.startThread(codexRuntime.threadOptions);\n const prompt = codexPromptFromMessages(messages);\n\n if (callbacks.onToken || callbacks.onStep) {\n const { events } = await thread.runStreamed(prompt, { signal: callbacks.signal });\n let finalResponse = '';\n let usage = null;\n\n for await (const event of events) {\n if (event.type === 'item.completed') {\n const item = event.item;\n if (item.type === 'agent_message') {\n finalResponse = item.text || finalResponse;\n callbacks.onToken?.(item.text || '');\n } else if (item.type === 'todo_list') {\n callbacks.onStep?.({\n title: 'Codex SDK 执行计划',\n message: item.items.map((todo) => `${todo.completed ? '✓' : '•'} ${todo.text}`).join('\\n'),\n });\n } else if (item.type === 'command_execution') {\n callbacks.onStep?.({\n title: 'Codex SDK 执行命令',\n message: item.command,\n });\n } else if (item.type === 'mcp_tool_call') {\n callbacks.onStep?.({\n title: 'Codex SDK 调用工具',\n message: `${item.server}.${item.tool}`,\n });\n }\n } else if (event.type === 'turn.completed') {\n usage = event.usage;\n } else if (event.type === 'turn.failed') {\n throw new Error(event.error?.message || 'Codex SDK turn failed');\n } else if (event.type === 'error') {\n throw new Error(event.message || 'Codex SDK stream failed');\n }\n }\n\n if (!finalResponse.trim()) {\n const error = new Error('Codex SDK returned an empty response');\n error.statusCode = 502;\n throw error;\n }\n\n return {\n message: finalResponse.trim(),\n usage: {\n promptTokens: Number(usage?.input_tokens || 0),\n completionTokens: Number(usage?.output_tokens || 0),\n totalTokens: Number((usage?.input_tokens || 0) + (usage?.output_tokens || 0)),\n },\n };\n }\n\n const turn = await thread.run(prompt);\n if (!turn.finalResponse?.trim()) {\n const error = new Error('Codex SDK returned an empty response');\n error.statusCode = 502;\n throw error;\n }\n\n return {\n message: turn.finalResponse.trim(),\n usage: {\n promptTokens: Number(turn.usage?.input_tokens || 0),\n completionTokens: Number(turn.usage?.output_tokens || 0),\n totalTokens: Number((turn.usage?.input_tokens || 0) + (turn.usage?.output_tokens || 0)),\n },\n };\n}\n\napp.post(\n '/codex-deepseek/v1/responses',\n asyncHandler(async (req, res) => {\n const bearer = bearerToken(req);\n if (!deepseekApiKey || bearer !== deepseekApiKey) {\n res.status(401).json({\n error: { message: 'Unauthorized Codex DeepSeek adapter request' },\n });\n return;\n }\n\n const prompt = codexResponsesPrompt(req.body);\n if (!prompt) {\n res.status(400).json({ error: { message: 'Responses input is required' } });\n return;\n }\n\n const model = String(req.body?.model || deepseekModel);\n const responseId = `resp_${crypto.randomUUID().replaceAll('-', '')}`;\n const itemId = `msg_${crypto.randomUUID().replaceAll('-', '')}`;\n const wantsStream =\n req.body?.stream !== false ||\n String(req.get('accept') || '').includes('text/event-stream');\n const messages = [{ role: 'user', content: prompt }];\n\n if (!wantsStream) {\n const result = await callDeepSeek(messages);\n res.json(\n responsesPayload({\n responseId,\n itemId,\n status: 'completed',\n model,\n text: result.message,\n usage: result.usage,\n }),\n );\n return;\n }\n\n res.status(200);\n res.set({\n 'Content-Type': 'text/event-stream; charset=utf-8',\n 'Cache-Control': 'no-cache, no-transform',\n Connection: 'keep-alive',\n 'X-Accel-Buffering': 'no',\n });\n res.flushHeaders?.();\n\n writeResponsesEvent(res, 'response.created', {\n type: 'response.created',\n response: responsesPayload({\n responseId,\n itemId,\n status: 'in_progress',\n model,\n }),\n });\n writeResponsesEvent(res, 'response.output_item.added', {\n type: 'response.output_item.added',\n output_index: 0,\n item: {\n id: itemId,\n type: 'message',\n role: 'assistant',\n status: 'in_progress',\n content: [],\n },\n });\n writeResponsesEvent(res, 'response.content_part.added', {\n type: 'response.content_part.added',\n item_id: itemId,\n output_index: 0,\n content_index: 0,\n part: { type: 'output_text', text: '' },\n });\n\n let text = '';\n try {\n const result = await callDeepSeekStream(messages, {\n onToken: (delta) => {\n text += delta;\n writeResponsesEvent(res, 'response.output_text.delta', {\n type: 'response.output_text.delta',\n item_id: itemId,\n output_index: 0,\n content_index: 0,\n delta,\n });\n },\n });\n text = result.message || text;\n writeResponsesEvent(res, 'response.output_text.done', {\n type: 'response.output_text.done',\n item_id: itemId,\n output_index: 0,\n content_index: 0,\n text,\n });\n writeResponsesEvent(res, 'response.content_part.done', {\n type: 'response.content_part.done',\n item_id: itemId,\n output_index: 0,\n content_index: 0,\n part: { type: 'output_text', text },\n });\n writeResponsesEvent(res, 'response.output_item.done', {\n type: 'response.output_item.done',\n output_index: 0,\n item: {\n id: itemId,\n type: 'message',\n role: 'assistant',\n status: 'completed',\n content: [{ type: 'output_text', text }],\n },\n });\n writeResponsesEvent(res, 'response.completed', {\n type: 'response.completed',\n response: responsesPayload({\n responseId,\n itemId,\n status: 'completed',\n model,\n text,\n usage: result.usage,\n }),\n });\n res.end();\n } catch (error) {\n writeResponsesEvent(res, 'response.failed', {\n type: 'response.failed',\n response: responsesPayload({\n responseId,\n itemId,\n status: 'failed',\n model,\n text,\n error,\n }),\n });\n res.end();\n }\n }),\n);\n\napp.get('/health', (_req, res) => {\n const codexRuntime = createCodexDeepSeekRuntime();\n jsonResponse(res, 200, {\n status: 'ok',\n service: 'dacheng-ai-backend',\n provider: 'deepseek',\n model: deepseekModel,\n codexSdkAvailable: true,\n codexSdkChatEnabled: codexRuntime.enabled,\n codexSdkProvider: codexRuntime.provider,\n cbetaApiRoot,\n timestamp: nowIso(),\n });\n});\n\napp.post(\n '/api/ai/chat',\n asyncHandler(async (req, res) => {\n const message = safeUserText(req.body?.message);\n if (!message) {\n return jsonResponse(res, 400, { success: false, message: 'message is required' });\n }\n\n const user = await resolveUser(req, req.body || {});\n const estimated = estimateTokens(message) + 600;\n const budget = enforceTokenBudget(user, estimated);\n const createdAt = nowIso();\n const conversationId = safeUserText(req.body?.conversationId) || crypto.randomUUID();\n let conversation = statements.getConversation.get(conversationId, user.userId);\n const isNewConversation = !conversation;\n\n if (isNewConversation) {\n conversation = {\n id: conversationId,\n user_id: user.userId,\n title: titleFromMessage(message),\n };\n statements.insertConversation.run({\n id: conversationId,\n userId: user.userId,\n title: conversation.title,\n provider: 'deepseek',\n model: deepseekModel,\n createdAt,\n updatedAt: createdAt,\n });\n }\n\n statements.insertMessage.run({\n id: crypto.randomUUID(),\n conversationId,\n role: 'user',\n content: message,\n promptTokens: 0,\n completionTokens: 0,\n totalTokens: 0,\n createdAt,\n });\n\n const skillResult = await runResourceFinderSkill({ message, user });\n const skillContext = resourceContextMessage(skillResult);\n const historyRows = statements.listMessages.all(conversationId).slice(-14);\n const modelMessages = [\n { role: 'system', content: systemPrompt },\n ...(skillContext ? [{ role: 'system', content: skillContext }] : []),\n ...normalizeMessages(historyRows),\n ];\n\n const codexRuntime = createCodexDeepSeekRuntime();\n let provider = 'deepseek';\n let aiResult;\n try {\n if (codexRuntime.enabled) {\n provider = codexRuntime.provider;\n aiResult = await callCodexSdkDeepSeek(modelMessages);\n } else {\n aiResult = await callDeepSeek(modelMessages);\n }\n } catch (error) {\n if (!codexRuntime.enabled || isDeepSeekQuotaError(error)) throw error;\n logger.warn({ error: String(error) }, 'Codex SDK chat failed, falling back to DeepSeek direct');\n provider = 'deepseek';\n aiResult = await callDeepSeek(modelMessages);\n }\n const usage = {\n promptTokens: aiResult.usage.promptTokens || estimateTokens(JSON.stringify(modelMessages)),\n completionTokens: aiResult.usage.completionTokens || estimateTokens(aiResult.message),\n totalTokens:\n aiResult.usage.totalTokens ||\n estimateTokens(JSON.stringify(modelMessages)) + estimateTokens(aiResult.message),\n };\n recordUsage(user.userId, usage.totalTokens);\n\n const answeredAt = nowIso();\n statements.insertMessage.run({\n id: crypto.randomUUID(),\n conversationId,\n role: 'assistant',\n content: aiResult.message,\n promptTokens: usage.promptTokens,\n completionTokens: usage.completionTokens,\n totalTokens: usage.totalTokens,\n createdAt: answeredAt,\n });\n statements.updateConversation.run({\n id: conversationId,\n userId: user.userId,\n title: isNewConversation ? titleFromMessage(message) : conversation.title,\n updatedAt: answeredAt,\n });\n\n const latestUsage = usageFor(user.userId);\n const monthlyLimit = budget.limit;\n jsonResponse(res, 200, {\n success: true,\n conversationId,\n provider,\n model: deepseekModel,\n message: aiResult.message,\n usage: {\n ...usage,\n monthlyLimit,\n remainingTokens: Math.max(0, monthlyLimit - latestUsage.used),\n },\n });\n }),\n);\n\napp.post('/api/ai/chat/stream', async (req, res) => {\n sseHeaders(res);\n\n try {\n const message = safeUserText(req.body?.message);\n if (!message) {\n writeSse(res, 'error', { message: 'message is required' });\n res.end();\n return;\n }\n\n const user = await resolveUser(req, req.body || {});\n const estimated = estimateTokens(message) + 600;\n const budget = enforceTokenBudget(user, estimated);\n const createdAt = nowIso();\n const conversationId = safeUserText(req.body?.conversationId) || crypto.randomUUID();\n let conversation = statements.getConversation.get(conversationId, user.userId);\n const isNewConversation = !conversation;\n\n if (isNewConversation) {\n conversation = {\n id: conversationId,\n user_id: user.userId,\n title: titleFromMessage(message),\n };\n statements.insertConversation.run({\n id: conversationId,\n userId: user.userId,\n title: conversation.title,\n provider: 'deepseek',\n model: deepseekModel,\n createdAt,\n updatedAt: createdAt,\n });\n }\n\n statements.insertMessage.run({\n id: crypto.randomUUID(),\n conversationId,\n role: 'user',\n content: message,\n promptTokens: 0,\n completionTokens: 0,\n totalTokens: 0,\n createdAt,\n });\n\n const codexRuntime = createCodexDeepSeekRuntime();\n let provider = codexRuntime.enabled ? codexRuntime.provider : 'deepseek';\n writeSse(res, 'meta', {\n conversationId,\n provider,\n model: deepseekModel,\n });\n writeSse(res, 'step', {\n title: '连接后端 AI',\n message: '大乘后端已直连 VPS,并使用 DeepSeek OpenAI-compatible API。',\n });\n\n const skillResult = await runResourceFinderSkill({\n message,\n user,\n onStep: (step) => writeSse(res, 'step', step),\n });\n const skillContext = resourceContextMessage(skillResult);\n const historyRows = statements.listMessages.all(conversationId).slice(-14);\n const modelMessages = [\n { role: 'system', content: systemPrompt },\n ...(skillContext ? [{ role: 'system', content: skillContext }] : []),\n ...normalizeMessages(historyRows),\n ];\n\n let aiResult;\n try {\n if (codexRuntime.enabled) {\n aiResult = await callCodexSdkDeepSeek(modelMessages, {\n onToken: (token) => writeSse(res, 'delta', { text: token }),\n onStep: (step) => writeSse(res, 'step', step),\n });\n } else {\n aiResult = await callDeepSeekStream(modelMessages, {\n onToken: (token) => writeSse(res, 'delta', { text: token }),\n });\n }\n } catch (error) {\n if (!codexRuntime.enabled || isDeepSeekQuotaError(error)) throw error;\n logger.warn({ error: String(error) }, 'Codex SDK streaming chat failed, falling back to DeepSeek direct');\n provider = 'deepseek';\n writeSse(res, 'step', {\n title: 'Codex SDK 暂不可用',\n message: '已自动回退为 DeepSeek 直连流式返回。',\n });\n aiResult = await callDeepSeekStream(modelMessages, {\n onToken: (token) => writeSse(res, 'delta', { text: token }),\n });\n }\n\n const usage = {\n promptTokens: aiResult.usage.promptTokens || estimateTokens(JSON.stringify(modelMessages)),\n completionTokens: aiResult.usage.completionTokens || estimateTokens(aiResult.message),\n totalTokens:\n aiResult.usage.totalTokens ||\n estimateTokens(JSON.stringify(modelMessages)) + estimateTokens(aiResult.message),\n };\n recordUsage(user.userId, usage.totalTokens);\n\n const answeredAt = nowIso();\n statements.insertMessage.run({\n id: crypto.randomUUID(),\n conversationId,\n role: 'assistant',\n content: aiResult.message,\n promptTokens: usage.promptTokens,\n completionTokens: usage.completionTokens,\n totalTokens: usage.totalTokens,\n createdAt: answeredAt,\n });\n statements.updateConversation.run({\n id: conversationId,\n userId: user.userId,\n title: isNewConversation ? titleFromMessage(message) : conversation.title,\n updatedAt: answeredAt,\n });\n\n const latestUsage = usageFor(user.userId);\n const monthlyLimit = budget.limit;\n writeSse(res, 'done', {\n conversationId,\n provider,\n model: deepseekModel,\n message: aiResult.message,\n usage: {\n ...usage,\n monthlyLimit,\n remainingTokens: Math.max(0, monthlyLimit - latestUsage.used),\n },\n });\n res.end();\n } catch (error) {\n logger.error({ error: error.stack || String(error) }, 'streaming chat failed');\n writeSse(res, 'error', {\n message: publicAiErrorMessage(error),\n details: error.details,\n });\n res.end();\n }\n});\n\napp.get(\n '/api/ai/conversations',\n asyncHandler(async (req, res) => {\n const user = await resolveUser(req, { username: req.query.username });\n const items = statements.listConversations.all(user.userId).map((item) => ({\n id: item.id,\n title: item.title,\n updatedAt: item.updatedAt,\n }));\n jsonResponse(res, 200, { success: true, items });\n }),\n);\n\napp.get(\n '/api/ai/conversations/:id',\n asyncHandler(async (req, res) => {\n const user = await resolveUser(req);\n const conversation = statements.getConversation.get(req.params.id, user.userId);\n if (!conversation) {\n return jsonResponse(res, 404, { success: false, message: 'conversation not found' });\n }\n const messages = statements.listMessages.all(req.params.id).map((item) => ({\n role: item.role,\n content: item.content,\n createdAt: item.createdAt,\n }));\n jsonResponse(res, 200, {\n success: true,\n conversation: {\n id: conversation.id,\n title: conversation.title,\n updatedAt: conversation.updated_at,\n },\n messages,\n });\n }),\n);\n\nfunction buildUrl(base, endpoint, params = {}) {\n const url = new URL(endpoint, `${base}/`);\n for (const [key, value] of Object.entries(params)) {\n if (Array.isArray(value)) {\n for (const item of value) {\n if (item !== undefined && item !== null && item !== '') {\n url.searchParams.append(key, String(item));\n }\n }\n } else if (value !== undefined && value !== null && value !== '') {\n url.searchParams.set(key, String(value));\n }\n }\n return url;\n}\n\nasync function fetchJson(url) {\n const response = await fetch(url, {\n headers: { Accept: 'application/json' },\n signal: AbortSignal.timeout(15_000),\n });\n if (!response.ok) {\n throw new Error(`Request failed: ${response.status}`);\n }\n return response.json();\n}\n\nfunction stripHtml(value) {\n return String(value || '')\n .replace(/<script[\\s\\S]*?<\\/script>/gi, ' ')\n .replace(/<style[\\s\\S]*?<\\/style>/gi, ' ')\n .replace(/<[^>]+>/g, ' ')\n .replace(/ /g, ' ')\n .replace(/</g, '<')\n .replace(/>/g, '>')\n .replace(/&/g, '&')\n .replace(/&#(\\d+);/g, (_match, code) => String.fromCharCode(Number(code)))\n .replace(/\\s+/g, ' ')\n .trim();\n}\n\nfunction cleanExtractedText(value) {\n let text = String(value || '')\n .replace(/\\b[A-Z]{1,3}\\d{2}n\\d{4,5}[a-z]?_p?\\d{4}[a-z]\\d{2}\\b/g, ' ')\n .replace(/\\uFFFD/g, '')\n .replace(/\\s+([,。;:!?、」』)》】])/g, '$1')\n .replace(/([「『《(【])\\s+/g, '$1');\n for (let i = 0; i < 3; i += 1) {\n text = text.replace(/([\\u3400-\\u9fff])\\s+([\\u3400-\\u9fff])/g, '$1$2');\n }\n return text.replace(/[ \\t]{2,}/g, ' ').trim();\n}\n\nfunction compactText(value, limit = 140) {\n const text = stripHtml(value);\n return text.length > limit ? `${text.slice(0, limit)}...` : text;\n}\n\nfunction collectSearchResults(data) {\n if (!data || typeof data !== 'object') return [];\n if (Array.isArray(data.results)) return data.results;\n if (Array.isArray(data?.results?.docs)) return data.results.docs;\n if (Array.isArray(data?.response?.docs)) return data.response.docs;\n if (Array.isArray(data.docs)) return data.docs;\n return [];\n}\n\nfunction firstText(item, keys) {\n for (const key of keys) {\n const value = item?.[key];\n if (Array.isArray(value)) {\n const first = value.map((entry) => safeUserText(String(entry))).find(Boolean);\n if (first) return first;\n }\n if (typeof value === 'string' && value.trim()) return value.trim();\n if (typeof value === 'number') return String(value);\n }\n return '';\n}\n\nfunction firstNumber(item, keys, fallback = undefined) {\n for (const key of keys) {\n const value = item?.[key];\n if (typeof value === 'number') return value;\n if (typeof value === 'string' && value.trim() && Number.isFinite(Number(value))) {\n return Number(value);\n }\n }\n return fallback;\n}\n\nasync function searchCbetaTitle(query, limit) {\n const url = buildUrl(cbetaApiRoot, 'search/title', { q: query, start: 0, rows: limit });\n const data = await fetchJson(url);\n return collectSearchResults(data).flatMap((item) => {\n const work = firstText(item, ['work', 'work_id', 'workId']);\n const title = firstText(item, ['content', 'title', 'work_title', 'workTitle']);\n if (!work || !title) return [];\n const juan = firstNumber(item, ['juan', 'juan_num', 'juanNum'], 1);\n return [\n {\n id: `cbeta-title-${work}-${juan}`,\n title,\n sourceName: 'CBETA 佛典',\n url: `cbeta:${work}:${juan}`,\n snippet: firstText(item, ['byline', 'creators', 'time_dynasty']) || '可下载经文正文并用于全球法布施。',\n resourceType: 'scripture',\n work,\n juan,\n },\n ];\n });\n}\n\nasync function searchCbetaContent(query, limit) {\n const endpoints = ['search/fulltext', 'search/content', 'search/all_in_one'];\n const settled = await Promise.allSettled(\n endpoints.map((endpoint) =>\n fetchJson(buildUrl(cbetaApiRoot, endpoint, { q: query, start: 0, rows: limit, field: 'content' })),\n ),\n );\n const results = [];\n for (const response of settled) {\n if (response.status !== 'fulfilled') continue;\n for (const item of collectSearchResults(response.value)) {\n const work = firstText(item, ['work', 'work_id', 'workId', 'sutra', 'id']);\n const snippet = compactText(firstText(item, ['kwic', 'highlight', 'snippet', 'content', 'text', 'body', 'p']));\n if (!work || !snippet) continue;\n const juan = firstNumber(item, ['juan', 'juan_num', 'juanNum', '卷'], 1);\n const title = firstText(item, ['title', 'work_title', 'workTitle', 'sutra_name', 'book', 'name']) || work;\n results.push({\n id: `cbeta-content-${work}-${juan}-${sha256(snippet).slice(0, 8)}`,\n title,\n sourceName: 'CBETA 佛典',\n url: `cbeta:${work}:${juan}`,\n snippet,\n resourceType: 'scripture',\n work,\n juan,\n });\n if (results.length >= limit) return results;\n }\n }\n return results;\n}\n\nfunction directUrlResource(query) {\n const url = safeUserText(query);\n let parsed;\n try {\n parsed = new URL(url);\n } catch {\n return null;\n }\n if (!['http:', 'https:'].includes(parsed.protocol)) return null;\n return {\n id: `url-${sha256(url).slice(0, 12)}`,\n title: parsed.hostname,\n sourceName: '网页资源',\n url,\n snippet: '直接下载并提取这个链接的可读正文。',\n resourceType: 'web',\n };\n}\n\nfunction looksLikeAudioQuery(query) {\n return /音乐|音樂|音频|音訊|mp3|audio|念诵|念誦|诵经|誦經|唱诵|唱誦|读诵|讀誦/.test(\n safeUserText(query).toLowerCase(),\n );\n}\n\nfunction archiveSearchTerm(query) {\n return (\n safeUserText(query)\n .replace(/下载|查找|寻找|找一?个|资源|音频|音訊|音乐|音樂|mp3|audio|念诵|念誦|诵经|誦經|唱诵|唱誦|读诵|讀誦/gi, ' ')\n .replace(/\\s+/g, ' ')\n .trim() || safeUserText(query)\n );\n}\n\nfunction solrPhrase(value) {\n return String(value).replace(/\\\\/g, '\\\\\\\\').replace(/\"/g, '\\\\\"');\n}\n\nasync function searchInternetArchiveAudio(query, limit) {\n if (!looksLikeAudioQuery(query)) return [];\n const term = archiveSearchTerm(query);\n if (!term) return [];\n const variants = Array.from(\n new Set([\n term,\n term.replaceAll('金刚经', '金剛經'),\n term.replaceAll('金剛經', '金刚经'),\n ].filter(Boolean)),\n );\n const phraseQuery = variants\n .flatMap((item) => [`title:\"${solrPhrase(item)}\"`, `description:\"${solrPhrase(item)}\"`])\n .join(' OR ');\n const url = buildUrl('https://archive.org', 'advancedsearch.php', {\n q: `(${phraseQuery}) AND mediatype:audio`,\n rows: Math.min(Math.max(limit, 1), 10),\n output: 'json',\n 'fl[]': ['identifier', 'title', 'description'],\n });\n const data = await fetchJson(url);\n const docs = Array.isArray(data?.response?.docs) ? data.response.docs : [];\n return docs\n .map((item) => {\n const identifier = firstText(item, ['identifier']);\n const title = firstText(item, ['title']) || identifier;\n if (!identifier || !title) return null;\n return {\n id: `ia-audio-${identifier}`,\n title,\n sourceName: 'Internet Archive 音频',\n url: `ia:${identifier}`,\n snippet: compactText(firstText(item, ['description'])) || '公开可访问的音频资源,将下载第一个可用 MP3 文件。',\n resourceType: 'audio',\n identifier,\n };\n })\n .filter(Boolean);\n}\n\nconst curatedCbetaWorks = [\n {\n work: 'T0235',\n juan: 1,\n title: '金剛般若波羅蜜經',\n keywords: ['金剛般若波羅蜜經', '金刚般若波罗蜜经', '金剛經', '金刚经'],\n snippet: '姚秦 鳩摩羅什譯。常用于读诵、共修与法布施的般若经典。',\n },\n {\n work: 'T0251',\n juan: 1,\n title: '般若波羅蜜多心經',\n keywords: ['般若波羅蜜多心經', '般若波罗蜜多心经', '心經', '心经'],\n snippet: '唐 玄奘譯。篇幅短,适合加入禅室功课本与日常持诵。',\n },\n {\n work: 'T0262',\n juan: 1,\n title: '妙法蓮華經',\n keywords: ['妙法蓮華經', '妙法莲华经', '法華經', '法华经'],\n snippet: '姚秦 鳩摩羅什譯。可按卷下载正文,用于长期功课与分享。',\n },\n {\n work: 'T0279',\n juan: 1,\n title: '大方廣佛華嚴經',\n keywords: ['大方廣佛華嚴經', '大方广佛华严经', '華嚴經', '华严经'],\n snippet: '唐 實叉難陀譯。可按卷下载正文,用于系统研读与法布施。',\n },\n {\n work: 'T0360',\n juan: 1,\n title: '佛說無量壽經',\n keywords: ['佛說無量壽經', '佛说无量寿经', '無量壽經', '无量寿经'],\n snippet: '净土经典。可下载正文,用于净土功课本与全球法布施。',\n },\n {\n work: 'T0365',\n juan: 1,\n title: '佛說觀無量壽佛經',\n keywords: ['佛說觀無量壽佛經', '佛说观无量寿佛经', '觀無量壽經', '观无量寿经'],\n snippet: '净土三经之一。适合查找下载后加入净土功课。',\n },\n {\n work: 'T0366',\n juan: 1,\n title: '佛說阿彌陀經',\n keywords: ['佛說阿彌陀經', '佛说阿弥陀经', '阿彌陀經', '阿弥陀经'],\n snippet: '姚秦 鳩摩羅什譯。篇幅适中,适合共修、持诵与分享。',\n },\n {\n work: 'T0475',\n juan: 1,\n title: '維摩詰所說經',\n keywords: ['維摩詰所說經', '维摩诘所说经', '維摩詰經', '维摩诘经'],\n snippet: '姚秦 鳩摩羅什譯。大乘经典,可按卷下载用于研读。',\n },\n {\n work: 'T0676',\n juan: 1,\n title: '解深密經',\n keywords: ['解深密經', '解深密经'],\n snippet: '唯识相关大乘经典。适合研读、整理笔记与法布施。',\n },\n {\n work: 'T1666',\n juan: 1,\n title: '大乘起信論',\n keywords: ['大乘起信論', '大乘起信论', '起信論', '起信论'],\n snippet: '大乘论典。适合下载后作为研习功课资料。',\n },\n];\n\nfunction localCbetaResults(query, limit) {\n const normalizedQuery = safeUserText(query).toLowerCase();\n if (!normalizedQuery) return [];\n return curatedCbetaWorks\n .filter((item) =>\n item.keywords.some((keyword) => {\n const normalizedKeyword = keyword.toLowerCase();\n return normalizedKeyword.includes(normalizedQuery) || normalizedQuery.includes(normalizedKeyword);\n }),\n )\n .slice(0, limit)\n .map((item) => ({\n id: `cbeta-curated-${item.work}-${item.juan}`,\n title: item.title,\n sourceName: 'CBETA 佛典',\n url: `cbeta:${item.work}:${item.juan}`,\n snippet: item.snippet,\n resourceType: 'scripture',\n work: item.work,\n juan: item.juan,\n }));\n}\n\nfunction uniqueResources(items, limit) {\n const seen = new Set();\n const results = [];\n for (const item of items) {\n const key = `${item.url}:${item.title}`;\n if (seen.has(key)) continue;\n seen.add(key);\n results.push(item);\n if (results.length >= limit) break;\n }\n return results;\n}\n\nasync function findResourceCandidates(query, limit) {\n const direct = directUrlResource(query);\n const results = [];\n if (direct) results.push(direct);\n try {\n results.push(...(await searchInternetArchiveAudio(query, limit)));\n } catch (error) {\n logger.warn({ error: String(error), query }, 'Internet Archive audio search skipped');\n }\n results.push(...localCbetaResults(query, limit));\n\n const [titleResults, contentResults] = await Promise.allSettled([\n searchCbetaTitle(query, limit),\n searchCbetaContent(query, limit),\n ]);\n if (titleResults.status === 'fulfilled') results.push(...titleResults.value);\n if (contentResults.status === 'fulfilled') results.push(...contentResults.value);\n\n return uniqueResources(results, limit);\n}\n\nfunction looksLikeResourceTask(message) {\n const text = safeUserText(message).toLowerCase();\n if (!text) return false;\n return /下载|查找|寻找|找一?个|资源|經文|经文|佛经|佛經|仪轨|儀軌|音乐|音樂|mp3|audio|念诵|念誦|诵经|誦經/.test(text);\n}\n\nfunction extractResourceQuery(message) {\n const text = safeUserText(message)\n .replace(/请|請|帮我|幫我|麻烦|麻煩|可以|能不能|需要|我要|想要/g, ' ')\n .replace(/下载|下載|查找|寻找|尋找|找一个|找一個|找|资源|資源|并|並|然后|然後|用于|用於|全球法布施|加入功课本|加入功課本/g, ' ')\n .replace(/\\s+/g, ' ')\n .trim();\n return text || safeUserText(message);\n}\n\nfunction resourceContextMessage(skillResult) {\n if (!skillResult) return '';\n const lines = [\n `后端已调用 skill: ${skillResult.skillName}`,\n `执行目标: ${skillResult.query}`,\n ];\n if (skillResult.selected) {\n lines.push(`候选资源: ${skillResult.selected.title}`);\n lines.push(`来源: ${skillResult.selected.sourceName}`);\n lines.push(`资源地址: ${skillResult.selected.url}`);\n }\n if (skillResult.downloaded) {\n lines.push(`下载标题: ${skillResult.downloaded.title}`);\n lines.push(`下载文件: ${skillResult.downloaded.fileName}`);\n const text = safeUserText(skillResult.downloaded.contentText);\n if (text) {\n lines.push('下载正文摘录:');\n lines.push(text.slice(0, 8000));\n }\n }\n return lines.join('\\n');\n}\n\nasync function runResourceFinderSkill({ message, user, onStep }) {\n if (!looksLikeResourceTask(message)) return null;\n\n const skillName = 'resource-finder-downloader';\n const query = extractResourceQuery(message);\n const audioTask = looksLikeAudioQuery(message) || looksLikeAudioQuery(query);\n onStep?.({\n type: 'skill.step',\n skillName,\n stage: 'start',\n title: '调用资源查找下载 skill',\n message: `识别到资源任务: ${query}`,\n });\n\n onStep?.({\n type: 'skill.step',\n skillName,\n stage: 'search',\n title: '搜索可分享资源',\n message: audioTask\n ? '优先检索公开可访问音频资源,失败时再回落到佛典正文资源。'\n : '优先检索 CBETA 佛典正文接口,搜索索引不可用时使用内置经典映射。',\n });\n const candidates = await findResourceCandidates(query, 8);\n if (candidates.length === 0) {\n onStep?.({\n type: 'skill.step',\n skillName,\n stage: 'empty',\n title: '没有找到可下载资源',\n message: '后端没有找到明确可下载且可分享的资源。',\n });\n return {\n skillName,\n query,\n candidates: [],\n selected: null,\n downloaded: null,\n };\n }\n\n const selected = candidates[0];\n onStep?.({\n type: 'skill.step',\n skillName,\n stage: 'select',\n title: '选择可信候选资源',\n message: `${selected.title} / ${selected.sourceName}`,\n });\n\n onStep?.({\n type: 'skill.step',\n skillName,\n stage: 'download',\n title: '下载并提取资源',\n message: selected.url,\n });\n const cbeta = parseCbetaResource(selected);\n const internetArchive = parseInternetArchiveResource(selected);\n const downloaded = cbeta\n ? await downloadCbetaResource({ ...selected, ...cbeta })\n : internetArchive\n ? await downloadInternetArchiveAudio({ ...selected, ...internetArchive })\n : await downloadWebResource(selected);\n const persisted = persistDownloadedResource(user, downloaded);\n\n onStep?.({\n type: 'skill.step',\n skillName,\n stage: 'done',\n title: '资源已准备完成',\n message: downloaded.fileName,\n });\n\n return {\n skillName,\n query,\n candidates,\n selected,\n downloaded: {\n ...downloaded,\n downloadId: persisted.id,\n },\n };\n}\n\napp.post(\n '/api/resources/search',\n asyncHandler(async (req, res) => {\n const query = safeUserText(req.body?.query);\n const limit = Math.min(Math.max(Number(req.body?.limit || 12), 1), 20);\n if (query.length < 2) {\n return jsonResponse(res, 400, { success: false, message: 'query is required' });\n }\n\n const items = await findResourceCandidates(query, limit);\n\n jsonResponse(res, 200, {\n success: true,\n source: 'resource-index',\n items,\n });\n }),\n);\n\nfunction parseCbetaResource(input) {\n const work = safeUserText(input.work);\n const juan = Number(input.juan || 1);\n if (work) return { work, juan: Number.isFinite(juan) ? juan : 1 };\n\n const url = safeUserText(input.url);\n const match = /^cbeta:([^:]+):(\\d+)$/i.exec(url);\n if (!match) return null;\n return {\n work: match[1].toUpperCase(),\n juan: Number(match[2] || 1),\n };\n}\n\nfunction parseInternetArchiveResource(input) {\n const url = safeUserText(input.url);\n const match = /^ia:([^:]+)$/i.exec(url);\n const identifier = match?.[1] || safeUserText(input.identifier);\n if (!identifier) return null;\n return { identifier };\n}\n\nfunction safeFileName(value) {\n const normalized = String(value || 'dharma-resource')\n .replace(/[\\\\/:*?\"<>|]+/g, '-')\n .replace(/\\s+/g, '-')\n .replace(/-+/g, '-')\n .replace(/^-|-$/g, '')\n .slice(0, 80);\n return normalized || 'dharma-resource';\n}\n\nfunction extractCbetaHtml(data) {\n const first = Array.isArray(data?.results) ? data.results[0] : null;\n if (typeof first === 'string') return first;\n if (first && typeof first === 'object' && typeof first.html === 'string') return first.html;\n if (typeof data?.html === 'string') return data.html;\n return '';\n}\n\nasync function downloadCbetaResource(resource) {\n const url = buildUrl(cbetaApiRoot, 'juans', {\n work: resource.work,\n juan: resource.juan,\n work_info: 1,\n toc: 1,\n });\n const data = await fetchJson(url);\n const workInfo = data.work_info || {};\n const title = safeUserText(workInfo.title) || safeUserText(resource.title) || resource.work;\n const html = extractCbetaHtml(data);\n const text = cleanExtractedText(stripHtml(html));\n if (!text) throw new Error('CBETA resource returned empty content');\n return {\n title,\n sourceName: 'CBETA 佛典',\n url: `cbeta:${resource.work}:${resource.juan}`,\n contentText: text.slice(0, maxResourceTextChars),\n fileName: `${resource.work}_${resource.juan}_${safeFileName(title)}.txt`,\n };\n}\n\nasync function downloadInternetArchiveAudio(resource) {\n const identifier = safeUserText(resource.identifier);\n if (!identifier) throw new Error('Internet Archive identifier is required');\n const metadata = await fetchJson(`https://archive.org/metadata/${encodeURIComponent(identifier)}`);\n const title = safeUserText(metadata?.metadata?.title) || safeUserText(resource.title) || identifier;\n const files = Array.isArray(metadata?.files) ? metadata.files : [];\n const audioFile = files.find((file) => {\n const name = safeUserText(file?.name);\n const format = safeUserText(file?.format).toLowerCase();\n return /\\.(mp3|m4a|ogg)$/i.test(name) || format.includes('mp3');\n });\n if (!audioFile?.name) {\n throw new Error('Internet Archive audio item has no downloadable audio file');\n }\n const sourceFileName = audioFile.name;\n const fileUrl = `https://archive.org/download/${encodeURIComponent(identifier)}/${sourceFileName\n .split('/')\n .map((part) => encodeURIComponent(part))\n .join('/')}`;\n const response = await fetch(fileUrl, {\n headers: {\n Accept: 'audio/mpeg, audio/*;q=0.9, */*;q=0.5',\n 'User-Agent': 'DachengResourceDownloader/1.0 (+https://ombhrum.com)',\n },\n signal: AbortSignal.timeout(60_000),\n });\n if (!response.ok) {\n throw new Error(`Internet Archive audio download failed: ${response.status}`);\n }\n const binaryContent = Buffer.from(await response.arrayBuffer());\n if (!binaryContent.length) throw new Error('Internet Archive audio returned empty content');\n const sizeMb = (binaryContent.length / 1024 / 1024).toFixed(1);\n return {\n title,\n sourceName: 'Internet Archive 音频',\n url: fileUrl,\n contentText: `已下载音频资源: ${title}\\n文件名: ${sourceFileName}\\n大小: ${sizeMb} MB\\n来源: ${fileUrl}`,\n binaryContent,\n fileName: safeFileName(sourceFileName),\n };\n}\n\nasync function downloadWebResource(input) {\n const url = safeUserText(input.url);\n const parsed = new URL(url);\n if (!['http:', 'https:'].includes(parsed.protocol)) {\n throw new Error('Only http/https resources can be downloaded');\n }\n\n const response = await fetch(parsed, {\n headers: {\n Accept: 'text/html, text/plain, application/xhtml+xml, application/xml;q=0.8, */*;q=0.5',\n 'User-Agent': 'DachengResourceDownloader/1.0 (+https://ombhrum.com)',\n },\n signal: AbortSignal.timeout(25_000),\n });\n if (!response.ok) throw new Error(`Resource download failed: ${response.status}`);\n const contentType = response.headers.get('content-type') || '';\n const body = await response.text();\n const title = safeUserText(input.title) || parsed.hostname;\n const contentText = contentType.includes('html') ? cleanExtractedText(stripHtml(body)) : body.trim();\n if (!contentText) throw new Error('Resource returned empty content');\n return {\n title,\n sourceName: safeUserText(input.sourceName) || parsed.hostname,\n url,\n contentText: contentText.slice(0, maxResourceTextChars),\n fileName: `${safeFileName(title)}.txt`,\n };\n}\n\nfunction persistDownloadedResource(user, content) {\n const id = crypto.randomUUID();\n const fileName = `${id}-${safeFileName(content.fileName)}`;\n const filePath = path.join(resourcesDir, fileName);\n if (content.binaryContent) {\n fs.writeFileSync(filePath, content.binaryContent);\n } else {\n fs.writeFileSync(filePath, content.contentText, 'utf8');\n }\n statements.insertResourceDownload.run({\n id,\n userId: user.userId,\n title: content.title,\n sourceName: content.sourceName,\n sourceUrl: content.url,\n fileName: content.fileName,\n filePath,\n createdAt: nowIso(),\n });\n return { id, fileName, filePath };\n}\n\nfunction createLibreChatMcpServer() {\n const server = new McpServer({\n name: 'dacheng-ai-tools',\n version: '0.1.0',\n });\n\n server.registerTool(\n 'search_dharma_resources',\n {\n title: 'Search dharma resources',\n description:\n 'Search for legally shareable Buddhist scripture, text, web, or audio resources that can be prepared for Fabushi sharing.',\n inputSchema: {\n query: z.string().min(2).max(200),\n limit: z.number().int().min(1).max(12).optional(),\n },\n outputSchema: {\n query: z.string(),\n items: z.array(\n z.object({\n id: z.string(),\n title: z.string(),\n sourceName: z.string(),\n url: z.string(),\n snippet: z.string(),\n resourceType: z.string(),\n work: z.string().optional(),\n juan: z.number().optional(),\n }),\n ),\n },\n annotations: {\n readOnlyHint: true,\n destructiveHint: false,\n openWorldHint: true,\n },\n },\n async ({ query, limit }) => {\n const items = await findResourceCandidates(query, limit ?? 8);\n return textToolResult(`Found ${items.length} candidate resources for \"${query}\".`, {\n query,\n items,\n });\n },\n );\n\n server.registerTool(\n 'download_dharma_resource',\n {\n title: 'Download dharma resource',\n description:\n 'Download and extract a selected resource. Use the url/work/juan returned by search_dharma_resources whenever possible.',\n inputSchema: {\n url: z.string().min(1).max(1000),\n title: z.string().max(300).optional(),\n sourceName: z.string().max(120).optional(),\n work: z.string().max(40).optional(),\n juan: z.number().int().min(1).optional(),\n identifier: z.string().max(300).optional(),\n },\n outputSchema: {\n title: z.string(),\n sourceName: z.string(),\n url: z.string(),\n fileName: z.string(),\n downloadId: z.string(),\n contentText: z.string(),\n },\n annotations: {\n readOnlyHint: false,\n destructiveHint: false,\n openWorldHint: true,\n },\n },\n async (input) => {\n const user = { userId: 'librechat:mcp' };\n const cbeta = parseCbetaResource(input);\n const internetArchive = parseInternetArchiveResource(input);\n const content = cbeta\n ? await downloadCbetaResource({ ...input, ...cbeta })\n : internetArchive\n ? await downloadInternetArchiveAudio({ ...input, ...internetArchive })\n : await downloadWebResource(input);\n const persisted = persistDownloadedResource(user, content);\n const { binaryContent: _binaryContent, ...publicContent } = content;\n return textToolResult(`Downloaded ${publicContent.title} as ${publicContent.fileName}.`, {\n ...publicContent,\n downloadId: persisted.id,\n });\n },\n );\n\n server.registerTool(\n 'prepare_dharma_share_text',\n {\n title: 'Prepare dharma sharing text',\n description:\n 'Prepare a text payload for the Fabushi app to confirm and add to global dharma sharing. This returns a clientAction; it does not start sending.',\n inputSchema: {\n title: z.string().min(1).max(120),\n text: z.string().min(1).max(20000),\n },\n outputSchema: {\n clientAction: z.object({\n type: z.literal('prepare_dharma_share_text'),\n title: z.string(),\n text: z.string(),\n }),\n },\n annotations: {\n readOnlyHint: false,\n destructiveHint: false,\n openWorldHint: false,\n },\n },\n async ({ title, text }) =>\n textToolResult(`Prepared \"${title}\" for user confirmation in the Fabushi app.`, {\n clientAction: {\n type: 'prepare_dharma_share_text',\n title: safeUserText(title).slice(0, 120),\n text: String(text || '').trim().slice(0, 20000),\n },\n }),\n );\n\n server.registerTool(\n 'prepare_practice_book_item',\n {\n title: 'Prepare practice book item',\n description:\n 'Prepare a scripture, chant, or practice item for the Fabushi app practice book. This returns a clientAction; it does not mutate the app by itself.',\n inputSchema: {\n title: z.string().min(1).max(120),\n sourceName: z.string().max(120).optional(),\n text: z.string().min(1).max(20000),\n },\n outputSchema: {\n clientAction: z.object({\n type: z.literal('prepare_practice_book_item'),\n title: z.string(),\n sourceName: z.string(),\n text: z.string(),\n }),\n },\n annotations: {\n readOnlyHint: false,\n destructiveHint: false,\n openWorldHint: false,\n },\n },\n async ({ title, sourceName, text }) =>\n textToolResult(`Prepared \"${title}\" for the Fabushi practice book confirmation flow.`, {\n clientAction: {\n type: 'prepare_practice_book_item',\n title: safeUserText(title).slice(0, 120),\n sourceName: safeUserText(sourceName || '大乘 AI').slice(0, 120),\n text: String(text || '').trim().slice(0, 20000),\n },\n }),\n );\n\n return server;\n}\n\nasync function handleMcpRequest(req, res) {\n const server = createLibreChatMcpServer();\n const transport = new StreamableHTTPServerTransport({\n sessionIdGenerator: undefined,\n enableJsonResponse: true,\n });\n\n res.on('close', () => {\n transport.close();\n server.close();\n });\n\n await server.connect(transport);\n await transport.handleRequest(req, res, req.body);\n}\n\napp.all('/mcp', async (req, res) => {\n try {\n await handleMcpRequest(req, res);\n } catch (error) {\n logger.error({ error: error.stack || String(error) }, 'MCP request failed');\n if (!res.headersSent) {\n res.status(500).json({\n error: 'MCP request failed',\n message: publicAiErrorMessage(error),\n });\n }\n }\n});\n\napp.post(\n '/api/resources/download',\n asyncHandler(async (req, res) => {\n const user = await resolveUser(req, req.body || {});\n const cbeta = parseCbetaResource(req.body || {});\n const internetArchive = parseInternetArchiveResource(req.body || {});\n const content = cbeta\n ? await downloadCbetaResource({ ...req.body, ...cbeta })\n : internetArchive\n ? await downloadInternetArchiveAudio({ ...req.body, ...internetArchive })\n : await downloadWebResource(req.body || {});\n const persisted = persistDownloadedResource(user, content);\n const { binaryContent: _binaryContent, ...publicContent } = content;\n jsonResponse(res, 200, {\n success: true,\n downloadId: persisted.id,\n ...publicContent,\n });\n }),\n);\n\napp.post(\n '/api/codex/resource-task',\n asyncHandler(async (req, res) => {\n if (process.env.ENABLE_CODEX_SDK !== 'true') {\n return jsonResponse(res, 403, {\n success: false,\n message: 'Codex SDK task runner is disabled. Set ENABLE_CODEX_SDK=true on the server to enable it.',\n });\n }\n const prompt = safeUserText(req.body?.prompt);\n if (!prompt) {\n return jsonResponse(res, 400, { success: false, message: 'prompt is required' });\n }\n const { Codex } = await import('@openai/codex-sdk');\n const codexRuntime = createCodexDeepSeekRuntime();\n const codex = new Codex(codexRuntime.enabled ? codexRuntime.options : {});\n const thread = codex.startThread(\n codexRuntime.enabled\n ? codexRuntime.threadOptions\n : { skipGitRepoCheck: true, networkAccessEnabled: true },\n );\n const result = await thread.run(prompt);\n jsonResponse(res, 200, {\n success: true,\n threadId: thread.id,\n provider: codexRuntime.provider,\n model: deepseekModel,\n result,\n });\n }),\n);\n\napp.use((error, _req, res, _next) => {\n const status = Number(error.statusCode || error.status || 500);\n const safeStatus = status >= 400 && status < 600 ? status : 500;\n logger.error({ error: error.stack || String(error) }, 'request failed');\n jsonResponse(res, safeStatus, {\n success: false,\n message: error.message || 'Internal Server Error',\n details: error.details,\n });\n});\n\nconst port = Number(process.env.PORT || 8788);\napp.listen(port, '0.0.0.0', () => {\n logger.info(\n {\n port,\n dbPath,\n dataDir,\n deepseekModel,\n cbetaApiRoot,\n },\n 'Dacheng AI backend started',\n );\n});\n"}, "7b5358f7f07c445a317052859bf1db8e6d7d7e7c": {"t": 1785757998.0914426, "n": 58627, "h": "443c304557041549", "c": "import crypto from 'node:crypto';\nimport fs from 'node:fs';\nimport path from 'node:path';\nimport process from 'node:process';\nimport { fileURLToPath } from 'node:url';\n\nimport Database from 'better-sqlite3';\nimport cors from 'cors';\nimport dotenv from 'dotenv';\nimport express from 'express';\nimport rateLimit from 'express-rate-limit';\nimport helmet from 'helmet';\nimport pino from 'pino';\n\ndotenv.config();\n\nconst __dirname = path.dirname(fileURLToPath(import.meta.url));\nconst rootDir = path.resolve(__dirname, '..');\nconst dataDir = process.env.DATA_DIR || path.join(rootDir, 'data');\nconst resourcesDir = path.join(dataDir, 'resources');\nconst codexHomeDir = process.env.CODEX_HOME || path.join(dataDir, 'codex-home');\nconst codexTempDir = process.env.CODEX_TMPDIR || path.join(dataDir, 'codex-tmp');\nconst codexRuntimeDir = process.env.XDG_RUNTIME_DIR || path.join(dataDir, 'codex-runtime');\nconst dbPath = process.env.SQLITE_PATH || path.join(dataDir, 'dacheng-ai.sqlite');\n\nfs.mkdirSync(resourcesDir, { recursive: true });\nfs.mkdirSync(codexHomeDir, { recursive: true });\nfs.mkdirSync(codexTempDir, { recursive: true });\nfs.mkdirSync(codexRuntimeDir, { recursive: true });\n\nconst logger = pino({\n level: process.env.LOG_LEVEL || 'info',\n});\n\nconst app = express();\napp.disable('x-powered-by');\napp.set('trust proxy', Number(process.env.TRUST_PROXY_HOPS || 1));\napp.use(helmet());\nconst corsOrigin =\n !process.env.CORS_ORIGIN || process.env.CORS_ORIGIN.trim() === '*'\n ? true\n : process.env.CORS_ORIGIN.split(',').map((item) => item.trim()).filter(Boolean);\napp.use(cors({ origin: corsOrigin }));\napp.use(express.json({ limit: '1mb' }));\napp.use(\n rateLimit({\n windowMs: 60_000,\n limit: Number(process.env.RATE_LIMIT_PER_MINUTE || 90),\n standardHeaders: true,\n legacyHeaders: false,\n }),\n);\n\nconst db = new Database(dbPath);\ndb.pragma('journal_mode = WAL');\ndb.pragma('foreign_keys = ON');\n\ndb.exec(`\n CREATE TABLE IF NOT EXISTS conversations (\n id TEXT PRIMARY KEY,\n user_id TEXT NOT NULL,\n title TEXT NOT NULL,\n provider TEXT NOT NULL,\n model TEXT NOT NULL,\n created_at TEXT NOT NULL,\n updated_at TEXT NOT NULL\n );\n\n CREATE INDEX IF NOT EXISTS idx_conversations_user_updated\n ON conversations (user_id, updated_at DESC);\n\n CREATE TABLE IF NOT EXISTS messages (\n id TEXT PRIMARY KEY,\n conversation_id TEXT NOT NULL,\n role TEXT NOT NULL,\n content TEXT NOT NULL,\n prompt_tokens INTEGER NOT NULL DEFAULT 0,\n completion_tokens INTEGER NOT NULL DEFAULT 0,\n total_tokens INTEGER NOT NULL DEFAULT 0,\n created_at TEXT NOT NULL,\n FOREIGN KEY (conversation_id) REFERENCES conversations(id) ON DELETE CASCADE\n );\n\n CREATE INDEX IF NOT EXISTS idx_messages_conversation_created\n ON messages (conversation_id, created_at ASC);\n\n CREATE TABLE IF NOT EXISTS usage_months (\n user_id TEXT NOT NULL,\n month TEXT NOT NULL,\n total_tokens INTEGER NOT NULL DEFAULT 0,\n updated_at TEXT NOT NULL,\n PRIMARY KEY (user_id, month)\n );\n\n CREATE TABLE IF NOT EXISTS resource_downloads (\n id TEXT PRIMARY KEY,\n user_id TEXT NOT NULL,\n title TEXT NOT NULL,\n source_name TEXT NOT NULL,\n source_url TEXT NOT NULL,\n file_name TEXT NOT NULL,\n file_path TEXT NOT NULL,\n created_at TEXT NOT NULL\n );\n`);\n\nconst statements = {\n insertConversation: db.prepare(`\n INSERT INTO conversations (id, user_id, title, provider, model, created_at, updated_at)\n VALUES (@id, @userId, @title, @provider, @model, @createdAt, @updatedAt)\n `),\n updateConversation: db.prepare(`\n UPDATE conversations\n SET title = @title, updated_at = @updatedAt\n WHERE id = @id AND user_id = @userId\n `),\n getConversation: db.prepare(`\n SELECT * FROM conversations WHERE id = ? AND user_id = ? LIMIT 1\n `),\n listConversations: db.prepare(`\n SELECT id, title, updated_at AS updatedAt\n FROM conversations\n WHERE user_id = ?\n ORDER BY updated_at DESC\n LIMIT 30\n `),\n insertMessage: db.prepare(`\n INSERT INTO messages (\n id, conversation_id, role, content,\n prompt_tokens, completion_tokens, total_tokens, created_at\n )\n VALUES (\n @id, @conversationId, @role, @content,\n @promptTokens, @completionTokens, @totalTokens, @createdAt\n )\n `),\n listMessages: db.prepare(`\n SELECT role, content, prompt_tokens AS promptTokens,\n completion_tokens AS completionTokens, total_tokens AS totalTokens,\n created_at AS createdAt\n FROM messages\n WHERE conversation_id = ?\n ORDER BY created_at ASC\n `),\n getUsage: db.prepare(`\n SELECT total_tokens AS totalTokens FROM usage_months WHERE user_id = ? AND month = ?\n `),\n upsertUsage: db.prepare(`\n INSERT INTO usage_months (user_id, month, total_tokens, updated_at)\n VALUES (@userId, @month, @totalTokens, @updatedAt)\n ON CONFLICT(user_id, month)\n DO UPDATE SET total_tokens = usage_months.total_tokens + excluded.total_tokens,\n updated_at = excluded.updated_at\n `),\n insertResourceDownload: db.prepare(`\n INSERT INTO resource_downloads (\n id, user_id, title, source_name, source_url, file_name, file_path, created_at\n )\n VALUES (\n @id, @userId, @title, @sourceName, @sourceUrl, @fileName, @filePath, @createdAt\n )\n `),\n};\n\nconst deepseekApiKey = process.env.DEEPSEEK_API_KEY || '';\nconst deepseekBaseUrl = (process.env.DEEPSEEK_BASE_URL || 'https://api.deepseek.com').replace(/\\/+$/, '');\nconst deepseekModel = process.env.DEEPSEEK_MODEL || 'deepseek-chat';\nconst cbetaApiRoot = (process.env.CBETA_API_ROOT || 'https://144.24.17.21.sslip.io').replace(/\\/+$/, '');\nconst fabushiApiBaseUrl = (process.env.FABUSHI_API_BASE_URL || 'http://141.148.140.39').replace(/\\/+$/, '');\nconst memberMonthlyLimit = Number(process.env.MEMBER_MONTHLY_TOKEN_LIMIT || 1_000_000);\nconst freeMonthlyLimit = Number(process.env.FREE_MONTHLY_TOKEN_LIMIT || 50_000);\nconst maxResourceTextChars = Number(process.env.MAX_RESOURCE_TEXT_CHARS || 80_000);\nconst enableCodexSdkChat = process.env.ENABLE_CODEX_SDK_CHAT === 'true';\nconst codexDeepSeekProviderId = 'deepseek-chat-completions';\nconst codexResponsesBaseUrl = (\n process.env.CODEX_DEEPSEEK_RESPONSES_BASE_URL ||\n `http://127.0.0.1:${process.env.PORT || 8788}/codex-deepseek/v1`\n).replace(/\\/+$/, '');\n\nconst systemPrompt = [\n '你是“大乘”App 的 AI 助手。',\n '你的核心任务是帮助用户查找、整理、理解并全球法布施合法可分享的佛法资源。',\n '回答要庄重、简洁、可执行;涉及经典、仪轨或资源时,提醒用户尊重版权、来源和当地法规。',\n '如果用户想找资源、下载经文、寻找音频或准备可分享资料,你必须先在后端自动执行搜索、验证和下载步骤;不要要求用户去前端 + 菜单手动查找。',\n '你可以把执行进度总结给用户,但不要输出隐藏推理原文。',\n].join('\\n');\n\nfunction nowIso() {\n return new Date().toISOString();\n}\n\nfunction monthKey(date = new Date()) {\n return `${date.getUTCFullYear()}-${String(date.getUTCMonth() + 1).padStart(2, '0')}`;\n}\n\nfunction jsonResponse(res, status, payload) {\n res.status(status).json(payload);\n}\n\nfunction sseHeaders(res) {\n res.status(200);\n res.setHeader('Content-Type', 'text/event-stream; charset=utf-8');\n res.setHeader('Cache-Control', 'no-cache, no-transform');\n res.setHeader('Connection', 'keep-alive');\n res.setHeader('X-Accel-Buffering', 'no');\n if (typeof res.flushHeaders === 'function') res.flushHeaders();\n}\n\nfunction writeSse(res, event, payload) {\n res.write(`event: ${event}\\n`);\n res.write(`data: ${JSON.stringify(payload)}\\n\\n`);\n}\n\nfunction asyncHandler(fn) {\n return (req, res, next) => {\n Promise.resolve(fn(req, res, next)).catch(next);\n };\n}\n\nfunction bearerToken(req) {\n const header = req.get('Authorization') || '';\n if (!header.startsWith('Bearer ')) return '';\n return header.slice('Bearer '.length).trim();\n}\n\nfunction sha256(value) {\n return crypto.createHash('sha256').update(value).digest('hex');\n}\n\nfunction safeUserText(value, fallback = '') {\n if (typeof value !== 'string') return fallback;\n return value.replace(/\\u0000/g, '').trim();\n}\n\nfunction estimateTokens(text) {\n const normalized = String(text || '').trim();\n if (!normalized) return 0;\n return Math.max(1, Math.ceil(normalized.length / 3));\n}\n\nfunction normalizeMembershipPayload(payload) {\n if (!payload || typeof payload !== 'object') return false;\n const data = payload.data && typeof payload.data === 'object' ? payload.data : payload;\n return Boolean(\n data.isActive ||\n data.active ||\n data.hasPremiumAccess ||\n data.hasPremiumMembership ||\n data.isPremium ||\n data.status === 'active' ||\n data.type === 'paid' ||\n data.membershipType === 'paid',\n );\n}\n\nasync function checkRemoteMembership(token) {\n if (!token || !fabushiApiBaseUrl) return false;\n const endpoints = [\n '/api/stripe/membership-status',\n '/api/alipay/check-membership',\n '/api/auth/user-info',\n ];\n\n for (const endpoint of endpoints) {\n try {\n const response = await fetch(`${fabushiApiBaseUrl}${endpoint}`, {\n headers: {\n Accept: 'application/json',\n Authorization: `Bearer ${token}`,\n },\n signal: AbortSignal.timeout(8_000),\n });\n if (!response.ok) continue;\n const payload = await response.json();\n if (normalizeMembershipPayload(payload)) return true;\n } catch (error) {\n logger.debug({ endpoint, error: String(error) }, 'membership lookup skipped');\n }\n }\n\n return false;\n}\n\nasync function resolveUser(req, body = {}) {\n const token = bearerToken(req);\n const username = safeUserText(body.username || req.query.username);\n const tokenHash = token ? sha256(token).slice(0, 24) : '';\n const anonymousHash = sha256(`${req.ip}|${req.get('User-Agent') || ''}`).slice(0, 24);\n const userId = username\n ? `user:${username}`\n : tokenHash\n ? `token:${tokenHash}`\n : `anon:${anonymousHash}`;\n const remoteMember = await checkRemoteMembership(token);\n const memberHint = body.clientMembershipHint === true;\n return {\n userId,\n username,\n tokenHash,\n isMember: remoteMember || memberHint,\n };\n}\n\nfunction usageFor(userId) {\n const month = monthKey();\n const row = statements.getUsage.get(userId, month);\n return {\n month,\n used: Number(row?.totalTokens || 0),\n };\n}\n\nfunction enforceTokenBudget(user, estimatedTokensForRequest) {\n const limit = user.isMember ? memberMonthlyLimit : freeMonthlyLimit;\n const usage = usageFor(user.userId);\n if (usage.used + estimatedTokensForRequest > limit) {\n const error = new Error('本月 AI token 额度已不足');\n error.statusCode = 429;\n error.details = {\n monthlyLimit: limit,\n usedTokens: usage.used,\n remainingTokens: Math.max(0, limit - usage.used),\n };\n throw error;\n }\n return { limit, usage };\n}\n\nfunction recordUsage(userId, totalTokens) {\n if (!totalTokens || totalTokens < 1) return;\n statements.upsertUsage.run({\n userId,\n month: monthKey(),\n totalTokens,\n updatedAt: nowIso(),\n });\n}\n\nfunction titleFromMessage(message) {\n const compact = message.replace(/\\s+/g, ' ').trim();\n if (!compact) return '新对话';\n return compact.length > 22 ? `${compact.slice(0, 22)}...` : compact;\n}\n\nfunction normalizeMessages(rows) {\n return rows.map((row) => ({\n role: row.role,\n content: row.content,\n }));\n}\n\nasync function callDeepSeek(messages) {\n if (!deepseekApiKey) {\n const error = new Error('DeepSeek API key is not configured');\n error.statusCode = 503;\n throw error;\n }\n\n const response = await fetch(`${deepseekBaseUrl}/chat/completions`, {\n method: 'POST',\n headers: {\n Accept: 'application/json',\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${deepseekApiKey}`,\n },\n body: JSON.stringify({\n model: deepseekModel,\n messages,\n temperature: 0.4,\n max_tokens: 1400,\n stream: false,\n }),\n signal: AbortSignal.timeout(60_000),\n });\n\n const bodyText = await response.text();\n let payload;\n try {\n payload = bodyText ? JSON.parse(bodyText) : {};\n } catch {\n payload = { error: bodyText };\n }\n\n if (!response.ok) {\n const upstreamMessage = payload?.error?.message || payload?.message || '';\n const friendlyMessage =\n response.status === 402 || /insufficient\\s+balance/i.test(upstreamMessage)\n ? '大乘 AI 服务额度暂不可用,请联系管理员充值 DeepSeek 账户后重试。'\n : upstreamMessage || `DeepSeek request failed: ${response.status}`;\n const error = new Error(friendlyMessage);\n error.statusCode = response.status;\n throw error;\n }\n\n const message = payload?.choices?.[0]?.message?.content?.trim();\n if (!message) {\n const error = new Error('DeepSeek returned an empty response');\n error.statusCode = 502;\n throw error;\n }\n\n return {\n message,\n usage: {\n promptTokens: Number(payload?.usage?.prompt_tokens || 0),\n completionTokens: Number(payload?.usage?.completion_tokens || 0),\n totalTokens: Number(payload?.usage?.total_tokens || 0),\n },\n };\n}\n\nasync function callDeepSeekStream(messages, callbacks = {}) {\n if (!deepseekApiKey) {\n const error = new Error('DeepSeek API key is not configured');\n error.statusCode = 503;\n throw error;\n }\n\n const response = await fetch(`${deepseekBaseUrl}/chat/completions`, {\n method: 'POST',\n headers: {\n Accept: 'text/event-stream',\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${deepseekApiKey}`,\n },\n body: JSON.stringify({\n model: deepseekModel,\n messages,\n temperature: 0.4,\n max_tokens: 1400,\n stream: true,\n stream_options: { include_usage: true },\n }),\n signal: callbacks.signal || AbortSignal.timeout(90_000),\n });\n\n if (!response.ok) {\n const bodyText = await response.text();\n let payload;\n try {\n payload = bodyText ? JSON.parse(bodyText) : {};\n } catch {\n payload = { error: bodyText };\n }\n const upstreamMessage = payload?.error?.message || payload?.message || '';\n const friendlyMessage =\n response.status === 402 || /insufficient\\s+balance/i.test(upstreamMessage)\n ? '大乘 AI 服务额度暂不可用,请联系管理员充值 DeepSeek 账户后重试。'\n : upstreamMessage || `DeepSeek request failed: ${response.status}`;\n const error = new Error(friendlyMessage);\n error.statusCode = response.status;\n throw error;\n }\n\n let buffer = '';\n let message = '';\n let usage = {\n promptTokens: 0,\n completionTokens: 0,\n totalTokens: 0,\n };\n const decoder = new TextDecoder();\n\n for await (const chunk of response.body) {\n buffer += decoder.decode(chunk, { stream: true });\n const lines = buffer.split(/\\r?\\n/);\n buffer = lines.pop() || '';\n\n for (const rawLine of lines) {\n const line = rawLine.trim();\n if (!line.startsWith('data:')) continue;\n const data = line.slice('data:'.length).trim();\n if (!data || data === '[DONE]') continue;\n\n let payload;\n try {\n payload = JSON.parse(data);\n } catch {\n continue;\n }\n\n const delta = payload?.choices?.[0]?.delta?.content || '';\n if (delta) {\n message += delta;\n callbacks.onToken?.(delta);\n }\n\n if (payload?.usage) {\n usage = {\n promptTokens: Number(payload.usage.prompt_tokens || usage.promptTokens || 0),\n completionTokens: Number(payload.usage.completion_tokens || usage.completionTokens || 0),\n totalTokens: Number(payload.usage.total_tokens || usage.totalTokens || 0),\n };\n }\n }\n }\n\n message = message.trim();\n if (!message) {\n const error = new Error('DeepSeek returned an empty response');\n error.statusCode = 502;\n throw error;\n }\n\n return { message, usage };\n}\n\nfunction createCodexDeepSeekRuntime() {\n if (!enableCodexSdkChat || !deepseekApiKey) {\n return {\n enabled: false,\n provider: 'deepseek-direct',\n reason: enableCodexSdkChat ? 'DeepSeek API key is not configured' : 'ENABLE_CODEX_SDK_CHAT is not true',\n };\n }\n\n return {\n enabled: true,\n provider: 'codex-sdk-deepseek',\n options: {\n apiKey: deepseekApiKey,\n baseUrl: deepseekBaseUrl,\n env: {\n ...process.env,\n DEEPSEEK_API_KEY: deepseekApiKey,\n HOME: codexHomeDir,\n CODEX_HOME: codexHomeDir,\n TMPDIR: codexTempDir,\n XDG_RUNTIME_DIR: codexRuntimeDir,\n XDG_CACHE_HOME: path.join(codexHomeDir, '.cache'),\n XDG_CONFIG_HOME: path.join(codexHomeDir, '.config'),\n XDG_DATA_HOME: path.join(codexHomeDir, '.local', 'share'),\n PATH: process.env.PATH || '/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin',\n },\n config: {\n model_provider: codexDeepSeekProviderId,\n model_providers: {\n [codexDeepSeekProviderId]: {\n name: 'DeepSeek Chat Completions',\n base_url: codexResponsesBaseUrl,\n env_key: 'DEEPSEEK_API_KEY',\n wire_api: 'responses',\n query_params: {},\n },\n },\n },\n },\n threadOptions: {\n model: deepseekModel,\n approvalPolicy: 'never',\n sandboxMode: 'read-only',\n skipGitRepoCheck: true,\n networkAccessEnabled: true,\n },\n };\n}\n\nfunction codexPromptFromMessages(messages) {\n return messages\n .map((message) => {\n const role = String(message.role || 'user').toUpperCase();\n return `${role}:\\n${message.content}`;\n })\n .join('\\n\\n');\n}\n\nfunction responseContentText(content) {\n if (typeof content === 'string') return content;\n if (!Array.isArray(content)) return '';\n return content\n .map((part) => {\n if (typeof part === 'string') return part;\n if (!part || typeof part !== 'object') return '';\n return String(part.text || part.input_text || part.output_text || '');\n })\n .filter(Boolean)\n .join('\\n');\n}\n\nfunction codexResponsesPrompt(body) {\n const input = Array.isArray(body?.input) ? body.input : [];\n const userItems = input.filter((item) => item?.role === 'user');\n const source = userItems[userItems.length - 1] || input[input.length - 1];\n const prompt = responseContentText(source?.content).trim();\n if (prompt) return prompt;\n if (typeof body?.input === 'string') return body.input.trim();\n return '';\n}\n\nfunction writeResponsesEvent(res, event, data) {\n res.write(`event: ${event}\\n`);\n res.write(`data: ${JSON.stringify(data)}\\n\\n`);\n}\n\nfunction responseUsage(usage = {}) {\n return {\n input_tokens: Number(usage.promptTokens || usage.input_tokens || 0),\n cached_input_tokens: 0,\n output_tokens: Number(usage.completionTokens || usage.output_tokens || 0),\n reasoning_output_tokens: 0,\n total_tokens: Number(usage.totalTokens || usage.total_tokens || 0),\n };\n}\n\nfunction isDeepSeekQuotaError(error) {\n const message = String(error?.message || error || '');\n return /额度|insufficient\\s+balance|balance/i.test(message);\n}\n\nfunction publicAiErrorMessage(error) {\n if (isDeepSeekQuotaError(error)) {\n return '大乘 AI 服务额度暂不可用,请联系管理员充值 DeepSeek 账户后重试。';\n }\n return error?.message || 'Internal Server Error';\n}\n\nfunction responsesPayload({\n responseId,\n itemId,\n status,\n model,\n text = '',\n usage = null,\n error = null,\n}) {\n return {\n id: responseId,\n object: 'response',\n status,\n model,\n output:\n status === 'completed'\n ? [\n {\n id: itemId,\n type: 'message',\n role: 'assistant',\n status: 'completed',\n content: [{ type: 'output_text', text }],\n },\n ]\n : [],\n ...(usage ? { usage: responseUsage(usage) } : {}),\n ...(error\n ? {\n error: {\n message: error.message || String(error),\n type: 'server_error',\n },\n }\n : {}),\n };\n}\n\nasync function callCodexSdkDeepSeek(messages, callbacks = {}) {\n const codexRuntime = createCodexDeepSeekRuntime();\n if (!codexRuntime.enabled) {\n const error = new Error(codexRuntime.reason || 'Codex SDK chat is not enabled');\n error.statusCode = 503;\n throw error;\n }\n\n const { Codex } = await import('@openai/codex-sdk');\n const codex = new Codex(codexRuntime.options);\n const thread = codex.startThread(codexRuntime.threadOptions);\n const prompt = codexPromptFromMessages(messages);\n\n if (callbacks.onToken || callbacks.onStep) {\n const { events } = await thread.runStreamed(prompt, { signal: callbacks.signal });\n let finalResponse = '';\n let usage = null;\n\n for await (const event of events) {\n if (event.type === 'item.completed') {\n const item = event.item;\n if (item.type === 'agent_message') {\n finalResponse = item.text || finalResponse;\n callbacks.onToken?.(item.text || '');\n } else if (item.type === 'todo_list') {\n callbacks.onStep?.({\n title: 'Codex SDK 执行计划',\n message: item.items.map((todo) => `${todo.completed ? '✓' : '•'} ${todo.text}`).join('\\n'),\n });\n } else if (item.type === 'command_execution') {\n callbacks.onStep?.({\n title: 'Codex SDK 执行命令',\n message: item.command,\n });\n } else if (item.type === 'mcp_tool_call') {\n callbacks.onStep?.({\n title: 'Codex SDK 调用工具',\n message: `${item.server}.${item.tool}`,\n });\n }\n } else if (event.type === 'turn.completed') {\n usage = event.usage;\n } else if (event.type === 'turn.failed') {\n throw new Error(event.error?.message || 'Codex SDK turn failed');\n } else if (event.type === 'error') {\n throw new Error(event.message || 'Codex SDK stream failed');\n }\n }\n\n if (!finalResponse.trim()) {\n const error = new Error('Codex SDK returned an empty response');\n error.statusCode = 502;\n throw error;\n }\n\n return {\n message: finalResponse.trim(),\n usage: {\n promptTokens: Number(usage?.input_tokens || 0),\n completionTokens: Number(usage?.output_tokens || 0),\n totalTokens: Number((usage?.input_tokens || 0) + (usage?.output_tokens || 0)),\n },\n };\n }\n\n const turn = await thread.run(prompt);\n if (!turn.finalResponse?.trim()) {\n const error = new Error('Codex SDK returned an empty response');\n error.statusCode = 502;\n throw error;\n }\n\n return {\n message: turn.finalResponse.trim(),\n usage: {\n promptTokens: Number(turn.usage?.input_tokens || 0),\n completionTokens: Number(turn.usage?.output_tokens || 0),\n totalTokens: Number((turn.usage?.input_tokens || 0) + (turn.usage?.output_tokens || 0)),\n },\n };\n}\n\napp.post(\n '/codex-deepseek/v1/responses',\n asyncHandler(async (req, res) => {\n const bearer = bearerToken(req);\n if (!deepseekApiKey || bearer !== deepseekApiKey) {\n res.status(401).json({\n error: { message: 'Unauthorized Codex DeepSeek adapter request' },\n });\n return;\n }\n\n const prompt = codexResponsesPrompt(req.body);\n if (!prompt) {\n res.status(400).json({ error: { message: 'Responses input is required' } });\n return;\n }\n\n const model = String(req.body?.model || deepseekModel);\n const responseId = `resp_${crypto.randomUUID().replaceAll('-', '')}`;\n const itemId = `msg_${crypto.randomUUID().replaceAll('-', '')}`;\n const wantsStream =\n req.body?.stream !== false ||\n String(req.get('accept') || '').includes('text/event-stream');\n const messages = [{ role: 'user', content: prompt }];\n\n if (!wantsStream) {\n const result = await callDeepSeek(messages);\n res.json(\n responsesPayload({\n responseId,\n itemId,\n status: 'completed',\n model,\n text: result.message,\n usage: result.usage,\n }),\n );\n return;\n }\n\n res.status(200);\n res.set({\n 'Content-Type': 'text/event-stream; charset=utf-8',\n 'Cache-Control': 'no-cache, no-transform',\n Connection: 'keep-alive',\n 'X-Accel-Buffering': 'no',\n });\n res.flushHeaders?.();\n\n writeResponsesEvent(res, 'response.created', {\n type: 'response.created',\n response: responsesPayload({\n responseId,\n itemId,\n status: 'in_progress',\n model,\n }),\n });\n writeResponsesEvent(res, 'response.output_item.added', {\n type: 'response.output_item.added',\n output_index: 0,\n item: {\n id: itemId,\n type: 'message',\n role: 'assistant',\n status: 'in_progress',\n content: [],\n },\n });\n writeResponsesEvent(res, 'response.content_part.added', {\n type: 'response.content_part.added',\n item_id: itemId,\n output_index: 0,\n content_index: 0,\n part: { type: 'output_text', text: '' },\n });\n\n let text = '';\n try {\n const result = await callDeepSeekStream(messages, {\n onToken: (delta) => {\n text += delta;\n writeResponsesEvent(res, 'response.output_text.delta', {\n type: 'response.output_text.delta',\n item_id: itemId,\n output_index: 0,\n content_index: 0,\n delta,\n });\n },\n });\n text = result.message || text;\n writeResponsesEvent(res, 'response.output_text.done', {\n type: 'response.output_text.done',\n item_id: itemId,\n output_index: 0,\n content_index: 0,\n text,\n });\n writeResponsesEvent(res, 'response.content_part.done', {\n type: 'response.content_part.done',\n item_id: itemId,\n output_index: 0,\n content_index: 0,\n part: { type: 'output_text', text },\n });\n writeResponsesEvent(res, 'response.output_item.done', {\n type: 'response.output_item.done',\n output_index: 0,\n item: {\n id: itemId,\n type: 'message',\n role: 'assistant',\n status: 'completed',\n content: [{ type: 'output_text', text }],\n },\n });\n writeResponsesEvent(res, 'response.completed', {\n type: 'response.completed',\n response: responsesPayload({\n responseId,\n itemId,\n status: 'completed',\n model,\n text,\n usage: result.usage,\n }),\n });\n res.end();\n } catch (error) {\n writeResponsesEvent(res, 'response.failed', {\n type: 'response.failed',\n response: responsesPayload({\n responseId,\n itemId,\n status: 'failed',\n model,\n text,\n error,\n }),\n });\n res.end();\n }\n }),\n);\n\napp.get('/health', (_req, res) => {\n const codexRuntime = createCodexDeepSeekRuntime();\n jsonResponse(res, 200, {\n status: 'ok',\n service: 'dacheng-ai-backend',\n provider: 'deepseek',\n model: deepseekModel,\n codexSdkAvailable: true,\n codexSdkChatEnabled: codexRuntime.enabled,\n codexSdkProvider: codexRuntime.provider,\n cbetaApiRoot,\n timestamp: nowIso(),\n });\n});\n\napp.post(\n '/api/ai/chat',\n asyncHandler(async (req, res) => {\n const message = safeUserText(req.body?.message);\n if (!message) {\n return jsonResponse(res, 400, { success: false, message: 'message is required' });\n }\n\n const user = await resolveUser(req, req.body || {});\n const estimated = estimateTokens(message) + 600;\n const budget = enforceTokenBudget(user, estimated);\n const createdAt = nowIso();\n const conversationId = safeUserText(req.body?.conversationId) || crypto.randomUUID();\n let conversation = statements.getConversation.get(conversationId, user.userId);\n const isNewConversation = !conversation;\n\n if (isNewConversation) {\n conversation = {\n id: conversationId,\n user_id: user.userId,\n title: titleFromMessage(message),\n };\n statements.insertConversation.run({\n id: conversationId,\n userId: user.userId,\n title: conversation.title,\n provider: 'deepseek',\n model: deepseekModel,\n createdAt,\n updatedAt: createdAt,\n });\n }\n\n statements.insertMessage.run({\n id: crypto.randomUUID(),\n conversationId,\n role: 'user',\n content: message,\n promptTokens: 0,\n completionTokens: 0,\n totalTokens: 0,\n createdAt,\n });\n\n const skillResult = await runResourceFinderSkill({ message, user });\n const skillContext = resourceContextMessage(skillResult);\n const historyRows = statements.listMessages.all(conversationId).slice(-14);\n const modelMessages = [\n { role: 'system', content: systemPrompt },\n ...(skillContext ? [{ role: 'system', content: skillContext }] : []),\n ...normalizeMessages(historyRows),\n ];\n\n const codexRuntime = createCodexDeepSeekRuntime();\n let provider = 'deepseek';\n let aiResult;\n try {\n if (codexRuntime.enabled) {\n provider = codexRuntime.provider;\n aiResult = await callCodexSdkDeepSeek(modelMessages);\n } else {\n aiResult = await callDeepSeek(modelMessages);\n }\n } catch (error) {\n if (!codexRuntime.enabled || isDeepSeekQuotaError(error)) throw error;\n logger.warn({ error: String(error) }, 'Codex SDK chat failed, falling back to DeepSeek direct');\n provider = 'deepseek';\n aiResult = await callDeepSeek(modelMessages);\n }\n const usage = {\n promptTokens: aiResult.usage.promptTokens || estimateTokens(JSON.stringify(modelMessages)),\n completionTokens: aiResult.usage.completionTokens || estimateTokens(aiResult.message),\n totalTokens:\n aiResult.usage.totalTokens ||\n estimateTokens(JSON.stringify(modelMessages)) + estimateTokens(aiResult.message),\n };\n recordUsage(user.userId, usage.totalTokens);\n\n const answeredAt = nowIso();\n statements.insertMessage.run({\n id: crypto.randomUUID(),\n conversationId,\n role: 'assistant',\n content: aiResult.message,\n promptTokens: usage.promptTokens,\n completionTokens: usage.completionTokens,\n totalTokens: usage.totalTokens,\n createdAt: answeredAt,\n });\n statements.updateConversation.run({\n id: conversationId,\n userId: user.userId,\n title: isNewConversation ? titleFromMessage(message) : conversation.title,\n updatedAt: answeredAt,\n });\n\n const latestUsage = usageFor(user.userId);\n const monthlyLimit = budget.limit;\n jsonResponse(res, 200, {\n success: true,\n conversationId,\n provider,\n model: deepseekModel,\n message: aiResult.message,\n usage: {\n ...usage,\n monthlyLimit,\n remainingTokens: Math.max(0, monthlyLimit - latestUsage.used),\n },\n });\n }),\n);\n\napp.post('/api/ai/chat/stream', async (req, res) => {\n sseHeaders(res);\n\n try {\n const message = safeUserText(req.body?.message);\n if (!message) {\n writeSse(res, 'error', { message: 'message is required' });\n res.end();\n return;\n }\n\n const user = await resolveUser(req, req.body || {});\n const estimated = estimateTokens(message) + 600;\n const budget = enforceTokenBudget(user, estimated);\n const createdAt = nowIso();\n const conversationId = safeUserText(req.body?.conversationId) || crypto.randomUUID();\n let conversation = statements.getConversation.get(conversationId, user.userId);\n const isNewConversation = !conversation;\n\n if (isNewConversation) {\n conversation = {\n id: conversationId,\n user_id: user.userId,\n title: titleFromMessage(message),\n };\n statements.insertConversation.run({\n id: conversationId,\n userId: user.userId,\n title: conversation.title,\n provider: 'deepseek',\n model: deepseekModel,\n createdAt,\n updatedAt: createdAt,\n });\n }\n\n statements.insertMessage.run({\n id: crypto.randomUUID(),\n conversationId,\n role: 'user',\n content: message,\n promptTokens: 0,\n completionTokens: 0,\n totalTokens: 0,\n createdAt,\n });\n\n const codexRuntime = createCodexDeepSeekRuntime();\n let provider = codexRuntime.enabled ? codexRuntime.provider : 'deepseek';\n writeSse(res, 'meta', {\n conversationId,\n provider,\n model: deepseekModel,\n });\n writeSse(res, 'step', {\n title: '连接后端 AI',\n message: '大乘后端已直连 VPS,并使用 DeepSeek OpenAI-compatible API。',\n });\n\n const skillResult = await runResourceFinderSkill({\n message,\n user,\n onStep: (step) => writeSse(res, 'step', step),\n });\n const skillContext = resourceContextMessage(skillResult);\n const historyRows = statements.listMessages.all(conversationId).slice(-14);\n const modelMessages = [\n { role: 'system', content: systemPrompt },\n ...(skillContext ? [{ role: 'system', content: skillContext }] : []),\n ...normalizeMessages(historyRows),\n ];\n\n let aiResult;\n try {\n if (codexRuntime.enabled) {\n aiResult = await callCodexSdkDeepSeek(modelMessages, {\n onToken: (token) => writeSse(res, 'delta', { text: token }),\n onStep: (step) => writeSse(res, 'step', step),\n });\n } else {\n aiResult = await callDeepSeekStream(modelMessages, {\n onToken: (token) => writeSse(res, 'delta', { text: token }),\n });\n }\n } catch (error) {\n if (!codexRuntime.enabled || isDeepSeekQuotaError(error)) throw error;\n logger.warn({ error: String(error) }, 'Codex SDK streaming chat failed, falling back to DeepSeek direct');\n provider = 'deepseek';\n writeSse(res, 'step', {\n title: 'Codex SDK 暂不可用',\n message: '已自动回退为 DeepSeek 直连流式返回。',\n });\n aiResult = await callDeepSeekStream(modelMessages, {\n onToken: (token) => writeSse(res, 'delta', { text: token }),\n });\n }\n\n const usage = {\n promptTokens: aiResult.usage.promptTokens || estimateTokens(JSON.stringify(modelMessages)),\n completionTokens: aiResult.usage.completionTokens || estimateTokens(aiResult.message),\n totalTokens:\n aiResult.usage.totalTokens ||\n estimateTokens(JSON.stringify(modelMessages)) + estimateTokens(aiResult.message),\n };\n recordUsage(user.userId, usage.totalTokens);\n\n const answeredAt = nowIso();\n statements.insertMessage.run({\n id: crypto.randomUUID(),\n conversationId,\n role: 'assistant',\n content: aiResult.message,\n promptTokens: usage.promptTokens,\n completionTokens: usage.completionTokens,\n totalTokens: usage.totalTokens,\n createdAt: answeredAt,\n });\n statements.updateConversation.run({\n id: conversationId,\n userId: user.userId,\n title: isNewConversation ? titleFromMessage(message) : conversation.title,\n updatedAt: answeredAt,\n });\n\n const latestUsage = usageFor(user.userId);\n const monthlyLimit = budget.limit;\n writeSse(res, 'done', {\n conversationId,\n provider,\n model: deepseekModel,\n message: aiResult.message,\n usage: {\n ...usage,\n monthlyLimit,\n remainingTokens: Math.max(0, monthlyLimit - latestUsage.used),\n },\n });\n res.end();\n } catch (error) {\n logger.error({ error: error.stack || String(error) }, 'streaming chat failed');\n writeSse(res, 'error', {\n message: publicAiErrorMessage(error),\n details: error.details,\n });\n res.end();\n }\n});\n\napp.get(\n '/api/ai/conversations',\n asyncHandler(async (req, res) => {\n const user = await resolveUser(req, { username: req.query.username });\n const items = statements.listConversations.all(user.userId).map((item) => ({\n id: item.id,\n title: item.title,\n updatedAt: item.updatedAt,\n }));\n jsonResponse(res, 200, { success: true, items });\n }),\n);\n\napp.get(\n '/api/ai/conversations/:id',\n asyncHandler(async (req, res) => {\n const user = await resolveUser(req);\n const conversation = statements.getConversation.get(req.params.id, user.userId);\n if (!conversation) {\n return jsonResponse(res, 404, { success: false, message: 'conversation not found' });\n }\n const messages = statements.listMessages.all(req.params.id).map((item) => ({\n role: item.role,\n content: item.content,\n createdAt: item.createdAt,\n }));\n jsonResponse(res, 200, {\n success: true,\n conversation: {\n id: conversation.id,\n title: conversation.title,\n updatedAt: conversation.updated_at,\n },\n messages,\n });\n }),\n);\n\nfunction buildUrl(base, endpoint, params = {}) {\n const url = new URL(endpoint, `${base}/`);\n for (const [key, value] of Object.entries(params)) {\n if (Array.isArray(value)) {\n for (const item of value) {\n if (item !== undefined && item !== null && item !== '') {\n url.searchParams.append(key, String(item));\n }\n }\n } else if (value !== undefined && value !== null && value !== '') {\n url.searchParams.set(key, String(value));\n }\n }\n return url;\n}\n\nasync function fetchJson(url) {\n const response = await fetch(url, {\n headers: { Accept: 'application/json' },\n signal: AbortSignal.timeout(15_000),\n });\n if (!response.ok) {\n throw new Error(`Request failed: ${response.status}`);\n }\n return response.json();\n}\n\nfunction stripHtml(value) {\n return String(value || '')\n .replace(/<script[\\s\\S]*?<\\/script>/gi, ' ')\n .replace(/<style[\\s\\S]*?<\\/style>/gi, ' ')\n .replace(/<[^>]+>/g, ' ')\n .replace(/ /g, ' ')\n .replace(/</g, '<')\n .replace(/>/g, '>')\n .replace(/&/g, '&')\n .replace(/&#(\\d+);/g, (_match, code) => String.fromCharCode(Number(code)))\n .replace(/\\s+/g, ' ')\n .trim();\n}\n\nfunction cleanExtractedText(value) {\n let text = String(value || '')\n .replace(/\\b[A-Z]{1,3}\\d{2}n\\d{4,5}[a-z]?_p?\\d{4}[a-z]\\d{2}\\b/g, ' ')\n .replace(/\\uFFFD/g, '')\n .replace(/\\s+([,。;:!?、」』)》】])/g, '$1')\n .replace(/([「『《(【])\\s+/g, '$1');\n for (let i = 0; i < 3; i += 1) {\n text = text.replace(/([\\u3400-\\u9fff])\\s+([\\u3400-\\u9fff])/g, '$1$2');\n }\n return text.replace(/[ \\t]{2,}/g, ' ').trim();\n}\n\nfunction compactText(value, limit = 140) {\n const text = stripHtml(value);\n return text.length > limit ? `${text.slice(0, limit)}...` : text;\n}\n\nfunction collectSearchResults(data) {\n if (!data || typeof data !== 'object') return [];\n if (Array.isArray(data.results)) return data.results;\n if (Array.isArray(data?.results?.docs)) return data.results.docs;\n if (Array.isArray(data?.response?.docs)) return data.response.docs;\n if (Array.isArray(data.docs)) return data.docs;\n return [];\n}\n\nfunction firstText(item, keys) {\n for (const key of keys) {\n const value = item?.[key];\n if (Array.isArray(value)) {\n const first = value.map((entry) => safeUserText(String(entry))).find(Boolean);\n if (first) return first;\n }\n if (typeof value === 'string' && value.trim()) return value.trim();\n if (typeof value === 'number') return String(value);\n }\n return '';\n}\n\nfunction firstNumber(item, keys, fallback = undefined) {\n for (const key of keys) {\n const value = item?.[key];\n if (typeof value === 'number') return value;\n if (typeof value === 'string' && value.trim() && Number.isFinite(Number(value))) {\n return Number(value);\n }\n }\n return fallback;\n}\n\nasync function searchCbetaTitle(query, limit) {\n const url = buildUrl(cbetaApiRoot, 'search/title', { q: query, start: 0, rows: limit });\n const data = await fetchJson(url);\n return collectSearchResults(data).flatMap((item) => {\n const work = firstText(item, ['work', 'work_id', 'workId']);\n const title = firstText(item, ['content', 'title', 'work_title', 'workTitle']);\n if (!work || !title) return [];\n const juan = firstNumber(item, ['juan', 'juan_num', 'juanNum'], 1);\n return [\n {\n id: `cbeta-title-${work}-${juan}`,\n title,\n sourceName: 'CBETA 佛典',\n url: `cbeta:${work}:${juan}`,\n snippet: firstText(item, ['byline', 'creators', 'time_dynasty']) || '可下载经文正文并用于全球法布施。',\n resourceType: 'scripture',\n work,\n juan,\n },\n ];\n });\n}\n\nasync function searchCbetaContent(query, limit) {\n const endpoints = ['search/fulltext', 'search/content', 'search/all_in_one'];\n const settled = await Promise.allSettled(\n endpoints.map((endpoint) =>\n fetchJson(buildUrl(cbetaApiRoot, endpoint, { q: query, start: 0, rows: limit, field: 'content' })),\n ),\n );\n const results = [];\n for (const response of settled) {\n if (response.status !== 'fulfilled') continue;\n for (const item of collectSearchResults(response.value)) {\n const work = firstText(item, ['work', 'work_id', 'workId', 'sutra', 'id']);\n const snippet = compactText(firstText(item, ['kwic', 'highlight', 'snippet', 'content', 'text', 'body', 'p']));\n if (!work || !snippet) continue;\n const juan = firstNumber(item, ['juan', 'juan_num', 'juanNum', '卷'], 1);\n const title = firstText(item, ['title', 'work_title', 'workTitle', 'sutra_name', 'book', 'name']) || work;\n results.push({\n id: `cbeta-content-${work}-${juan}-${sha256(snippet).slice(0, 8)}`,\n title,\n sourceName: 'CBETA 佛典',\n url: `cbeta:${work}:${juan}`,\n snippet,\n resourceType: 'scripture',\n work,\n juan,\n });\n if (results.length >= limit) return results;\n }\n }\n return results;\n}\n\nfunction directUrlResource(query) {\n const url = safeUserText(query);\n let parsed;\n try {\n parsed = new URL(url);\n } catch {\n return null;\n }\n if (!['http:', 'https:'].includes(parsed.protocol)) return null;\n return {\n id: `url-${sha256(url).slice(0, 12)}`,\n title: parsed.hostname,\n sourceName: '网页资源',\n url,\n snippet: '直接下载并提取这个链接的可读正文。',\n resourceType: 'web',\n };\n}\n\nfunction looksLikeAudioQuery(query) {\n return /音乐|音樂|音频|音訊|mp3|audio|念诵|念誦|诵经|誦經|唱诵|唱誦|读诵|讀誦/.test(\n safeUserText(query).toLowerCase(),\n );\n}\n\nfunction archiveSearchTerm(query) {\n return (\n safeUserText(query)\n .replace(/下载|查找|寻找|找一?个|资源|音频|音訊|音乐|音樂|mp3|audio|念诵|念誦|诵经|誦經|唱诵|唱誦|读诵|讀誦/gi, ' ')\n .replace(/\\s+/g, ' ')\n .trim() || safeUserText(query)\n );\n}\n\nfunction solrPhrase(value) {\n return String(value).replace(/\\\\/g, '\\\\\\\\').replace(/\"/g, '\\\\\"');\n}\n\nasync function searchInternetArchiveAudio(query, limit) {\n if (!looksLikeAudioQuery(query)) return [];\n const term = archiveSearchTerm(query);\n if (!term) return [];\n const variants = Array.from(\n new Set([\n term,\n term.replaceAll('金刚经', '金剛經'),\n term.replaceAll('金剛經', '金刚经'),\n ].filter(Boolean)),\n );\n const phraseQuery = variants\n .flatMap((item) => [`title:\"${solrPhrase(item)}\"`, `description:\"${solrPhrase(item)}\"`])\n .join(' OR ');\n const url = buildUrl('https://archive.org', 'advancedsearch.php', {\n q: `(${phraseQuery}) AND mediatype:audio`,\n rows: Math.min(Math.max(limit, 1), 10),\n output: 'json',\n 'fl[]': ['identifier', 'title', 'description'],\n });\n const data = await fetchJson(url);\n const docs = Array.isArray(data?.response?.docs) ? data.response.docs : [];\n return docs\n .map((item) => {\n const identifier = firstText(item, ['identifier']);\n const title = firstText(item, ['title']) || identifier;\n if (!identifier || !title) return null;\n return {\n id: `ia-audio-${identifier}`,\n title,\n sourceName: 'Internet Archive 音频',\n url: `ia:${identifier}`,\n snippet: compactText(firstText(item, ['description'])) || '公开可访问的音频资源,将下载第一个可用 MP3 文件。',\n resourceType: 'audio',\n identifier,\n };\n })\n .filter(Boolean);\n}\n\nconst curatedCbetaWorks = [\n {\n work: 'T0235',\n juan: 1,\n title: '金剛般若波羅蜜經',\n keywords: ['金剛般若波羅蜜經', '金刚般若波罗蜜经', '金剛經', '金刚经'],\n snippet: '姚秦 鳩摩羅什譯。常用于读诵、共修与法布施的般若经典。',\n },\n {\n work: 'T0251',\n juan: 1,\n title: '般若波羅蜜多心經',\n keywords: ['般若波羅蜜多心經', '般若波罗蜜多心经', '心經', '心经'],\n snippet: '唐 玄奘譯。篇幅短,适合加入禅室功课本与日常持诵。',\n },\n {\n work: 'T0262',\n juan: 1,\n title: '妙法蓮華經',\n keywords: ['妙法蓮華經', '妙法莲华经', '法華經', '法华经'],\n snippet: '姚秦 鳩摩羅什譯。可按卷下载正文,用于长期功课与分享。',\n },\n {\n work: 'T0279',\n juan: 1,\n title: '大方廣佛華嚴經',\n keywords: ['大方廣佛華嚴經', '大方广佛华严经', '華嚴經', '华严经'],\n snippet: '唐 實叉難陀譯。可按卷下载正文,用于系统研读与法布施。',\n },\n {\n work: 'T0360',\n juan: 1,\n title: '佛說無量壽經',\n keywords: ['佛說無量壽經', '佛说无量寿经', '無量壽經', '无量寿经'],\n snippet: '净土经典。可下载正文,用于净土功课本与全球法布施。',\n },\n {\n work: 'T0365',\n juan: 1,\n title: '佛說觀無量壽佛經',\n keywords: ['佛說觀無量壽佛經', '佛说观无量寿佛经', '觀無量壽經', '观无量寿经'],\n snippet: '净土三经之一。适合查找下载后加入净土功课。',\n },\n {\n work: 'T0366',\n juan: 1,\n title: '佛說阿彌陀經',\n keywords: ['佛說阿彌陀經', '佛说阿弥陀经', '阿彌陀經', '阿弥陀经'],\n snippet: '姚秦 鳩摩羅什譯。篇幅适中,适合共修、持诵与分享。',\n },\n {\n work: 'T0475',\n juan: 1,\n title: '維摩詰所說經',\n keywords: ['維摩詰所說經', '维摩诘所说经', '維摩詰經', '维摩诘经'],\n snippet: '姚秦 鳩摩羅什譯。大乘经典,可按卷下载用于研读。',\n },\n {\n work: 'T0676',\n juan: 1,\n title: '解深密經',\n keywords: ['解深密經', '解深密经'],\n snippet: '唯识相关大乘经典。适合研读、整理笔记与法布施。',\n },\n {\n work: 'T1666',\n juan: 1,\n title: '大乘起信論',\n keywords: ['大乘起信論', '大乘起信论', '起信論', '起信论'],\n snippet: '大乘论典。适合下载后作为研习功课资料。',\n },\n];\n\nfunction localCbetaResults(query, limit) {\n const normalizedQuery = safeUserText(query).toLowerCase();\n if (!normalizedQuery) return [];\n return curatedCbetaWorks\n .filter((item) =>\n item.keywords.some((keyword) => {\n const normalizedKeyword = keyword.toLowerCase();\n return normalizedKeyword.includes(normalizedQuery) || normalizedQuery.includes(normalizedKeyword);\n }),\n )\n .slice(0, limit)\n .map((item) => ({\n id: `cbeta-curated-${item.work}-${item.juan}`,\n title: item.title,\n sourceName: 'CBETA 佛典',\n url: `cbeta:${item.work}:${item.juan}`,\n snippet: item.snippet,\n resourceType: 'scripture',\n work: item.work,\n juan: item.juan,\n }));\n}\n\nfunction uniqueResources(items, limit) {\n const seen = new Set();\n const results = [];\n for (const item of items) {\n const key = `${item.url}:${item.title}`;\n if (seen.has(key)) continue;\n seen.add(key);\n results.push(item);\n if (results.length >= limit) break;\n }\n return results;\n}\n\nasync function findResourceCandidates(query, limit) {\n const direct = directUrlResource(query);\n const results = [];\n if (direct) results.push(direct);\n try {\n results.push(...(await searchInternetArchiveAudio(query, limit)));\n } catch (error) {\n logger.warn({ error: String(error), query }, 'Internet Archive audio search skipped');\n }\n results.push(...localCbetaResults(query, limit));\n\n const [titleResults, contentResults] = await Promise.allSettled([\n searchCbetaTitle(query, limit),\n searchCbetaContent(query, limit),\n ]);\n if (titleResults.status === 'fulfilled') results.push(...titleResults.value);\n if (contentResults.status === 'fulfilled') results.push(...contentResults.value);\n\n return uniqueResources(results, limit);\n}\n\nfunction looksLikeResourceTask(message) {\n const text = safeUserText(message).toLowerCase();\n if (!text) return false;\n return /下载|查找|寻找|找一?个|资源|經文|经文|佛经|佛經|仪轨|儀軌|音乐|音樂|mp3|audio|念诵|念誦|诵经|誦經/.test(text);\n}\n\nfunction extractResourceQuery(message) {\n const text = safeUserText(message)\n .replace(/请|請|帮我|幫我|麻烦|麻煩|可以|能不能|需要|我要|想要/g, ' ')\n .replace(/下载|下載|查找|寻找|尋找|找一个|找一個|找|资源|資源|并|並|然后|然後|用于|用於|全球法布施|加入功课本|加入功課本/g, ' ')\n .replace(/\\s+/g, ' ')\n .trim();\n return text || safeUserText(message);\n}\n\nfunction resourceContextMessage(skillResult) {\n if (!skillResult) return '';\n const lines = [\n `后端已调用 skill: ${skillResult.skillName}`,\n `执行目标: ${skillResult.query}`,\n ];\n if (skillResult.selected) {\n lines.push(`候选资源: ${skillResult.selected.title}`);\n lines.push(`来源: ${skillResult.selected.sourceName}`);\n lines.push(`资源地址: ${skillResult.selected.url}`);\n }\n if (skillResult.downloaded) {\n lines.push(`下载标题: ${skillResult.downloaded.title}`);\n lines.push(`下载文件: ${skillResult.downloaded.fileName}`);\n const text = safeUserText(skillResult.downloaded.contentText);\n if (text) {\n lines.push('下载正文摘录:');\n lines.push(text.slice(0, 8000));\n }\n }\n return lines.join('\\n');\n}\n\nasync function runResourceFinderSkill({ message, user, onStep }) {\n if (!looksLikeResourceTask(message)) return null;\n\n const skillName = 'resource-finder-downloader';\n const query = extractResourceQuery(message);\n const audioTask = looksLikeAudioQuery(message) || looksLikeAudioQuery(query);\n onStep?.({\n type: 'skill.step',\n skillName,\n stage: 'start',\n title: '调用资源查找下载 skill',\n message: `识别到资源任务: ${query}`,\n });\n\n onStep?.({\n type: 'skill.step',\n skillName,\n stage: 'search',\n title: '搜索可分享资源',\n message: audioTask\n ? '优先检索公开可访问音频资源,失败时再回落到佛典正文资源。'\n : '优先检索 CBETA 佛典正文接口,搜索索引不可用时使用内置经典映射。',\n });\n const candidates = await findResourceCandidates(query, 8);\n if (candidates.length === 0) {\n onStep?.({\n type: 'skill.step',\n skillName,\n stage: 'empty',\n title: '没有找到可下载资源',\n message: '后端没有找到明确可下载且可分享的资源。',\n });\n return {\n skillName,\n query,\n candidates: [],\n selected: null,\n downloaded: null,\n };\n }\n\n const selected = candidates[0];\n onStep?.({\n type: 'skill.step',\n skillName,\n stage: 'select',\n title: '选择可信候选资源',\n message: `${selected.title} / ${selected.sourceName}`,\n });\n\n onStep?.({\n type: 'skill.step',\n skillName,\n stage: 'download',\n title: '下载并提取资源',\n message: selected.url,\n });\n const cbeta = parseCbetaResource(selected);\n const internetArchive = parseInternetArchiveResource(selected);\n const downloaded = cbeta\n ? await downloadCbetaResource({ ...selected, ...cbeta })\n : internetArchive\n ? await downloadInternetArchiveAudio({ ...selected, ...internetArchive })\n : await downloadWebResource(selected);\n const persisted = persistDownloadedResource(user, downloaded);\n\n onStep?.({\n type: 'skill.step',\n skillName,\n stage: 'done',\n title: '资源已准备完成',\n message: downloaded.fileName,\n });\n\n return {\n skillName,\n query,\n candidates,\n selected,\n downloaded: {\n ...downloaded,\n downloadId: persisted.id,\n },\n };\n}\n\napp.post(\n '/api/resources/search',\n asyncHandler(async (req, res) => {\n const query = safeUserText(req.body?.query);\n const limit = Math.min(Math.max(Number(req.body?.limit || 12), 1), 20);\n if (query.length < 2) {\n return jsonResponse(res, 400, { success: false, message: 'query is required' });\n }\n\n const items = await findResourceCandidates(query, limit);\n\n jsonResponse(res, 200, {\n success: true,\n source: 'resource-index',\n items,\n });\n }),\n);\n\nfunction parseCbetaResource(input) {\n const work = safeUserText(input.work);\n const juan = Number(input.juan || 1);\n if (work) return { work, juan: Number.isFinite(juan) ? juan : 1 };\n\n const url = safeUserText(input.url);\n const match = /^cbeta:([^:]+):(\\d+)$/i.exec(url);\n if (!match) return null;\n return {\n work: match[1].toUpperCase(),\n juan: Number(match[2] || 1),\n };\n}\n\nfunction parseInternetArchiveResource(input) {\n const url = safeUserText(input.url);\n const match = /^ia:([^:]+)$/i.exec(url);\n const identifier = match?.[1] || safeUserText(input.identifier);\n if (!identifier) return null;\n return { identifier };\n}\n\nfunction safeFileName(value) {\n const normalized = String(value || 'dharma-resource')\n .replace(/[\\\\/:*?\"<>|]+/g, '-')\n .replace(/\\s+/g, '-')\n .replace(/-+/g, '-')\n .replace(/^-|-$/g, '')\n .slice(0, 80);\n return normalized || 'dharma-resource';\n}\n\nfunction extractCbetaHtml(data) {\n const first = Array.isArray(data?.results) ? data.results[0] : null;\n if (typeof first === 'string') return first;\n if (first && typeof first === 'object' && typeof first.html === 'string') return first.html;\n if (typeof data?.html === 'string') return data.html;\n return '';\n}\n\nasync function downloadCbetaResource(resource) {\n const url = buildUrl(cbetaApiRoot, 'juans', {\n work: resource.work,\n juan: resource.juan,\n work_info: 1,\n toc: 1,\n });\n const data = await fetchJson(url);\n const workInfo = data.work_info || {};\n const title = safeUserText(workInfo.title) || safeUserText(resource.title) || resource.work;\n const html = extractCbetaHtml(data);\n const text = cleanExtractedText(stripHtml(html));\n if (!text) throw new Error('CBETA resource returned empty content');\n return {\n title,\n sourceName: 'CBETA 佛典',\n url: `cbeta:${resource.work}:${resource.juan}`,\n contentText: text.slice(0, maxResourceTextChars),\n fileName: `${resource.work}_${resource.juan}_${safeFileName(title)}.txt`,\n };\n}\n\nasync function downloadInternetArchiveAudio(resource) {\n const identifier = safeUserText(resource.identifier);\n if (!identifier) throw new Error('Internet Archive identifier is required');\n const metadata = await fetchJson(`https://archive.org/metadata/${encodeURIComponent(identifier)}`);\n const title = safeUserText(metadata?.metadata?.title) || safeUserText(resource.title) || identifier;\n const files = Array.isArray(metadata?.files) ? metadata.files : [];\n const audioFile = files.find((file) => {\n const name = safeUserText(file?.name);\n const format = safeUserText(file?.format).toLowerCase();\n return /\\.(mp3|m4a|ogg)$/i.test(name) || format.includes('mp3');\n });\n if (!audioFile?.name) {\n throw new Error('Internet Archive audio item has no downloadable audio file');\n }\n const sourceFileName = audioFile.name;\n const fileUrl = `https://archive.org/download/${encodeURIComponent(identifier)}/${sourceFileName\n .split('/')\n .map((part) => encodeURIComponent(part))\n .join('/')}`;\n const response = await fetch(fileUrl, {\n headers: {\n Accept: 'audio/mpeg, audio/*;q=0.9, */*;q=0.5',\n 'User-Agent': 'DachengResourceDownloader/1.0 (+https://ombhrum.com)',\n },\n signal: AbortSignal.timeout(60_000),\n });\n if (!response.ok) {\n throw new Error(`Internet Archive audio download failed: ${response.status}`);\n }\n const binaryContent = Buffer.from(await response.arrayBuffer());\n if (!binaryContent.length) throw new Error('Internet Archive audio returned empty content');\n const sizeMb = (binaryContent.length / 1024 / 1024).toFixed(1);\n return {\n title,\n sourceName: 'Internet Archive 音频',\n url: fileUrl,\n contentText: `已下载音频资源: ${title}\\n文件名: ${sourceFileName}\\n大小: ${sizeMb} MB\\n来源: ${fileUrl}`,\n binaryContent,\n fileName: safeFileName(sourceFileName),\n };\n}\n\nasync function downloadWebResource(input) {\n const url = safeUserText(input.url);\n const parsed = new URL(url);\n if (!['http:', 'https:'].includes(parsed.protocol)) {\n throw new Error('Only http/https resources can be downloaded');\n }\n\n const response = await fetch(parsed, {\n headers: {\n Accept: 'text/html, text/plain, application/xhtml+xml, application/xml;q=0.8, */*;q=0.5',\n 'User-Agent': 'DachengResourceDownloader/1.0 (+https://ombhrum.com)',\n },\n signal: AbortSignal.timeout(25_000),\n });\n if (!response.ok) throw new Error(`Resource download failed: ${response.status}`);\n const contentType = response.headers.get('content-type') || '';\n const body = await response.text();\n const title = safeUserText(input.title) || parsed.hostname;\n const contentText = contentType.includes('html') ? cleanExtractedText(stripHtml(body)) : body.trim();\n if (!contentText) throw new Error('Resource returned empty content');\n return {\n title,\n sourceName: safeUserText(input.sourceName) || parsed.hostname,\n url,\n contentText: contentText.slice(0, maxResourceTextChars),\n fileName: `${safeFileName(title)}.txt`,\n };\n}\n\nfunction persistDownloadedResource(user, content) {\n const id = crypto.randomUUID();\n const fileName = `${id}-${safeFileName(content.fileName)}`;\n const filePath = path.join(resourcesDir, fileName);\n if (content.binaryContent) {\n fs.writeFileSync(filePath, content.binaryContent);\n } else {\n fs.writeFileSync(filePath, content.contentText, 'utf8');\n }\n statements.insertResourceDownload.run({\n id,\n userId: user.userId,\n title: content.title,\n sourceName: content.sourceName,\n sourceUrl: content.url,\n fileName: content.fileName,\n filePath,\n createdAt: nowIso(),\n });\n return { id, fileName, filePath };\n}\n\napp.post(\n '/api/resources/download',\n asyncHandler(async (req, res) => {\n const user = await resolveUser(req, req.body || {});\n const cbeta = parseCbetaResource(req.body || {});\n const internetArchive = parseInternetArchiveResource(req.body || {});\n const content = cbeta\n ? await downloadCbetaResource({ ...req.body, ...cbeta })\n : internetArchive\n ? await downloadInternetArchiveAudio({ ...req.body, ...internetArchive })\n : await downloadWebResource(req.body || {});\n const persisted = persistDownloadedResource(user, content);\n const { binaryContent: _binaryContent, ...publicContent } = content;\n jsonResponse(res, 200, {\n success: true,\n downloadId: persisted.id,\n ...publicContent,\n });\n }),\n);\n\napp.post(\n '/api/codex/resource-task',\n asyncHandler(async (req, res) => {\n if (process.env.ENABLE_CODEX_SDK !== 'true') {\n return jsonResponse(res, 403, {\n success: false,\n message: 'Codex SDK task runner is disabled. Set ENABLE_CODEX_SDK=true on the server to enable it.',\n });\n }\n const prompt = safeUserText(req.body?.prompt);\n if (!prompt) {\n return jsonResponse(res, 400, { success: false, message: 'prompt is required' });\n }\n const { Codex } = await import('@openai/codex-sdk');\n const codexRuntime = createCodexDeepSeekRuntime();\n const codex = new Codex(codexRuntime.enabled ? codexRuntime.options : {});\n const thread = codex.startThread(\n codexRuntime.enabled\n ? codexRuntime.threadOptions\n : { skipGitRepoCheck: true, networkAccessEnabled: true },\n );\n const result = await thread.run(prompt);\n jsonResponse(res, 200, {\n success: true,\n threadId: thread.id,\n provider: codexRuntime.provider,\n model: deepseekModel,\n result,\n });\n }),\n);\n\napp.use((error, _req, res, _next) => {\n const status = Number(error.statusCode || error.status || 500);\n const safeStatus = status >= 400 && status < 600 ? status : 500;\n logger.error({ error: error.stack || String(error) }, 'request failed');\n jsonResponse(res, safeStatus, {\n success: false,\n message: error.message || 'Internal Server Error',\n details: error.details,\n });\n});\n\nconst port = Number(process.env.PORT || 8788);\napp.listen(port, '0.0.0.0', () => {\n logger.info(\n {\n port,\n dbPath,\n dataDir,\n deepseekModel,\n cbetaApiRoot,\n },\n 'Dacheng AI backend started',\n );\n});\n"}, "b1acfa3a7a5cc0a4dbba10a87672aa919fcfe6e9": {"t": 1785757999.6073735, "n": 29686, "h": "208b853bd6009860", "c": "import crypto from 'node:crypto';\nimport fs from 'node:fs';\nimport path from 'node:path';\nimport process from 'node:process';\nimport { fileURLToPath } from 'node:url';\n\nimport Database from 'better-sqlite3';\nimport cors from 'cors';\nimport dotenv from 'dotenv';\nimport express from 'express';\nimport rateLimit from 'express-rate-limit';\nimport helmet from 'helmet';\nimport pino from 'pino';\n\ndotenv.config();\n\nconst __dirname = path.dirname(fileURLToPath(import.meta.url));\nconst rootDir = path.resolve(__dirname, '..');\nconst dataDir = process.env.DATA_DIR || path.join(rootDir, 'data');\nconst resourcesDir = path.join(dataDir, 'resources');\nconst dbPath = process.env.SQLITE_PATH || path.join(dataDir, 'dacheng-ai.sqlite');\n\nfs.mkdirSync(resourcesDir, { recursive: true });\n\nconst logger = pino({\n level: process.env.LOG_LEVEL || 'info',\n});\n\nconst app = express();\napp.disable('x-powered-by');\napp.set('trust proxy', Number(process.env.TRUST_PROXY_HOPS || 1));\napp.use(helmet());\nconst corsOrigin =\n !process.env.CORS_ORIGIN || process.env.CORS_ORIGIN.trim() === '*'\n ? true\n : process.env.CORS_ORIGIN.split(',').map((item) => item.trim()).filter(Boolean);\napp.use(cors({ origin: corsOrigin }));\napp.use(express.json({ limit: '1mb' }));\napp.use(\n rateLimit({\n windowMs: 60_000,\n limit: Number(process.env.RATE_LIMIT_PER_MINUTE || 90),\n standardHeaders: true,\n legacyHeaders: false,\n }),\n);\n\nconst db = new Database(dbPath);\ndb.pragma('journal_mode = WAL');\ndb.pragma('foreign_keys = ON');\n\ndb.exec(`\n CREATE TABLE IF NOT EXISTS conversations (\n id TEXT PRIMARY KEY,\n user_id TEXT NOT NULL,\n title TEXT NOT NULL,\n provider TEXT NOT NULL,\n model TEXT NOT NULL,\n created_at TEXT NOT NULL,\n updated_at TEXT NOT NULL\n );\n\n CREATE INDEX IF NOT EXISTS idx_conversations_user_updated\n ON conversations (user_id, updated_at DESC);\n\n CREATE TABLE IF NOT EXISTS messages (\n id TEXT PRIMARY KEY,\n conversation_id TEXT NOT NULL,\n role TEXT NOT NULL,\n content TEXT NOT NULL,\n prompt_tokens INTEGER NOT NULL DEFAULT 0,\n completion_tokens INTEGER NOT NULL DEFAULT 0,\n total_tokens INTEGER NOT NULL DEFAULT 0,\n created_at TEXT NOT NULL,\n FOREIGN KEY (conversation_id) REFERENCES conversations(id) ON DELETE CASCADE\n );\n\n CREATE INDEX IF NOT EXISTS idx_messages_conversation_created\n ON messages (conversation_id, created_at ASC);\n\n CREATE TABLE IF NOT EXISTS usage_months (\n user_id TEXT NOT NULL,\n month TEXT NOT NULL,\n total_tokens INTEGER NOT NULL DEFAULT 0,\n updated_at TEXT NOT NULL,\n PRIMARY KEY (user_id, month)\n );\n\n CREATE TABLE IF NOT EXISTS resource_downloads (\n id TEXT PRIMARY KEY,\n user_id TEXT NOT NULL,\n title TEXT NOT NULL,\n source_name TEXT NOT NULL,\n source_url TEXT NOT NULL,\n file_name TEXT NOT NULL,\n file_path TEXT NOT NULL,\n created_at TEXT NOT NULL\n );\n`);\n\nconst statements = {\n insertConversation: db.prepare(`\n INSERT INTO conversations (id, user_id, title, provider, model, created_at, updated_at)\n VALUES (@id, @userId, @title, @provider, @model, @createdAt, @updatedAt)\n `),\n updateConversation: db.prepare(`\n UPDATE conversations\n SET title = @title, updated_at = @updatedAt\n WHERE id = @id AND user_id = @userId\n `),\n getConversation: db.prepare(`\n SELECT * FROM conversations WHERE id = ? AND user_id = ? LIMIT 1\n `),\n listConversations: db.prepare(`\n SELECT id, title, updated_at AS updatedAt\n FROM conversations\n WHERE user_id = ?\n ORDER BY updated_at DESC\n LIMIT 30\n `),\n insertMessage: db.prepare(`\n INSERT INTO messages (\n id, conversation_id, role, content,\n prompt_tokens, completion_tokens, total_tokens, created_at\n )\n VALUES (\n @id, @conversationId, @role, @content,\n @promptTokens, @completionTokens, @totalTokens, @createdAt\n )\n `),\n listMessages: db.prepare(`\n SELECT role, content, prompt_tokens AS promptTokens,\n completion_tokens AS completionTokens, total_tokens AS totalTokens,\n created_at AS createdAt\n FROM messages\n WHERE conversation_id = ?\n ORDER BY created_at ASC\n `),\n getUsage: db.prepare(`\n SELECT total_tokens AS totalTokens FROM usage_months WHERE user_id = ? AND month = ?\n `),\n upsertUsage: db.prepare(`\n INSERT INTO usage_months (user_id, month, total_tokens, updated_at)\n VALUES (@userId, @month, @totalTokens, @updatedAt)\n ON CONFLICT(user_id, month)\n DO UPDATE SET total_tokens = usage_months.total_tokens + excluded.total_tokens,\n updated_at = excluded.updated_at\n `),\n insertResourceDownload: db.prepare(`\n INSERT INTO resource_downloads (\n id, user_id, title, source_name, source_url, file_name, file_path, created_at\n )\n VALUES (\n @id, @userId, @title, @sourceName, @sourceUrl, @fileName, @filePath, @createdAt\n )\n `),\n};\n\nconst deepseekApiKey = process.env.DEEPSEEK_API_KEY || '';\nconst deepseekBaseUrl = (process.env.DEEPSEEK_BASE_URL || 'https://api.deepseek.com').replace(/\\/+$/, '');\nconst deepseekModel = process.env.DEEPSEEK_MODEL || 'deepseek-chat';\nconst cbetaApiRoot = (process.env.CBETA_API_ROOT || 'http://144.24.17.21.sslip.io:3000').replace(/\\/+$/, '');\nconst fabushiApiBaseUrl = (process.env.FABUSHI_API_BASE_URL || 'https://api.ombhrum.com').replace(/\\/+$/, '');\nconst memberMonthlyLimit = Number(process.env.MEMBER_MONTHLY_TOKEN_LIMIT || 1_000_000);\nconst freeMonthlyLimit = Number(process.env.FREE_MONTHLY_TOKEN_LIMIT || 50_000);\nconst maxResourceTextChars = Number(process.env.MAX_RESOURCE_TEXT_CHARS || 80_000);\n\nconst systemPrompt = [\n '你是“大乘”App 的 AI 助手。',\n '你的核心任务是帮助用户查找、整理、理解并全球法布施合法可分享的佛法资源。',\n '回答要庄重、简洁、可执行;涉及经典、仪轨或资源时,提醒用户尊重版权、来源和当地法规。',\n '如果用户想找资源,优先建议使用 + 菜单里的“查找下载资源”。',\n].join('\\n');\n\nfunction nowIso() {\n return new Date().toISOString();\n}\n\nfunction monthKey(date = new Date()) {\n return `${date.getUTCFullYear()}-${String(date.getUTCMonth() + 1).padStart(2, '0')}`;\n}\n\nfunction jsonResponse(res, status, payload) {\n res.status(status).json(payload);\n}\n\nfunction asyncHandler(fn) {\n return (req, res, next) => {\n Promise.resolve(fn(req, res, next)).catch(next);\n };\n}\n\nfunction bearerToken(req) {\n const header = req.get('Authorization') || '';\n if (!header.startsWith('Bearer ')) return '';\n return header.slice('Bearer '.length).trim();\n}\n\nfunction sha256(value) {\n return crypto.createHash('sha256').update(value).digest('hex');\n}\n\nfunction safeUserText(value, fallback = '') {\n if (typeof value !== 'string') return fallback;\n return value.replace(/\\u0000/g, '').trim();\n}\n\nfunction estimateTokens(text) {\n const normalized = String(text || '').trim();\n if (!normalized) return 0;\n return Math.max(1, Math.ceil(normalized.length / 3));\n}\n\nfunction normalizeMembershipPayload(payload) {\n if (!payload || typeof payload !== 'object') return false;\n const data = payload.data && typeof payload.data === 'object' ? payload.data : payload;\n return Boolean(\n data.isActive ||\n data.active ||\n data.hasPremiumAccess ||\n data.hasPremiumMembership ||\n data.isPremium ||\n data.status === 'active' ||\n data.type === 'paid' ||\n data.membershipType === 'paid',\n );\n}\n\nasync function checkRemoteMembership(token) {\n if (!token || !fabushiApiBaseUrl) return false;\n const endpoints = [\n '/api/stripe/membership-status',\n '/api/alipay/check-membership',\n '/api/auth/user-info',\n ];\n\n for (const endpoint of endpoints) {\n try {\n const response = await fetch(`${fabushiApiBaseUrl}${endpoint}`, {\n headers: {\n Accept: 'application/json',\n Authorization: `Bearer ${token}`,\n },\n signal: AbortSignal.timeout(8_000),\n });\n if (!response.ok) continue;\n const payload = await response.json();\n if (normalizeMembershipPayload(payload)) return true;\n } catch (error) {\n logger.debug({ endpoint, error: String(error) }, 'membership lookup skipped');\n }\n }\n\n return false;\n}\n\nasync function resolveUser(req, body = {}) {\n const token = bearerToken(req);\n const username = safeUserText(body.username || req.query.username);\n const tokenHash = token ? sha256(token).slice(0, 24) : '';\n const anonymousHash = sha256(`${req.ip}|${req.get('User-Agent') || ''}`).slice(0, 24);\n const userId = username\n ? `user:${username}`\n : tokenHash\n ? `token:${tokenHash}`\n : `anon:${anonymousHash}`;\n const remoteMember = await checkRemoteMembership(token);\n const memberHint = body.clientMembershipHint === true;\n return {\n userId,\n username,\n tokenHash,\n isMember: remoteMember || memberHint,\n };\n}\n\nfunction usageFor(userId) {\n const month = monthKey();\n const row = statements.getUsage.get(userId, month);\n return {\n month,\n used: Number(row?.totalTokens || 0),\n };\n}\n\nfunction enforceTokenBudget(user, estimatedTokensForRequest) {\n const limit = user.isMember ? memberMonthlyLimit : freeMonthlyLimit;\n const usage = usageFor(user.userId);\n if (usage.used + estimatedTokensForRequest > limit) {\n const error = new Error('本月 AI token 额度已不足');\n error.statusCode = 429;\n error.details = {\n monthlyLimit: limit,\n usedTokens: usage.used,\n remainingTokens: Math.max(0, limit - usage.used),\n };\n throw error;\n }\n return { limit, usage };\n}\n\nfunction recordUsage(userId, totalTokens) {\n if (!totalTokens || totalTokens < 1) return;\n statements.upsertUsage.run({\n userId,\n month: monthKey(),\n totalTokens,\n updatedAt: nowIso(),\n });\n}\n\nfunction titleFromMessage(message) {\n const compact = message.replace(/\\s+/g, ' ').trim();\n if (!compact) return '新对话';\n return compact.length > 22 ? `${compact.slice(0, 22)}...` : compact;\n}\n\nfunction normalizeMessages(rows) {\n return rows.map((row) => ({\n role: row.role,\n content: row.content,\n }));\n}\n\nasync function callDeepSeek(messages) {\n if (!deepseekApiKey) {\n const error = new Error('DeepSeek API key is not configured');\n error.statusCode = 503;\n throw error;\n }\n\n const response = await fetch(`${deepseekBaseUrl}/chat/completions`, {\n method: 'POST',\n headers: {\n Accept: 'application/json',\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${deepseekApiKey}`,\n },\n body: JSON.stringify({\n model: deepseekModel,\n messages,\n temperature: 0.4,\n max_tokens: 1400,\n stream: false,\n }),\n signal: AbortSignal.timeout(60_000),\n });\n\n const bodyText = await response.text();\n let payload;\n try {\n payload = bodyText ? JSON.parse(bodyText) : {};\n } catch {\n payload = { error: bodyText };\n }\n\n if (!response.ok) {\n const upstreamMessage = payload?.error?.message || payload?.message || '';\n const friendlyMessage =\n response.status === 402 || /insufficient\\s+balance/i.test(upstreamMessage)\n ? '大乘 AI 服务额度暂不可用,请联系管理员充值 DeepSeek 账户后重试。'\n : upstreamMessage || `DeepSeek request failed: ${response.status}`;\n const error = new Error(friendlyMessage);\n error.statusCode = response.status;\n throw error;\n }\n\n const message = payload?.choices?.[0]?.message?.content?.trim();\n if (!message) {\n const error = new Error('DeepSeek returned an empty response');\n error.statusCode = 502;\n throw error;\n }\n\n return {\n message,\n usage: {\n promptTokens: Number(payload?.usage?.prompt_tokens || 0),\n completionTokens: Number(payload?.usage?.completion_tokens || 0),\n totalTokens: Number(payload?.usage?.total_tokens || 0),\n },\n };\n}\n\napp.get('/health', (_req, res) => {\n jsonResponse(res, 200, {\n status: 'ok',\n service: 'dacheng-ai-backend',\n provider: 'deepseek',\n model: deepseekModel,\n codexSdkAvailable: true,\n timestamp: nowIso(),\n });\n});\n\napp.post(\n '/api/ai/chat',\n asyncHandler(async (req, res) => {\n const message = safeUserText(req.body?.message);\n if (!message) {\n return jsonResponse(res, 400, { success: false, message: 'message is required' });\n }\n\n const user = await resolveUser(req, req.body || {});\n const estimated = estimateTokens(message) + 600;\n const budget = enforceTokenBudget(user, estimated);\n const createdAt = nowIso();\n const conversationId = safeUserText(req.body?.conversationId) || crypto.randomUUID();\n let conversation = statements.getConversation.get(conversationId, user.userId);\n const isNewConversation = !conversation;\n\n if (isNewConversation) {\n conversation = {\n id: conversationId,\n user_id: user.userId,\n title: titleFromMessage(message),\n };\n statements.insertConversation.run({\n id: conversationId,\n userId: user.userId,\n title: conversation.title,\n provider: 'deepseek',\n model: deepseekModel,\n createdAt,\n updatedAt: createdAt,\n });\n }\n\n statements.insertMessage.run({\n id: crypto.randomUUID(),\n conversationId,\n role: 'user',\n content: message,\n promptTokens: 0,\n completionTokens: 0,\n totalTokens: 0,\n createdAt,\n });\n\n const historyRows = statements.listMessages.all(conversationId).slice(-14);\n const modelMessages = [\n { role: 'system', content: systemPrompt },\n ...normalizeMessages(historyRows),\n ];\n\n const aiResult = await callDeepSeek(modelMessages);\n const usage = {\n promptTokens: aiResult.usage.promptTokens || estimateTokens(JSON.stringify(modelMessages)),\n completionTokens: aiResult.usage.completionTokens || estimateTokens(aiResult.message),\n totalTokens:\n aiResult.usage.totalTokens ||\n estimateTokens(JSON.stringify(modelMessages)) + estimateTokens(aiResult.message),\n };\n recordUsage(user.userId, usage.totalTokens);\n\n const answeredAt = nowIso();\n statements.insertMessage.run({\n id: crypto.randomUUID(),\n conversationId,\n role: 'assistant',\n content: aiResult.message,\n promptTokens: usage.promptTokens,\n completionTokens: usage.completionTokens,\n totalTokens: usage.totalTokens,\n createdAt: answeredAt,\n });\n statements.updateConversation.run({\n id: conversationId,\n userId: user.userId,\n title: isNewConversation ? titleFromMessage(message) : conversation.title,\n updatedAt: answeredAt,\n });\n\n const latestUsage = usageFor(user.userId);\n const monthlyLimit = budget.limit;\n jsonResponse(res, 200, {\n success: true,\n conversationId,\n provider: 'deepseek',\n model: deepseekModel,\n message: aiResult.message,\n usage: {\n ...usage,\n monthlyLimit,\n remainingTokens: Math.max(0, monthlyLimit - latestUsage.used),\n },\n });\n }),\n);\n\napp.get(\n '/api/ai/conversations',\n asyncHandler(async (req, res) => {\n const user = await resolveUser(req, { username: req.query.username });\n const items = statements.listConversations.all(user.userId).map((item) => ({\n id: item.id,\n title: item.title,\n updatedAt: item.updatedAt,\n }));\n jsonResponse(res, 200, { success: true, items });\n }),\n);\n\napp.get(\n '/api/ai/conversations/:id',\n asyncHandler(async (req, res) => {\n const user = await resolveUser(req);\n const conversation = statements.getConversation.get(req.params.id, user.userId);\n if (!conversation) {\n return jsonResponse(res, 404, { success: false, message: 'conversation not found' });\n }\n const messages = statements.listMessages.all(req.params.id).map((item) => ({\n role: item.role,\n content: item.content,\n createdAt: item.createdAt,\n }));\n jsonResponse(res, 200, {\n success: true,\n conversation: {\n id: conversation.id,\n title: conversation.title,\n updatedAt: conversation.updated_at,\n },\n messages,\n });\n }),\n);\n\nfunction buildUrl(base, endpoint, params = {}) {\n const url = new URL(endpoint, `${base}/`);\n for (const [key, value] of Object.entries(params)) {\n if (value !== undefined && value !== null && value !== '') {\n url.searchParams.set(key, String(value));\n }\n }\n return url;\n}\n\nasync function fetchJson(url) {\n const response = await fetch(url, {\n headers: { Accept: 'application/json' },\n signal: AbortSignal.timeout(15_000),\n });\n if (!response.ok) {\n throw new Error(`Request failed: ${response.status}`);\n }\n return response.json();\n}\n\nfunction stripHtml(value) {\n return String(value || '')\n .replace(/<script[\\s\\S]*?<\\/script>/gi, ' ')\n .replace(/<style[\\s\\S]*?<\\/style>/gi, ' ')\n .replace(/<[^>]+>/g, ' ')\n .replace(/ /g, ' ')\n .replace(/</g, '<')\n .replace(/>/g, '>')\n .replace(/&/g, '&')\n .replace(/&#(\\d+);/g, (_match, code) => String.fromCharCode(Number(code)))\n .replace(/\\s+/g, ' ')\n .trim();\n}\n\nfunction cleanExtractedText(value) {\n let text = String(value || '')\n .replace(/\\b[A-Z]{1,3}\\d{2}n\\d{4,5}[a-z]?_p?\\d{4}[a-z]\\d{2}\\b/g, ' ')\n .replace(/\\uFFFD/g, '')\n .replace(/\\s+([,。;:!?、」』)》】])/g, '$1')\n .replace(/([「『《(【])\\s+/g, '$1');\n for (let i = 0; i < 3; i += 1) {\n text = text.replace(/([\\u3400-\\u9fff])\\s+([\\u3400-\\u9fff])/g, '$1$2');\n }\n return text.replace(/[ \\t]{2,}/g, ' ').trim();\n}\n\nfunction compactText(value, limit = 140) {\n const text = stripHtml(value);\n return text.length > limit ? `${text.slice(0, limit)}...` : text;\n}\n\nfunction collectSearchResults(data) {\n if (!data || typeof data !== 'object') return [];\n if (Array.isArray(data.results)) return data.results;\n if (Array.isArray(data?.results?.docs)) return data.results.docs;\n if (Array.isArray(data?.response?.docs)) return data.response.docs;\n if (Array.isArray(data.docs)) return data.docs;\n return [];\n}\n\nfunction firstText(item, keys) {\n for (const key of keys) {\n const value = item?.[key];\n if (Array.isArray(value)) {\n const first = value.map((entry) => safeUserText(String(entry))).find(Boolean);\n if (first) return first;\n }\n if (typeof value === 'string' && value.trim()) return value.trim();\n if (typeof value === 'number') return String(value);\n }\n return '';\n}\n\nfunction firstNumber(item, keys, fallback = undefined) {\n for (const key of keys) {\n const value = item?.[key];\n if (typeof value === 'number') return value;\n if (typeof value === 'string' && value.trim() && Number.isFinite(Number(value))) {\n return Number(value);\n }\n }\n return fallback;\n}\n\nasync function searchCbetaTitle(query, limit) {\n const url = buildUrl(cbetaApiRoot, 'search/title', { q: query, start: 0, rows: limit });\n const data = await fetchJson(url);\n return collectSearchResults(data).flatMap((item) => {\n const work = firstText(item, ['work', 'work_id', 'workId']);\n const title = firstText(item, ['content', 'title', 'work_title', 'workTitle']);\n if (!work || !title) return [];\n const juan = firstNumber(item, ['juan', 'juan_num', 'juanNum'], 1);\n return [\n {\n id: `cbeta-title-${work}-${juan}`,\n title,\n sourceName: 'CBETA 佛典',\n url: `cbeta:${work}:${juan}`,\n snippet: firstText(item, ['byline', 'creators', 'time_dynasty']) || '可下载经文正文并用于全球法布施。',\n resourceType: 'scripture',\n work,\n juan,\n },\n ];\n });\n}\n\nasync function searchCbetaContent(query, limit) {\n const endpoints = ['search/fulltext', 'search/content', 'search/all_in_one'];\n const settled = await Promise.allSettled(\n endpoints.map((endpoint) =>\n fetchJson(buildUrl(cbetaApiRoot, endpoint, { q: query, start: 0, rows: limit, field: 'content' })),\n ),\n );\n const results = [];\n for (const response of settled) {\n if (response.status !== 'fulfilled') continue;\n for (const item of collectSearchResults(response.value)) {\n const work = firstText(item, ['work', 'work_id', 'workId', 'sutra', 'id']);\n const snippet = compactText(firstText(item, ['kwic', 'highlight', 'snippet', 'content', 'text', 'body', 'p']));\n if (!work || !snippet) continue;\n const juan = firstNumber(item, ['juan', 'juan_num', 'juanNum', '卷'], 1);\n const title = firstText(item, ['title', 'work_title', 'workTitle', 'sutra_name', 'book', 'name']) || work;\n results.push({\n id: `cbeta-content-${work}-${juan}-${sha256(snippet).slice(0, 8)}`,\n title,\n sourceName: 'CBETA 佛典',\n url: `cbeta:${work}:${juan}`,\n snippet,\n resourceType: 'scripture',\n work,\n juan,\n });\n if (results.length >= limit) return results;\n }\n }\n return results;\n}\n\nfunction directUrlResource(query) {\n const url = safeUserText(query);\n let parsed;\n try {\n parsed = new URL(url);\n } catch {\n return null;\n }\n if (!['http:', 'https:'].includes(parsed.protocol)) return null;\n return {\n id: `url-${sha256(url).slice(0, 12)}`,\n title: parsed.hostname,\n sourceName: '网页资源',\n url,\n snippet: '直接下载并提取这个链接的可读正文。',\n resourceType: 'web',\n };\n}\n\nconst curatedCbetaWorks = [\n {\n work: 'T0235',\n juan: 1,\n title: '金剛般若波羅蜜經',\n keywords: ['金剛般若波羅蜜經', '金刚般若波罗蜜经', '金剛經', '金刚经'],\n snippet: '姚秦 鳩摩羅什譯。常用于读诵、共修与法布施的般若经典。',\n },\n {\n work: 'T0251',\n juan: 1,\n title: '般若波羅蜜多心經',\n keywords: ['般若波羅蜜多心經', '般若波罗蜜多心经', '心經', '心经'],\n snippet: '唐 玄奘譯。篇幅短,适合加入禅室功课本与日常持诵。',\n },\n {\n work: 'T0262',\n juan: 1,\n title: '妙法蓮華經',\n keywords: ['妙法蓮華經', '妙法莲华经', '法華經', '法华经'],\n snippet: '姚秦 鳩摩羅什譯。可按卷下载正文,用于长期功课与分享。',\n },\n {\n work: 'T0279',\n juan: 1,\n title: '大方廣佛華嚴經',\n keywords: ['大方廣佛華嚴經', '大方广佛华严经', '華嚴經', '华严经'],\n snippet: '唐 實叉難陀譯。可按卷下载正文,用于系统研读与法布施。',\n },\n {\n work: 'T0360',\n juan: 1,\n title: '佛說無量壽經',\n keywords: ['佛說無量壽經', '佛说无量寿经', '無量壽經', '无量寿经'],\n snippet: '净土经典。可下载正文,用于净土功课本与全球法布施。',\n },\n {\n work: 'T0365',\n juan: 1,\n title: '佛說觀無量壽佛經',\n keywords: ['佛說觀無量壽佛經', '佛说观无量寿佛经', '觀無量壽經', '观无量寿经'],\n snippet: '净土三经之一。适合查找下载后加入净土功课。',\n },\n {\n work: 'T0366',\n juan: 1,\n title: '佛說阿彌陀經',\n keywords: ['佛說阿彌陀經', '佛说阿弥陀经', '阿彌陀經', '阿弥陀经'],\n snippet: '姚秦 鳩摩羅什譯。篇幅适中,适合共修、持诵与分享。',\n },\n {\n work: 'T0475',\n juan: 1,\n title: '維摩詰所說經',\n keywords: ['維摩詰所說經', '维摩诘所说经', '維摩詰經', '维摩诘经'],\n snippet: '姚秦 鳩摩羅什譯。大乘经典,可按卷下载用于研读。',\n },\n {\n work: 'T0676',\n juan: 1,\n title: '解深密經',\n keywords: ['解深密經', '解深密经'],\n snippet: '唯识相关大乘经典。适合研读、整理笔记与法布施。',\n },\n {\n work: 'T1666',\n juan: 1,\n title: '大乘起信論',\n keywords: ['大乘起信論', '大乘起信论', '起信論', '起信论'],\n snippet: '大乘论典。适合下载后作为研习功课资料。',\n },\n];\n\nfunction localCbetaResults(query, limit) {\n const normalizedQuery = safeUserText(query).toLowerCase();\n if (!normalizedQuery) return [];\n return curatedCbetaWorks\n .filter((item) =>\n item.keywords.some((keyword) => {\n const normalizedKeyword = keyword.toLowerCase();\n return normalizedKeyword.includes(normalizedQuery) || normalizedQuery.includes(normalizedKeyword);\n }),\n )\n .slice(0, limit)\n .map((item) => ({\n id: `cbeta-curated-${item.work}-${item.juan}`,\n title: item.title,\n sourceName: 'CBETA 佛典',\n url: `cbeta:${item.work}:${item.juan}`,\n snippet: item.snippet,\n resourceType: 'scripture',\n work: item.work,\n juan: item.juan,\n }));\n}\n\nfunction uniqueResources(items, limit) {\n const seen = new Set();\n const results = [];\n for (const item of items) {\n const key = `${item.url}:${item.title}`;\n if (seen.has(key)) continue;\n seen.add(key);\n results.push(item);\n if (results.length >= limit) break;\n }\n return results;\n}\n\napp.post(\n '/api/resources/search',\n asyncHandler(async (req, res) => {\n const query = safeUserText(req.body?.query);\n const limit = Math.min(Math.max(Number(req.body?.limit || 12), 1), 20);\n if (query.length < 2) {\n return jsonResponse(res, 400, { success: false, message: 'query is required' });\n }\n\n const direct = directUrlResource(query);\n const results = [];\n if (direct) results.push(direct);\n results.push(...localCbetaResults(query, limit));\n\n const [titleResults, contentResults] = await Promise.allSettled([\n searchCbetaTitle(query, limit),\n searchCbetaContent(query, limit),\n ]);\n if (titleResults.status === 'fulfilled') results.push(...titleResults.value);\n if (contentResults.status === 'fulfilled') results.push(...contentResults.value);\n\n jsonResponse(res, 200, {\n success: true,\n source: 'cbeta',\n items: uniqueResources(results, limit),\n });\n }),\n);\n\nfunction parseCbetaResource(input) {\n const work = safeUserText(input.work);\n const juan = Number(input.juan || 1);\n if (work) return { work, juan: Number.isFinite(juan) ? juan : 1 };\n\n const url = safeUserText(input.url);\n const match = /^cbeta:([^:]+):(\\d+)$/i.exec(url);\n if (!match) return null;\n return {\n work: match[1].toUpperCase(),\n juan: Number(match[2] || 1),\n };\n}\n\nfunction safeFileName(value) {\n const normalized = String(value || 'dharma-resource')\n .replace(/[\\\\/:*?\"<>|]+/g, '-')\n .replace(/\\s+/g, '-')\n .replace(/-+/g, '-')\n .replace(/^-|-$/g, '')\n .slice(0, 80);\n return normalized || 'dharma-resource';\n}\n\nfunction extractCbetaHtml(data) {\n const first = Array.isArray(data?.results) ? data.results[0] : null;\n if (typeof first === 'string') return first;\n if (first && typeof first === 'object' && typeof first.html === 'string') return first.html;\n if (typeof data?.html === 'string') return data.html;\n return '';\n}\n\nasync function downloadCbetaResource(resource) {\n const url = buildUrl(cbetaApiRoot, 'juans', {\n work: resource.work,\n juan: resource.juan,\n work_info: 1,\n toc: 1,\n });\n const data = await fetchJson(url);\n const workInfo = data.work_info || {};\n const title = safeUserText(workInfo.title) || safeUserText(resource.title) || resource.work;\n const html = extractCbetaHtml(data);\n const text = cleanExtractedText(stripHtml(html));\n if (!text) throw new Error('CBETA resource returned empty content');\n return {\n title,\n sourceName: 'CBETA 佛典',\n url: `cbeta:${resource.work}:${resource.juan}`,\n contentText: text.slice(0, maxResourceTextChars),\n fileName: `${resource.work}_${resource.juan}_${safeFileName(title)}.txt`,\n };\n}\n\nasync function downloadWebResource(input) {\n const url = safeUserText(input.url);\n const parsed = new URL(url);\n if (!['http:', 'https:'].includes(parsed.protocol)) {\n throw new Error('Only http/https resources can be downloaded');\n }\n\n const response = await fetch(parsed, {\n headers: {\n Accept: 'text/html, text/plain, application/xhtml+xml, application/xml;q=0.8, */*;q=0.5',\n 'User-Agent': 'DachengResourceDownloader/1.0 (+https://ombhrum.com)',\n },\n signal: AbortSignal.timeout(25_000),\n });\n if (!response.ok) throw new Error(`Resource download failed: ${response.status}`);\n const contentType = response.headers.get('content-type') || '';\n const body = await response.text();\n const title = safeUserText(input.title) || parsed.hostname;\n const contentText = contentType.includes('html') ? cleanExtractedText(stripHtml(body)) : body.trim();\n if (!contentText) throw new Error('Resource returned empty content');\n return {\n title,\n sourceName: safeUserText(input.sourceName) || parsed.hostname,\n url,\n contentText: contentText.slice(0, maxResourceTextChars),\n fileName: `${safeFileName(title)}.txt`,\n };\n}\n\nfunction persistDownloadedResource(user, content) {\n const id = crypto.randomUUID();\n const fileName = `${id}-${safeFileName(content.fileName)}`;\n const filePath = path.join(resourcesDir, fileName);\n fs.writeFileSync(filePath, content.contentText, 'utf8');\n statements.insertResourceDownload.run({\n id,\n userId: user.userId,\n title: content.title,\n sourceName: content.sourceName,\n sourceUrl: content.url,\n fileName: content.fileName,\n filePath,\n createdAt: nowIso(),\n });\n}\n\napp.post(\n '/api/resources/download',\n asyncHandler(async (req, res) => {\n const user = await resolveUser(req, req.body || {});\n const cbeta = parseCbetaResource(req.body || {});\n const content = cbeta\n ? await downloadCbetaResource({ ...req.body, ...cbeta })\n : await downloadWebResource(req.body || {});\n persistDownloadedResource(user, content);\n jsonResponse(res, 200, {\n success: true,\n ...content,\n });\n }),\n);\n\napp.post(\n '/api/codex/resource-task',\n asyncHandler(async (req, res) => {\n if (process.env.ENABLE_CODEX_SDK !== 'true') {\n return jsonResponse(res, 403, {\n success: false,\n message: 'Codex SDK task runner is disabled. Set ENABLE_CODEX_SDK=true on the server to enable it.',\n });\n }\n const prompt = safeUserText(req.body?.prompt);\n if (!prompt) {\n return jsonResponse(res, 400, { success: false, message: 'prompt is required' });\n }\n const { Codex } = await import('@openai/codex-sdk');\n const codex = new Codex();\n const thread = codex.startThread();\n const result = await thread.run(prompt);\n jsonResponse(res, 200, {\n success: true,\n threadId: thread.id,\n result,\n });\n }),\n);\n\napp.use((error, _req, res, _next) => {\n const status = Number(error.statusCode || error.status || 500);\n const safeStatus = status >= 400 && status < 600 ? status : 500;\n logger.error({ error: error.stack || String(error) }, 'request failed');\n jsonResponse(res, safeStatus, {\n success: false,\n message: error.message || 'Internal Server Error',\n details: error.details,\n });\n});\n\nconst port = Number(process.env.PORT || 8788);\napp.listen(port, '0.0.0.0', () => {\n logger.info(\n {\n port,\n dbPath,\n dataDir,\n deepseekModel,\n cbetaApiRoot,\n },\n 'Dacheng AI backend started',\n );\n});\n"}, "33b5aa0d214c8013886ef1773c6ff1304af084f4": {"t": 1786067877.8084881, "n": 144923, "h": "55cea71410263f42", "c": "import crypto from 'node:crypto';\nimport fs from 'node:fs';\nimport path from 'node:path';\nimport process from 'node:process';\nimport { fileURLToPath } from 'node:url';\n\nimport { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';\nimport { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';\nimport Database from 'better-sqlite3';\nimport cors from 'cors';\nimport dotenv from 'dotenv';\nimport express from 'express';\nimport rateLimit from 'express-rate-limit';\nimport helmet from 'helmet';\nimport pino from 'pino';\nimport { z } from 'zod';\n\nimport { registerPlatformApi } from './platform_api.js';\nimport {\n codexResponsesMessages,\n codexResponsesTools,\n deepSeekResultToResponseItems,\n} from './codex_deepseek_adapter.js';\nimport {\n createPluginCodexPolicy,\n pluginConversationNamespace,\n pluginMcpTokenEnv,\n} from './plugin_codex_policy.js';\nimport {\n handleOfficialMcpRequest,\n officialMcpApps,\n} from './official_mcp_apps.js';\n\ndotenv.config();\n\nconst __dirname = path.dirname(fileURLToPath(import.meta.url));\nconst rootDir = path.resolve(__dirname, '..');\nconst dataDir = process.env.DATA_DIR || path.join(rootDir, 'data');\nconst resourcesDir = path.join(dataDir, 'resources');\nconst openClawRuntimeUpdatesDir =\n process.env.OPENCLAW_RUNTIME_UPDATES_DIR || path.join(dataDir, 'openclaw-runtime');\nconst openClawRuntimeEnableMacosUpdates =\n process.env.OPENCLAW_RUNTIME_ENABLE_MACOS_UPDATES === 'true';\nconst codexHomeDir = process.env.CODEX_HOME || path.join(dataDir, 'codex-home');\nconst codexPluginSessionsDir = path.join(dataDir, 'codex-plugin-sessions');\nconst codexTempDir = process.env.CODEX_TMPDIR || path.join(dataDir, 'codex-tmp');\nconst codexRuntimeDir = process.env.XDG_RUNTIME_DIR || path.join(dataDir, 'codex-runtime');\nconst dbPath = process.env.SQLITE_PATH || path.join(dataDir, 'dacheng-ai.sqlite');\n\nfs.mkdirSync(resourcesDir, { recursive: true });\nfs.mkdirSync(openClawRuntimeUpdatesDir, { recursive: true });\nfs.mkdirSync(codexHomeDir, { recursive: true });\nfs.mkdirSync(codexPluginSessionsDir, { recursive: true });\nfs.mkdirSync(codexTempDir, { recursive: true });\nfs.mkdirSync(codexRuntimeDir, { recursive: true });\n\nconst logger = pino({\n level: process.env.LOG_LEVEL || 'info',\n});\n\nconst app = express();\napp.disable('x-powered-by');\napp.set('trust proxy', Number(process.env.TRUST_PROXY_HOPS || 1));\napp.use(helmet());\nconst corsOrigin =\n !process.env.CORS_ORIGIN || process.env.CORS_ORIGIN.trim() === '*'\n ? true\n : process.env.CORS_ORIGIN.split(',').map((item) => item.trim()).filter(Boolean);\napp.use(cors({\n origin: corsOrigin,\n credentials: true,\n exposedHeaders: ['mcp-session-id', 'mcp-protocol-version'],\n}));\napp.use(express.json({ limit: '1mb' }));\napp.use(\n rateLimit({\n windowMs: 60_000,\n limit: Number(process.env.RATE_LIMIT_PER_MINUTE || 90),\n standardHeaders: true,\n legacyHeaders: false,\n }),\n);\n\nconst db = new Database(dbPath);\ndb.pragma('journal_mode = WAL');\ndb.pragma('foreign_keys = ON');\n\ndb.exec(`\n CREATE TABLE IF NOT EXISTS conversations (\n id TEXT PRIMARY KEY,\n user_id TEXT NOT NULL,\n title TEXT NOT NULL,\n provider TEXT NOT NULL,\n model TEXT NOT NULL,\n created_at TEXT NOT NULL,\n updated_at TEXT NOT NULL\n );\n\n CREATE INDEX IF NOT EXISTS idx_conversations_user_updated\n ON conversations (user_id, updated_at DESC);\n\n CREATE TABLE IF NOT EXISTS messages (\n id TEXT PRIMARY KEY,\n conversation_id TEXT NOT NULL,\n role TEXT NOT NULL,\n content TEXT NOT NULL,\n prompt_tokens INTEGER NOT NULL DEFAULT 0,\n completion_tokens INTEGER NOT NULL DEFAULT 0,\n total_tokens INTEGER NOT NULL DEFAULT 0,\n created_at TEXT NOT NULL,\n FOREIGN KEY (conversation_id) REFERENCES conversations(id) ON DELETE CASCADE\n );\n\n CREATE INDEX IF NOT EXISTS idx_messages_conversation_created\n ON messages (conversation_id, created_at ASC);\n\n CREATE TABLE IF NOT EXISTS usage_months (\n user_id TEXT NOT NULL,\n month TEXT NOT NULL,\n total_tokens INTEGER NOT NULL DEFAULT 0,\n updated_at TEXT NOT NULL,\n PRIMARY KEY (user_id, month)\n );\n\n CREATE TABLE IF NOT EXISTS resource_downloads (\n id TEXT PRIMARY KEY,\n user_id TEXT NOT NULL,\n title TEXT NOT NULL,\n source_name TEXT NOT NULL,\n source_url TEXT NOT NULL,\n file_name TEXT NOT NULL,\n file_path TEXT NOT NULL,\n created_at TEXT NOT NULL\n );\n\n CREATE TABLE IF NOT EXISTS agent_runs (\n id TEXT PRIMARY KEY,\n user_id TEXT NOT NULL,\n conversation_id TEXT NOT NULL,\n message_id TEXT NOT NULL,\n openclaw_run_id TEXT,\n openclaw_session_key TEXT NOT NULL,\n status TEXT NOT NULL,\n mode TEXT NOT NULL,\n provider TEXT NOT NULL,\n model TEXT,\n input_tokens INTEGER NOT NULL DEFAULT 0,\n output_tokens INTEGER NOT NULL DEFAULT 0,\n tool_call_count INTEGER NOT NULL DEFAULT 0,\n cost_microusd INTEGER NOT NULL DEFAULT 0,\n started_at TEXT NOT NULL,\n completed_at TEXT,\n failed_at TEXT,\n error_code TEXT,\n error_message TEXT\n );\n\n CREATE INDEX IF NOT EXISTS idx_agent_runs_user_started\n ON agent_runs (user_id, started_at DESC);\n\n CREATE INDEX IF NOT EXISTS idx_agent_runs_status_started\n ON agent_runs (status, started_at DESC);\n\n CREATE TABLE IF NOT EXISTS agent_message_feedback (\n id TEXT PRIMARY KEY,\n message_id TEXT NOT NULL,\n user_id TEXT NOT NULL,\n rating TEXT NOT NULL,\n reason TEXT,\n comment TEXT,\n created_at TEXT NOT NULL\n );\n`);\n\nconst conversationColumns = new Set(\n db.prepare('PRAGMA table_info(conversations)').all().map((column) => column.name),\n);\nif (!conversationColumns.has('namespace')) {\n db.exec('ALTER TABLE conversations ADD COLUMN namespace TEXT');\n}\nif (!conversationColumns.has('thread_id')) {\n db.exec('ALTER TABLE conversations ADD COLUMN thread_id TEXT');\n}\ndb.exec(`\n CREATE INDEX IF NOT EXISTS idx_conversations_user_namespace_updated\n ON conversations (user_id, namespace, updated_at DESC)\n`);\n\nconst statements = {\n insertConversation: db.prepare(`\n INSERT INTO conversations (id, user_id, title, provider, model, created_at, updated_at)\n VALUES (@id, @userId, @title, @provider, @model, @createdAt, @updatedAt)\n `),\n updateConversation: db.prepare(`\n UPDATE conversations\n SET title = @title, updated_at = @updatedAt\n WHERE id = @id AND user_id = @userId\n `),\n getConversation: db.prepare(`\n SELECT * FROM conversations WHERE id = ? AND user_id = ? LIMIT 1\n `),\n listConversations: db.prepare(`\n SELECT id, title, updated_at AS updatedAt\n FROM conversations\n WHERE user_id = ? AND (namespace IS NULL OR namespace = '')\n ORDER BY updated_at DESC\n LIMIT 30\n `),\n insertPluginConversation: db.prepare(`\n INSERT INTO conversations (\n id, user_id, title, provider, model, namespace, thread_id, created_at, updated_at\n )\n VALUES (\n @id, @userId, @title, @provider, @model, @namespace, NULL, @createdAt, @updatedAt\n )\n `),\n getPluginConversation: db.prepare(`\n SELECT * FROM conversations\n WHERE id = ? AND user_id = ? AND namespace = ?\n LIMIT 1\n `),\n updatePluginConversation: db.prepare(`\n UPDATE conversations\n SET title = @title, thread_id = @threadId, updated_at = @updatedAt\n WHERE id = @id AND user_id = @userId AND namespace = @namespace\n `),\n insertMessage: db.prepare(`\n INSERT INTO messages (\n id, conversation_id, role, content,\n prompt_tokens, completion_tokens, total_tokens, created_at\n )\n VALUES (\n @id, @conversationId, @role, @content,\n @promptTokens, @completionTokens, @totalTokens, @createdAt\n )\n `),\n listMessages: db.prepare(`\n SELECT role, content, prompt_tokens AS promptTokens,\n completion_tokens AS completionTokens, total_tokens AS totalTokens,\n created_at AS createdAt\n FROM messages\n WHERE conversation_id = ?\n ORDER BY created_at ASC\n `),\n getUsage: db.prepare(`\n SELECT total_tokens AS totalTokens FROM usage_months WHERE user_id = ? AND month = ?\n `),\n upsertUsage: db.prepare(`\n INSERT INTO usage_months (user_id, month, total_tokens, updated_at)\n VALUES (@userId, @month, @totalTokens, @updatedAt)\n ON CONFLICT(user_id, month)\n DO UPDATE SET total_tokens = usage_months.total_tokens + excluded.total_tokens,\n updated_at = excluded.updated_at\n `),\n insertResourceDownload: db.prepare(`\n INSERT INTO resource_downloads (\n id, user_id, title, source_name, source_url, file_name, file_path, created_at\n )\n VALUES (\n @id, @userId, @title, @sourceName, @sourceUrl, @fileName, @filePath, @createdAt\n )\n `),\n insertAgentRun: db.prepare(`\n INSERT INTO agent_runs (\n id, user_id, conversation_id, message_id, openclaw_run_id, openclaw_session_key,\n status, mode, provider, model, started_at\n )\n VALUES (\n @id, @userId, @conversationId, @messageId, @openClawRunId, @openClawSessionKey,\n @status, @mode, @provider, @model, @startedAt\n )\n `),\n getAgentRun: db.prepare(`\n SELECT * FROM agent_runs WHERE id = ? AND user_id = ? LIMIT 1\n `),\n updateAgentRunStatus: db.prepare(`\n UPDATE agent_runs\n SET status = @status,\n openclaw_run_id = COALESCE(@openClawRunId, openclaw_run_id),\n input_tokens = COALESCE(@inputTokens, input_tokens),\n output_tokens = COALESCE(@outputTokens, output_tokens),\n tool_call_count = COALESCE(@toolCallCount, tool_call_count),\n completed_at = @completedAt,\n failed_at = @failedAt,\n error_code = @errorCode,\n error_message = @errorMessage\n WHERE id = @id AND user_id = @userId\n `),\n insertAgentFeedback: db.prepare(`\n INSERT INTO agent_message_feedback (id, message_id, user_id, rating, reason, comment, created_at)\n VALUES (@id, @messageId, @userId, @rating, @reason, @comment, @createdAt)\n `),\n};\n\nconst deepseekApiKey = process.env.DEEPSEEK_API_KEY || '';\nconst deepseekBaseUrl = (process.env.DEEPSEEK_BASE_URL || 'https://api.deepseek.com').replace(/\\/+$/, '');\nconst deepseekModel = process.env.DEEPSEEK_MODEL || 'deepseek-chat';\nconst deepseekAvailableModels = Array.from(\n new Set(\n (process.env.DEEPSEEK_MODELS || `${deepseekModel},deepseek-chat,deepseek-reasoner`)\n .split(',')\n .map((item) => normalizeDeepSeekModelName(item))\n .filter(Boolean),\n ),\n);\nconst cbetaApiRoot = (process.env.CBETA_API_ROOT || 'https://api.ombhrum.com/api/cbeta').replace(/\\/+$/, '');\nconst fabushiApiBaseUrl = (process.env.FABUSHI_API_BASE_URL || 'https://api.ombhrum.com').replace(/\\/+$/, '');\nconst memberMonthlyLimit = Number(process.env.MEMBER_MONTHLY_TOKEN_LIMIT || 1_000_000);\nconst freeMonthlyLimit = Number(process.env.FREE_MONTHLY_TOKEN_LIMIT || 1_000);\nconst requireMemberForAi = process.env.REQUIRE_MEMBER_FOR_AI === 'true';\nconst deepseekMaxCompletionTokens = Math.max(1, Number(process.env.DEEPSEEK_MAX_COMPLETION_TOKENS || 1400));\nconst botFatherMaxCompletionTokens = Math.max(\n deepseekMaxCompletionTokens,\n Math.min(12_000, Number(process.env.BOT_FATHER_MAX_COMPLETION_TOKENS || 6000)),\n);\nconst maxResourceTextChars = Number(process.env.MAX_RESOURCE_TEXT_CHARS || 80_000);\nconst openClawRuntimeManifestPath =\n process.env.OPENCLAW_RUNTIME_MANIFEST_PATH || path.join(openClawRuntimeUpdatesDir, 'manifest.json');\nconst openClawRuntimePublicBaseUrl = (process.env.OPENCLAW_RUNTIME_PUBLIC_BASE_URL || '').replace(/\\/+$/, '');\nconst enableLibreChatAgentChat = process.env.ENABLE_LIBRECHAT_AGENT_CHAT === 'true';\nconst libreChatAgentsBaseUrl = (\n process.env.LIBRECHAT_AGENTS_BASE_URL || 'http://127.0.0.1:3080/api/agents/v1'\n).replace(/\\/+$/, '');\nconst libreChatAgentApiKey = process.env.LIBRECHAT_AGENT_API_KEY || '';\nconst libreChatAgentId = process.env.LIBRECHAT_AGENT_ID || '';\nconst enableCodexSdkChat = process.env.ENABLE_CODEX_SDK_CHAT === 'true';\nconst enableOpenClawAgentChat = process.env.ENABLE_OPENCLAW_AGENT_CHAT === 'true';\nconst openClawGatewayUrl = (process.env.OPENCLAW_GATEWAY_URL || 'http://127.0.0.1:18789').replace(/\\/+$/, '');\nconst openClawGatewayToken = process.env.OPENCLAW_GATEWAY_TOKEN || '';\nconst openClawAgentId = process.env.OPENCLAW_AGENT_ID || 'fabushi-public-agent';\nconst openClawRunsEndpoint = process.env.OPENCLAW_RUNS_ENDPOINT || '';\nconst codexDeepSeekProviderId = 'dacheng-deepseek-proxy';\nconst codexResponsesBaseUrl = (\n process.env.CODEX_DEEPSEEK_RESPONSES_BASE_URL ||\n `http://127.0.0.1:${process.env.PORT || 8788}/codex-deepseek/v1`\n).replace(/\\/+$/, '');\nconst codexAdapterTokenPrefix = 'dacheng-codex-proxy.';\nconst codexAdapterTokenTtlMs = Math.max(\n 60_000,\n Number(process.env.CODEX_DEEPSEEK_ADAPTER_TOKEN_TTL_MS || 15 * 60_000),\n);\nconst codexAdapterSecret =\n process.env.CODEX_DEEPSEEK_ADAPTER_SECRET ||\n deepseekApiKey ||\n openClawGatewayToken ||\n 'dacheng-codex-local-dev-secret';\n\nconst systemPrompt = [\n '你是“大乘”App 的 AI 助手。',\n '你的核心任务是帮助用户查找、整理、理解并全球法布施合法可分享的佛法资源。',\n '回答要庄重、简洁、可执行;涉及经典、仪轨或资源时,提醒用户尊重版权、来源和当地法规。',\n '如果用户想找资源、下载经文、寻找音频或准备可分享资料,你必须先在后端自动执行搜索、验证和下载步骤;不要要求用户去前端 + 菜单手动查找。',\n '你可以把执行进度总结给用户,但不要输出隐藏推理原文。',\n].join('\\n');\n\nfunction nowIso() {\n return new Date().toISOString();\n}\n\nfunction monthKey(date = new Date()) {\n return `${date.getUTCFullYear()}-${String(date.getUTCMonth() + 1).padStart(2, '0')}`;\n}\n\nfunction jsonResponse(res, status, payload) {\n res.status(status).json(payload);\n}\n\nasync function readJsonFile(filePath) {\n const raw = await fs.promises.readFile(filePath, 'utf8');\n return JSON.parse(raw);\n}\n\nfunction requestOrigin(req) {\n if (openClawRuntimePublicBaseUrl) return openClawRuntimePublicBaseUrl;\n const proto = req.get('X-Forwarded-Proto') || req.protocol || 'https';\n const host = req.get('X-Forwarded-Host') || req.get('Host');\n return host ? `${proto}://${host}` : '';\n}\n\nfunction absolutizeOpenClawRuntimeUrl(req, value) {\n const text = readText(value);\n if (!text) return '';\n if (/^https?:\\/\\//i.test(text)) return text;\n const normalized = text.startsWith('/')\n ? text\n : `/api/openclaw/runtime/files/${text.replace(/^\\/+/, '')}`;\n const origin = requestOrigin(req);\n return origin ? new URL(normalized, `${origin}/`).toString() : normalized;\n}\n\nfunction textToolResult(text, structuredContent = {}) {\n return {\n structuredContent,\n content: [{ type: 'text', text }],\n };\n}\n\nfunction sseHeaders(res) {\n res.status(200);\n res.setHeader('Content-Type', 'text/event-stream; charset=utf-8');\n res.setHeader('Cache-Control', 'no-cache, no-transform');\n res.setHeader('Connection', 'keep-alive');\n res.setHeader('X-Accel-Buffering', 'no');\n if (typeof res.flushHeaders === 'function') res.flushHeaders();\n}\n\nfunction writeSse(res, event, payload) {\n res.write(`event: ${event}\\n`);\n res.write(`data: ${JSON.stringify(payload)}\\n\\n`);\n}\n\nfunction asyncHandler(fn) {\n return (req, res, next) => {\n Promise.resolve(fn(req, res, next)).catch(next);\n };\n}\n\nfunction bearerToken(req) {\n const delegated = readText(req.get('x-dacheng-auth-token'));\n if (\n delegated &&\n delegated !== 'DACHENG_AUTH_TOKEN' &&\n !delegated.startsWith('secretref-env:') &&\n delegated !== 'dacheng-openclaw-proxy'\n ) {\n return delegated;\n }\n const header = req.get('Authorization') || '';\n if (!header.startsWith('Bearer ')) return '';\n const token = header.slice('Bearer '.length).trim();\n return token === 'dacheng-openclaw-proxy' ? '' : token;\n}\n\nfunction sha256(value) {\n return crypto.createHash('sha256').update(value).digest('hex');\n}\n\nfunction safeUserText(value, fallback = '') {\n if (typeof value !== 'string') return fallback;\n return value.replace(/\\u0000/g, '').trim();\n}\n\nfunction estimateTokens(text) {\n const normalized = String(text || '').trim();\n if (!normalized) return 0;\n return Math.max(1, Math.ceil(normalized.length / 3));\n}\n\nfunction readText(value) {\n if (value === undefined || value === null) return '';\n return String(value).replace(/\\u0000/g, '').trim();\n}\n\nfunction clientContextMessage(client) {\n if (!client || typeof client !== 'object') return '';\n const selectedModel = readText(client.selectedModel || client.model);\n const project = client.project && typeof client.project === 'object' ? client.project : {};\n const projectName = readText(project.name);\n const projectPath = readText(project.path);\n const lines = ['桌面端上下文:'];\n if (selectedModel) lines.push(`用户选择的 DeepSeek 模型:${normalizeDeepSeekModelName(selectedModel)}`);\n if (projectPath) {\n lines.push(`当前项目名称:${projectName || projectPath}`);\n lines.push(`当前项目路径:${projectPath}`);\n lines.push('如果本轮任务涉及读取、修改、运行、分析或整理项目,请优先围绕当前项目路径工作;不要擅自操作其他目录。');\n }\n return lines.length > 1 ? lines.join('\\n') : '';\n}\nfunction unwrapPayload(payload) {\n if (!payload || typeof payload !== 'object') return {};\n return payload.data && typeof payload.data === 'object' ? payload.data : payload;\n}\n\nfunction readFirstText(values) {\n for (const value of values) {\n const text = readText(value);\n if (text) return text;\n }\n return '';\n}\n\nfunction activeFlag(value) {\n if (value === true) return true;\n if (value === 1) return true;\n if (typeof value !== 'string') return false;\n return ['true', '1', 'yes', 'active'].includes(value.trim().toLowerCase());\n}\n\nfunction futureDate(value) {\n const text = readText(value);\n if (!text) return false;\n const timestamp = Date.parse(text);\n return Number.isFinite(timestamp) && timestamp > Date.now();\n}\n\nfunction normalizeMembershipPayload(payload) {\n if (!payload || typeof payload !== 'object') return false;\n const data = unwrapPayload(payload);\n const membership = data.membership && typeof data.membership === 'object' ? data.membership : data;\n const type = readText(membership.type || membership.membershipType || data.membershipType).toLowerCase();\n const status = readText(membership.status || data.status).toLowerCase();\n const expiry = membership.expiresAt ||\n membership.expires_at ||\n membership.membershipExpiry ||\n membership.membership_expires_at ||\n data.membershipExpiry ||\n data.membership_expires_at;\n const hasExplicitActiveFlag =\n membership.isActive !== undefined ||\n membership.active !== undefined ||\n membership.hasPremiumAccess !== undefined ||\n membership.hasPremiumMembership !== undefined ||\n membership.isPremium !== undefined;\n\n if (hasExplicitActiveFlag) {\n return Boolean(\n activeFlag(membership.isActive) ||\n activeFlag(membership.active) ||\n activeFlag(membership.hasPremiumAccess) ||\n activeFlag(membership.hasPremiumMembership) ||\n activeFlag(membership.isPremium),\n );\n }\n\n return Boolean(\n status === 'active' ||\n type === 'paid' ||\n type === 'premium' ||\n type === 'trial' ||\n futureDate(expiry),\n );\n}\n\nfunction extractRemoteAccount(payload) {\n const data = unwrapPayload(payload);\n const nestedUser = data.user && typeof data.user === 'object' ? data.user : {};\n const id = readFirstText([\n data.userId,\n data.user_id,\n data.userNo,\n data.user_no,\n data.id,\n nestedUser.userId,\n nestedUser.user_id,\n nestedUser.userNo,\n nestedUser.user_no,\n nestedUser.id,\n ]);\n const username = readFirstText([data.username, data.name, nestedUser.username, nestedUser.name]);\n const email = readFirstText([data.email, nestedUser.email]);\n const membership = data.membership && typeof data.membership === 'object' ? data.membership : null;\n return {\n authenticated: Boolean(id || username || email || membership || data.success === true),\n id,\n username,\n email,\n isMember: normalizeMembershipPayload(payload),\n membership,\n };\n}\n\nasync function resolveRemoteAccount(token) {\n const fallback = {\n authenticated: false,\n id: '',\n username: '',\n email: '',\n isMember: false,\n membership: null,\n };\n if (!token || !fabushiApiBaseUrl) return fallback;\n\n const endpoints = [\n '/api/stripe/membership-status',\n '/api/alipay/check-membership',\n '/api/auth/user-info',\n ];\n\n for (const endpoint of endpoints) {\n try {\n const response = await fetch(`${fabushiApiBaseUrl}${endpoint}`, {\n headers: {\n Accept: 'application/json',\n Authorization: `Bearer ${token}`,\n },\n signal: AbortSignal.timeout(8_000),\n });\n if (response.status === 401 || response.status === 403) return fallback;\n if (!response.ok) continue;\n const payload = await response.json();\n const account = extractRemoteAccount(payload);\n if (account.authenticated || account.isMember) {\n return {\n ...fallback,\n ...account,\n authenticated: true,\n };\n }\n } catch (error) {\n logger.debug({ endpoint, error: String(error) }, 'membership lookup skipped');\n }\n }\n\n return fallback;\n}\n\nasync function resolveUser(req, body = {}) {\n const token = bearerToken(req);\n const usernameHint = safeUserText(body.username || req.query.username);\n\n const internalMcpAccount = resolveCodexAdapterToken(token);\n if (\n internalMcpAccount?.audience === 'mcp-plugin' &&\n req.path.startsWith('/api/mcp/apps/')\n ) {\n return internalMcpAccount.user;\n }\n\n const testToken = readText(process.env.TEST_ACCOUNT_TOKEN);\n if (testToken && token && token === testToken) {\n return {\n userId: 'user:test_account',\n username: 'TestAccount',\n tokenHash: sha256(token).slice(0, 24),\n isAuthenticated: true,\n isMember: true,\n isTestAccount: true,\n membership: { type: 'lifetime', active: true },\n };\n }\n\n const tokenHash = token ? sha256(token).slice(0, 24) : '';\n const anonymousHash = sha256(`${req.ip}|${req.get('User-Agent') || ''}`).slice(0, 24);\n const remoteAccount = await resolveRemoteAccount(token);\n const remoteIdentity = remoteAccount.id ||\n remoteAccount.username ||\n remoteAccount.email ||\n (remoteAccount.authenticated ? tokenHash : '');\n const userId = remoteAccount.authenticated\n ? `user:${remoteIdentity}`\n : tokenHash\n ? `token:${tokenHash}`\n : `anon:${anonymousHash}`;\n return {\n userId,\n username: remoteAccount.username || usernameHint,\n tokenHash,\n isAuthenticated: remoteAccount.authenticated,\n isMember: remoteAccount.isMember,\n membership: remoteAccount.membership,\n };\n}\n\nfunction base64UrlJson(value) {\n return Buffer.from(JSON.stringify(value), 'utf8').toString('base64url');\n}\n\nfunction timingSafeEqualText(left, right) {\n const leftBuffer = Buffer.from(String(left || ''));\n const rightBuffer = Buffer.from(String(right || ''));\n if (leftBuffer.length !== rightBuffer.length) return false;\n return crypto.timingSafeEqual(leftBuffer, rightBuffer);\n}\n\nfunction signCodexAdapterPayload(encodedPayload) {\n return crypto\n .createHmac('sha256', codexAdapterSecret)\n .update(encodedPayload)\n .digest('base64url');\n}\n\nfunction createCodexAdapterToken(user, options = {}) {\n const requestedMaxCompletionTokens = Number(options.maxCompletionTokens || 0);\n const payload = {\n sub: user.userId,\n username: user.username || '',\n tokenHash: user.tokenHash || '',\n isAuthenticated: Boolean(user.isAuthenticated),\n isMember: Boolean(user.isMember),\n isTestAccount: Boolean(user.isTestAccount),\n membership: user.membership || {},\n aud: safeUserText(options.audience) || 'codex-responses',\n recordUsage: options.recordUsage !== false,\n maxCompletionTokens:\n Number.isFinite(requestedMaxCompletionTokens) && requestedMaxCompletionTokens > 0\n ? Math.floor(requestedMaxCompletionTokens)\n : null,\n exp: Date.now() + codexAdapterTokenTtlMs,\n };\n const encodedPayload = base64UrlJson(payload);\n return `${codexAdapterTokenPrefix}${encodedPayload}.${signCodexAdapterPayload(encodedPayload)}`;\n}\n\nfunction resolveCodexAdapterToken(token) {\n const raw = readText(token);\n if (!raw.startsWith(codexAdapterTokenPrefix)) return null;\n\n const signed = raw.slice(codexAdapterTokenPrefix.length);\n const separator = signed.lastIndexOf('.');\n if (separator < 1) return null;\n\n const encodedPayload = signed.slice(0, separator);\n const signature = signed.slice(separator + 1);\n const expectedSignature = signCodexAdapterPayload(encodedPayload);\n if (!timingSafeEqualText(signature, expectedSignature)) return null;\n\n let payload;\n try {\n payload = JSON.parse(Buffer.from(encodedPayload, 'base64url').toString('utf8'));\n } catch {\n return null;\n }\n\n if (!payload || typeof payload !== 'object') return null;\n if (Number(payload.exp || 0) < Date.now()) return null;\n\n const userId = safeUserText(payload.sub);\n if (!userId) return null;\n\n return {\n user: {\n userId,\n username: safeUserText(payload.username),\n tokenHash: safeUserText(payload.tokenHash),\n isAuthenticated: Boolean(payload.isAuthenticated),\n isMember: Boolean(payload.isMember),\n isTestAccount: Boolean(payload.isTestAccount),\n membership:\n payload.membership && typeof payload.membership === 'object'\n ? payload.membership\n : {},\n },\n audience: safeUserText(payload.aud) || 'codex-responses',\n recordUsage: payload.recordUsage !== false,\n maxCompletionTokens:\n Number.isFinite(Number(payload.maxCompletionTokens)) && Number(payload.maxCompletionTokens) > 0\n ? Math.floor(Number(payload.maxCompletionTokens))\n : null,\n };\n}\n\nasync function resolveCodexResponsesBilling(req) {\n const token = bearerToken(req);\n const adapter = resolveCodexAdapterToken(token);\n if (adapter) return adapter;\n\n if (deepseekApiKey && token === deepseekApiKey) {\n const error = new Error('Codex DeepSeek adapter now requires first-party user billing');\n error.statusCode = 401;\n throw error;\n }\n\n return {\n user: await resolveUser(req, req.body || {}),\n recordUsage: true,\n };\n}\n\nfunction usageFor(userId) {\n const month = monthKey();\n const row = statements.getUsage.get(userId, month);\n return {\n month,\n used: Number(row?.totalTokens || 0),\n };\n}\n\nfunction enforceTokenBudget(user, estimatedTokensForRequest) {\n if (user && user.isTestAccount) {\n return { limit: 999999999, usage: { month: monthKey(), used: 0 } };\n }\n if (requireMemberForAi && !user.isMember) {\n const error = new Error('大乘 AI 需要开通会员后使用');\n error.statusCode = 403;\n error.code = 'AI_MEMBERSHIP_REQUIRED';\n error.details = {\n requireMemberForAi,\n monthlyLimit: memberMonthlyLimit,\n remainingTokens: 0,\n };\n throw error;\n }\n\n const limit = user.isMember ? memberMonthlyLimit : freeMonthlyLimit;\n const usage = usageFor(user.userId);\n if (usage.used + estimatedTokensForRequest > limit) {\n const error = new Error('本月 AI token 额度已不足');\n error.statusCode = 429;\n error.code = 'USER_TOKEN_QUOTA_EXCEEDED';\n error.details = {\n monthlyLimit: limit,\n usedTokens: usage.used,\n remainingTokens: Math.max(0, limit - usage.used),\n };\n throw error;\n }\n return { limit, usage };\n}\n\nfunction completionBudgetFor(\n budget,\n estimatedPromptTokens,\n maximumCompletionTokens = deepseekMaxCompletionTokens,\n) {\n const remainingAfterPrompt = budget.limit - budget.usage.used - estimatedPromptTokens;\n return Math.max(1, Math.min(maximumCompletionTokens, Math.floor(remainingAfterPrompt)));\n}\n\nfunction recordUsage(userId, totalTokens) {\n if (!totalTokens || totalTokens < 1) return;\n statements.upsertUsage.run({\n userId,\n month: monthKey(),\n totalTokens,\n updatedAt: nowIso(),\n });\n}\n\nfunction titleFromMessage(message) {\n const compact = message.replace(/\\s+/g, ' ').trim();\n if (!compact) return '新对话';\n return compact.length > 22 ? `${compact.slice(0, 22)}...` : compact;\n}\n\nfunction normalizeMessages(rows) {\n return rows.map((row) => ({\n role: row.role,\n content: row.content,\n }));\n}\n\nfunction normalizeMaxCompletionTokens(\n value,\n maximumCompletionTokens = deepseekMaxCompletionTokens,\n) {\n const numeric = Number(value || maximumCompletionTokens);\n if (!Number.isFinite(numeric) || numeric < 1) return maximumCompletionTokens;\n return Math.max(1, Math.min(maximumCompletionTokens, Math.floor(numeric)));\n}\n\nfunction normalizeDeepSeekModelName(value) {\n const raw = readText(value || deepseekModel);\n if (!raw) return deepseekModel;\n const modelId = raw.includes('/') ? raw.split('/').pop() : raw;\n return modelId || deepseekModel;\n}\n\nasync function callDeepSeek(messages, options = {}) {\n if (!deepseekApiKey) {\n const error = new Error('DeepSeek API key is not configured');\n error.statusCode = 503;\n throw error;\n }\n\n const model = normalizeDeepSeekModelName(options.model);\n const response = await fetch(`${deepseekBaseUrl}/chat/completions`, {\n method: 'POST',\n headers: {\n Accept: 'application/json',\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${deepseekApiKey}`,\n },\n body: JSON.stringify({\n model,\n messages,\n temperature: 0.4,\n max_tokens: normalizeMaxCompletionTokens(\n options.maxCompletionTokens,\n options.maximumCompletionTokens || deepseekMaxCompletionTokens,\n ),\n stream: false,\n ...(options.tools?.length ? { tools: options.tools, tool_choice: 'auto' } : {}),\n }),\n signal: AbortSignal.timeout(60_000),\n });\n\n const bodyText = await response.text();\n let payload;\n try {\n payload = bodyText ? JSON.parse(bodyText) : {};\n } catch {\n payload = { error: bodyText };\n }\n\n if (!response.ok) {\n const upstreamMessage = payload?.error?.message || payload?.message || '';\n const friendlyMessage =\n response.status === 402 || /insufficient\\s+balance/i.test(upstreamMessage)\n ? '大乘 AI 服务额度暂不可用,请联系管理员充值 DeepSeek 账户后重试。'\n : upstreamMessage || `DeepSeek request failed: ${response.status}`;\n const error = new Error(friendlyMessage);\n error.statusCode = response.status;\n if (response.status === 402 || /insufficient\\s+balance/i.test(upstreamMessage)) {\n error.code = 'DEEPSEEK_PROVIDER_QUOTA';\n }\n throw error;\n }\n\n const upstreamMessage = payload?.choices?.[0]?.message || {};\n const message = typeof upstreamMessage.content === 'string'\n ? upstreamMessage.content.trim()\n : '';\n const toolCalls = Array.isArray(upstreamMessage.tool_calls)\n ? upstreamMessage.tool_calls\n : [];\n if (!message && toolCalls.length === 0) {\n const error = new Error('DeepSeek returned an empty response');\n error.statusCode = 502;\n throw error;\n }\n\n return {\n message,\n toolCalls,\n model,\n usage: {\n promptTokens: Number(payload?.usage?.prompt_tokens || 0),\n completionTokens: Number(payload?.usage?.completion_tokens || 0),\n totalTokens: Number(payload?.usage?.total_tokens || 0),\n },\n };\n}\n\nasync function callDeepSeekStream(messages, callbacks = {}) {\n if (!deepseekApiKey) {\n const error = new Error('DeepSeek API key is not configured');\n error.statusCode = 503;\n throw error;\n }\n\n const model = normalizeDeepSeekModelName(callbacks.model);\n const response = await fetch(`${deepseekBaseUrl}/chat/completions`, {\n method: 'POST',\n headers: {\n Accept: 'text/event-stream',\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${deepseekApiKey}`,\n },\n body: JSON.stringify({\n model,\n messages,\n temperature: 0.4,\n max_tokens: normalizeMaxCompletionTokens(\n callbacks.maxCompletionTokens,\n callbacks.maximumCompletionTokens || deepseekMaxCompletionTokens,\n ),\n stream: true,\n stream_options: { include_usage: true },\n }),\n signal: callbacks.signal || AbortSignal.timeout(90_000),\n });\n\n if (!response.ok) {\n const bodyText = await response.text();\n let payload;\n try {\n payload = bodyText ? JSON.parse(bodyText) : {};\n } catch {\n payload = { error: bodyText };\n }\n const upstreamMessage = payload?.error?.message || payload?.message || '';\n const friendlyMessage =\n response.status === 402 || /insufficient\\s+balance/i.test(upstreamMessage)\n ? '大乘 AI 服务额度暂不可用,请联系管理员充值 DeepSeek 账户后重试。'\n : upstreamMessage || `DeepSeek request failed: ${response.status}`;\n const error = new Error(friendlyMessage);\n error.statusCode = response.status;\n if (response.status === 402 || /insufficient\\s+balance/i.test(upstreamMessage)) {\n error.code = 'DEEPSEEK_PROVIDER_QUOTA';\n }\n throw error;\n }\n\n let buffer = '';\n let message = '';\n let usage = {\n promptTokens: 0,\n completionTokens: 0,\n totalTokens: 0,\n };\n const decoder = new TextDecoder();\n\n for await (const chunk of response.body) {\n buffer += decoder.decode(chunk, { stream: true });\n const lines = buffer.split(/\\r?\\n/);\n buffer = lines.pop() || '';\n\n for (const rawLine of lines) {\n const line = rawLine.trim();\n if (!line.startsWith('data:')) continue;\n const data = line.slice('data:'.length).trim();\n if (!data || data === '[DONE]') continue;\n\n let payload;\n try {\n payload = JSON.parse(data);\n } catch {\n continue;\n }\n\n const delta = payload?.choices?.[0]?.delta?.content || '';\n if (delta) {\n message += delta;\n callbacks.onToken?.(delta);\n }\n\n if (payload?.usage) {\n usage = {\n promptTokens: Number(payload.usage.prompt_tokens || usage.promptTokens || 0),\n completionTokens: Number(payload.usage.completion_tokens || usage.completionTokens || 0),\n totalTokens: Number(payload.usage.total_tokens || usage.totalTokens || 0),\n };\n }\n }\n }\n\n message = message.trim();\n if (!message) {\n const error = new Error('DeepSeek returned an empty response');\n error.statusCode = 502;\n throw error;\n }\n\n return { message, model, usage };\n}\n\nfunction createCodexDeepSeekRuntime(user = null, runtimeOptions = {}) {\n if (!enableCodexSdkChat || !deepseekApiKey) {\n return {\n enabled: false,\n provider: codexDeepSeekProviderId,\n reason: enableCodexSdkChat ? 'DeepSeek API key is not configured' : 'ENABLE_CODEX_SDK_CHAT is not true',\n };\n }\n\n const adapterToken = user\n ? createCodexAdapterToken(user, {\n maxCompletionTokens: runtimeOptions.maxCompletionTokens,\n })\n : '';\n return {\n enabled: true,\n provider: codexDeepSeekProviderId,\n options: {\n apiKey: adapterToken,\n baseUrl: codexResponsesBaseUrl,\n env: {\n ...process.env,\n DEEPSEEK_API_KEY: adapterToken,\n HOME: runtimeOptions.codexHomeDir || codexHomeDir,\n CODEX_HOME: runtimeOptions.codexHomeDir || codexHomeDir,\n TMPDIR: codexTempDir,\n XDG_RUNTIME_DIR: codexRuntimeDir,\n XDG_CACHE_HOME: path.join(codexHomeDir, '.cache'),\n XDG_CONFIG_HOME: path.join(codexHomeDir, '.config'),\n XDG_DATA_HOME: path.join(codexHomeDir, '.local', 'share'),\n PATH: process.env.PATH || '/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin',\n ...(runtimeOptions.env || {}),\n },\n config: {\n model_provider: codexDeepSeekProviderId,\n model_providers: {\n [codexDeepSeekProviderId]: {\n name: 'Dacheng DeepSeek Proxy',\n base_url: codexResponsesBaseUrl,\n env_key: 'DEEPSEEK_API_KEY',\n wire_api: 'responses',\n query_params: {},\n },\n },\n ...(runtimeOptions.config || {}),\n },\n },\n threadOptions: {\n model: deepseekModel,\n approvalPolicy: runtimeOptions.approvalPolicy || 'never',\n sandboxMode: runtimeOptions.sandboxMode || 'read-only',\n ...(runtimeOptions.workingDirectory\n ? { workingDirectory: runtimeOptions.workingDirectory }\n : {}),\n skipGitRepoCheck: true,\n networkAccessEnabled: runtimeOptions.networkAccessEnabled ?? true,\n webSearchMode: runtimeOptions.webSearchMode || 'disabled',\n },\n };\n}\n\nfunction codexPromptFromMessages(messages) {\n return messages\n .map((message) => {\n const role = String(message.role || 'user').toUpperCase();\n return `${role}:\\n${message.content}`;\n })\n .join('\\n\\n');\n}\n\nfunction responseContentText(content) {\n if (typeof content === 'string') return content;\n if (!Array.isArray(content)) return '';\n return content\n .map((part) => {\n if (typeof part === 'string') return part;\n if (!part || typeof part !== 'object') return '';\n return String(part.text || part.input_text || part.output_text || '');\n })\n .filter(Boolean)\n .join('\\n');\n}\n\nfunction codexResponsesPrompt(body) {\n const input = Array.isArray(body?.input) ? body.input : [];\n const userItems = input.filter((item) => item?.role === 'user');\n const source = userItems[userItems.length - 1] || input[input.length - 1];\n const prompt = responseContentText(source?.content).trim();\n if (prompt) return prompt;\n if (typeof body?.input === 'string') return body.input.trim();\n return '';\n}\n\nfunction writeResponsesEvent(res, event, data) {\n res.write(`event: ${event}\\n`);\n res.write(`data: ${JSON.stringify(data)}\\n\\n`);\n}\n\nfunction responseUsage(usage = {}) {\n return {\n input_tokens: Number(usage.promptTokens || usage.input_tokens || 0),\n cached_input_tokens: 0,\n output_tokens: Number(usage.completionTokens || usage.output_tokens || 0),\n reasoning_output_tokens: 0,\n total_tokens: Number(usage.totalTokens || usage.total_tokens || 0),\n };\n}\n\nfunction createLibreChatAgentRuntime() {\n if (!enableLibreChatAgentChat || !libreChatAgentApiKey || !libreChatAgentId) {\n return {\n enabled: false,\n provider: 'librechat-agent',\n reason: !enableLibreChatAgentChat\n ? 'ENABLE_LIBRECHAT_AGENT_CHAT is not true'\n : !libreChatAgentApiKey\n ? 'LIBRECHAT_AGENT_API_KEY is not configured'\n : 'LIBRECHAT_AGENT_ID is not configured',\n };\n }\n\n return {\n enabled: true,\n provider: 'librechat-agent',\n model: libreChatAgentId,\n chatCompletionsUrl: `${libreChatAgentsBaseUrl}/chat/completions`,\n };\n}\n\nfunction libreChatUsage(usage = {}) {\n return {\n promptTokens: Number(usage.prompt_tokens || usage.input_tokens || usage.promptTokens || 0),\n completionTokens: Number(usage.completion_tokens || usage.output_tokens || usage.completionTokens || 0),\n totalTokens: Number(usage.total_tokens || usage.totalTokens || 0),\n };\n}\n\nasync function callLibreChatAgent(messages) {\n const runtime = createLibreChatAgentRuntime();\n if (!runtime.enabled) {\n const error = new Error(runtime.reason || 'LibreChat Agent chat is not enabled');\n error.statusCode = 503;\n throw error;\n }\n\n const response = await fetch(runtime.chatCompletionsUrl, {\n method: 'POST',\n headers: {\n Accept: 'application/json',\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${libreChatAgentApiKey}`,\n },\n body: JSON.stringify({\n model: runtime.model,\n messages,\n temperature: 0.4,\n stream: false,\n }),\n signal: AbortSignal.timeout(120_000),\n });\n\n const bodyText = await response.text();\n let payload;\n try {\n payload = bodyText ? JSON.parse(bodyText) : {};\n } catch {\n payload = { error: bodyText };\n }\n\n if (!response.ok) {\n const upstreamMessage = payload?.error?.message || payload?.message || bodyText || '';\n const error = new Error(upstreamMessage || `LibreChat Agent request failed: ${response.status}`);\n error.statusCode = response.status;\n throw error;\n }\n\n const message = payload?.choices?.[0]?.message?.content?.trim();\n if (!message) {\n const error = new Error('LibreChat Agent returned an empty response');\n error.statusCode = 502;\n throw error;\n }\n\n return {\n message,\n usage: libreChatUsage(payload?.usage),\n };\n}\n\nasync function callLibreChatAgentStream(messages, callbacks = {}) {\n const runtime = createLibreChatAgentRuntime();\n if (!runtime.enabled) {\n const error = new Error(runtime.reason || 'LibreChat Agent chat is not enabled');\n error.statusCode = 503;\n throw error;\n }\n\n const response = await fetch(runtime.chatCompletionsUrl, {\n method: 'POST',\n headers: {\n Accept: 'text/event-stream',\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${libreChatAgentApiKey}`,\n },\n body: JSON.stringify({\n model: runtime.model,\n messages,\n temperature: 0.4,\n stream: true,\n }),\n signal: callbacks.signal || AbortSignal.timeout(180_000),\n });\n\n if (!response.ok) {\n const bodyText = await response.text();\n let payload;\n try {\n payload = bodyText ? JSON.parse(bodyText) : {};\n } catch {\n payload = { error: bodyText };\n }\n const upstreamMessage = payload?.error?.message || payload?.message || bodyText || '';\n const error = new Error(upstreamMessage || `LibreChat Agent request failed: ${response.status}`);\n error.statusCode = response.status;\n throw error;\n }\n\n let buffer = '';\n let message = '';\n let usage = { promptTokens: 0, completionTokens: 0, totalTokens: 0 };\n const decoder = new TextDecoder();\n\n for await (const chunk of response.body) {\n buffer += decoder.decode(chunk, { stream: true });\n const lines = buffer.split(/\\r?\\n/);\n buffer = lines.pop() || '';\n\n for (const rawLine of lines) {\n const line = rawLine.trim();\n if (!line.startsWith('data:')) continue;\n const data = line.slice('data:'.length).trim();\n if (!data || data === '[DONE]') continue;\n\n let payload;\n try {\n payload = JSON.parse(data);\n } catch {\n continue;\n }\n\n const delta = payload?.choices?.[0]?.delta?.content || '';\n if (delta) {\n message += delta;\n callbacks.onToken?.(delta);\n }\n\n if (payload?.usage) {\n usage = libreChatUsage(payload.usage);\n }\n }\n }\n\n message = message.trim();\n if (!message) {\n const error = new Error('LibreChat Agent returned an empty response');\n error.statusCode = 502;\n throw error;\n }\n\n return { message, usage };\n}\n\nfunction isDeepSeekQuotaError(error) {\n if (error?.code === 'DEEPSEEK_PROVIDER_QUOTA') return true;\n if (error?.code === 'USER_TOKEN_QUOTA_EXCEEDED' || error?.code === 'AI_MEMBERSHIP_REQUIRED') return false;\n const message = String(error?.message || error || '');\n return /insufficient\\s+balance|balance/i.test(message);\n}\n\nfunction publicAiErrorMessage(error) {\n if (isDeepSeekQuotaError(error)) {\n return '大乘 AI 服务额度暂不可用,请联系管理员充值 DeepSeek 账户后重试。';\n }\n return error?.message || 'Internal Server Error';\n}\n\nfunction responsesPayload({\n responseId,\n itemId,\n status,\n model,\n text = '',\n usage = null,\n error = null,\n outputItems = null,\n}) {\n return {\n id: responseId,\n object: 'response',\n status,\n model,\n output: status === 'completed'\n ? outputItems || [\n {\n id: itemId,\n type: 'message',\n role: 'assistant',\n status: 'completed',\n content: [{ type: 'output_text', text }],\n },\n ]\n : [],\n ...(usage ? { usage: responseUsage(usage) } : {}),\n ...(error\n ? {\n error: {\n message: error.message || String(error),\n type: 'server_error',\n },\n }\n : {}),\n };\n}\n\nasync function callCodexSdkDeepSeek(messages, callbacks = {}) {\n const codexRuntime = createCodexDeepSeekRuntime(callbacks.user, {\n maxCompletionTokens: callbacks.maxCompletionTokens,\n workingDirectory: callbacks.workingDirectory,\n sandboxMode: callbacks.sandboxMode,\n approvalPolicy: callbacks.approvalPolicy,\n networkAccessEnabled: callbacks.networkAccessEnabled,\n webSearchMode: callbacks.webSearchMode,\n codexHomeDir: callbacks.codexHomeDir,\n env: callbacks.env,\n config: callbacks.config,\n });\n if (!codexRuntime.enabled) {\n const error = new Error(codexRuntime.reason || 'Codex SDK chat is not enabled');\n error.statusCode = 503;\n throw error;\n }\n if (!callbacks.user) {\n const error = new Error('Codex SDK DeepSeek proxy requires a resolved user for billing');\n error.statusCode = 401;\n throw error;\n }\n\n const { Codex } = await import('@openai/codex-sdk');\n const codex = new Codex(codexRuntime.options);\n const thread = callbacks.threadId\n ? codex.resumeThread(callbacks.threadId, codexRuntime.threadOptions)\n : codex.startThread(codexRuntime.threadOptions);\n const prompt = codexPromptFromMessages(messages);\n const turnOptions = {\n ...(callbacks.outputSchema ? { outputSchema: callbacks.outputSchema } : {}),\n ...(callbacks.signal ? { signal: callbacks.signal } : {}),\n };\n\n if (callbacks.onToken || callbacks.onStep) {\n const { events } = await thread.runStreamed(prompt, turnOptions);\n let finalResponse = '';\n let usage = null;\n let threadId = callbacks.threadId || null;\n\n for await (const event of events) {\n if (event.type === 'thread.started') {\n threadId = event.thread_id || threadId;\n } else if (event.type === 'item.completed') {\n const item = event.item;\n if (item.type === 'agent_message') {\n finalResponse = item.text || finalResponse;\n callbacks.onToken?.(item.text || '');\n } else if (item.type === 'todo_list') {\n callbacks.onStep?.({\n title: 'Codex SDK 执行计划',\n message: item.items.map((todo) => `${todo.completed ? '✓' : '•'} ${todo.text}`).join('\\n'),\n });\n } else if (item.type === 'command_execution') {\n callbacks.onStep?.({\n title: 'Codex SDK 执行命令',\n message: item.command,\n });\n } else if (item.type === 'file_change') {\n callbacks.onStep?.({\n title: 'Codex SDK 修改文件',\n message: item.changes.map((change) => `${change.kind}: ${change.path}`).join('\\n'),\n });\n } else if (item.type === 'mcp_tool_call') {\n callbacks.onStep?.({\n title: 'Codex SDK 调用工具',\n message: `${item.server}.${item.tool}`,\n });\n }\n } else if (event.type === 'turn.completed') {\n usage = event.usage;\n } else if (event.type === 'turn.failed') {\n throw new Error(event.error?.message || 'Codex SDK turn failed');\n } else if (event.type === 'error') {\n throw new Error(event.message || 'Codex SDK stream failed');\n }\n }\n\n if (!finalResponse.trim()) {\n const error = new Error('Codex SDK returned an empty response');\n error.statusCode = 502;\n throw error;\n }\n\n return {\n message: finalResponse.trim(),\n usage: {\n promptTokens: Number(usage?.input_tokens || 0),\n completionTokens: Number(usage?.output_tokens || 0),\n totalTokens: Number((usage?.input_tokens || 0) + (usage?.output_tokens || 0)),\n },\n usageAlreadyRecorded: true,\n threadId: thread.id || threadId,\n };\n }\n\n const turn = await thread.run(prompt, turnOptions);\n if (!turn.finalResponse?.trim()) {\n const error = new Error('Codex SDK returned an empty response');\n error.statusCode = 502;\n throw error;\n }\n\n return {\n message: turn.finalResponse.trim(),\n usage: {\n promptTokens: Number(turn.usage?.input_tokens || 0),\n completionTokens: Number(turn.usage?.output_tokens || 0),\n totalTokens: Number((turn.usage?.input_tokens || 0) + (turn.usage?.output_tokens || 0)),\n },\n usageAlreadyRecorded: true,\n threadId: thread.id || callbacks.threadId || null,\n };\n}\n\nconst activeAgentRuns = new Map();\n\nfunction normalizeAgentMode(value) {\n const mode = safeUserText(value || 'dharma_guide');\n return /^[a-z][a-z0-9_:-]{0,48}$/i.test(mode) ? mode : 'dharma_guide';\n}\n\nfunction openClawSessionKey(user, conversationId) {\n const env = process.env.NODE_ENV === 'production' ? 'prod' : 'dev';\n return `fabushi:env:${env}:user:${user.userId}:conversation:${conversationId}`;\n}\n\nfunction createOpenClawRuntime() {\n if (!enableOpenClawAgentChat || !openClawGatewayToken || !openClawGatewayUrl) {\n return {\n enabled: false,\n provider: 'openclaw-gateway',\n reason: !enableOpenClawAgentChat\n ? 'ENABLE_OPENCLAW_AGENT_CHAT is not true'\n : !openClawGatewayToken\n ? 'OPENCLAW_GATEWAY_TOKEN is not configured'\n : 'OPENCLAW_GATEWAY_URL is not configured',\n };\n }\n\n return {\n enabled: true,\n provider: 'openclaw-gateway',\n model: openClawAgentId,\n runsUrl: openClawRunsEndpoint || `${openClawGatewayUrl}/v1/agents/${encodeURIComponent(openClawAgentId)}/runs`,\n };\n}\n\nfunction extractOpenClawText(payload) {\n if (!payload || typeof payload !== 'object') return '';\n const candidates = [\n payload.message,\n payload.content,\n payload.output_text,\n payload.finalResponse,\n payload.final_response,\n payload.result?.message,\n payload.result?.content,\n payload.output?.message,\n payload.output?.content,\n payload.choices?.[0]?.message?.content,\n ];\n for (const candidate of candidates) {\n if (typeof candidate === 'string' && candidate.trim()) return candidate.trim();\n }\n return '';\n}\n\nfunction extractOpenClawUsage(payload) {\n const usage = payload?.usage || payload?.result?.usage || {};\n return {\n promptTokens: Number(usage.input_tokens || usage.prompt_tokens || usage.promptTokens || 0),\n completionTokens: Number(usage.output_tokens || usage.completion_tokens || usage.completionTokens || 0),\n totalTokens: Number(usage.total_tokens || usage.totalTokens || 0),\n };\n}\n\nasync function callOpenClawAgent({ messages, user, conversationId, mode, signal }) {\n const runtime = createOpenClawRuntime();\n if (!runtime.enabled) {\n const error = new Error(runtime.reason || 'OpenClaw Agent chat is not enabled');\n error.statusCode = 503;\n throw error;\n }\n\n const sessionKey = openClawSessionKey(user, conversationId);\n const response = await fetch(runtime.runsUrl, {\n method: 'POST',\n headers: {\n Accept: 'application/json',\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${openClawGatewayToken}`,\n },\n body: JSON.stringify({\n agentId: openClawAgentId,\n sessionKey,\n mode,\n stream: false,\n input: { messages },\n messages,\n metadata: {\n app: 'fabushi',\n userId: user.userId,\n conversationId,\n toolPolicy: 'public-user-read-mostly',\n },\n }),\n signal: signal || AbortSignal.timeout(180_000),\n });\n\n const bodyText = await response.text();\n let payload;\n try {\n payload = bodyText ? JSON.parse(bodyText) : {};\n } catch {\n payload = { message: bodyText };\n }\n\n if (!response.ok) {\n const error = new Error(payload?.error?.message || payload?.message || `OpenClaw request failed: ${response.status}`);\n error.statusCode = response.status;\n throw error;\n }\n\n const message = extractOpenClawText(payload);\n if (!message) {\n const error = new Error('OpenClaw returned an empty response');\n error.statusCode = 502;\n throw error;\n }\n\n return {\n message,\n runId: payload.runId || payload.id || payload.result?.runId || null,\n provider: runtime.provider,\n model: runtime.model,\n usage: extractOpenClawUsage(payload),\n };\n}\n\nasync function runAgentModel({\n messages,\n user,\n conversationId,\n mode,\n callbacks = {},\n signal,\n maxCompletionTokens,\n preferCodex = false,\n requireCodex = false,\n workingDirectory,\n sandboxMode,\n outputSchema,\n threadId,\n}) {\n const codexRuntime = createCodexDeepSeekRuntime(user, {\n workingDirectory,\n sandboxMode,\n maxCompletionTokens,\n });\n if (requireCodex && !codexRuntime.enabled) {\n const error = new Error(codexRuntime.reason || 'Codex SDK chat is not enabled');\n error.statusCode = 503;\n throw error;\n }\n\n let preferredCodexFailed = false;\n if ((preferCodex || requireCodex) && codexRuntime.enabled) {\n try {\n const result = await callCodexSdkDeepSeek(messages, {\n ...callbacks,\n user,\n signal,\n maxCompletionTokens,\n workingDirectory,\n sandboxMode,\n outputSchema,\n threadId,\n });\n return {\n ...result,\n provider: codexRuntime.provider,\n model: deepseekModel,\n };\n } catch (error) {\n if (requireCodex || isDeepSeekQuotaError(error)) throw error;\n logger.warn(\n { error: String(error), mode },\n 'Preferred Codex SDK model failed, continuing through agent fallbacks',\n );\n preferredCodexFailed = true;\n }\n }\n\n const openClawRuntime = createOpenClawRuntime();\n if (openClawRuntime.enabled && !callbacks.onToken) {\n return await callOpenClawAgent({ messages, user, conversationId, mode, signal });\n }\n\n const libreChatRuntime = createLibreChatAgentRuntime();\n let provider = libreChatRuntime.enabled\n ? libreChatRuntime.provider\n : codexRuntime.enabled && !preferredCodexFailed\n ? codexRuntime.provider\n : 'deepseek';\n let model = libreChatRuntime.enabled ? libreChatRuntime.model : deepseekModel;\n\n try {\n if (libreChatRuntime.enabled) {\n const result = callbacks.onToken\n ? await callLibreChatAgentStream(messages, { ...callbacks, signal })\n : await callLibreChatAgent(messages);\n return { ...result, provider, model };\n }\n if (codexRuntime.enabled && !preferredCodexFailed) {\n const result = await callCodexSdkDeepSeek(messages, {\n ...callbacks,\n user,\n signal,\n maxCompletionTokens,\n workingDirectory,\n sandboxMode,\n outputSchema,\n threadId,\n });\n return { ...result, provider, model };\n }\n const result = callbacks.onToken\n ? await callDeepSeekStream(messages, { ...callbacks, signal, maxCompletionTokens })\n : await callDeepSeek(messages, { maxCompletionTokens });\n return { ...result, provider, model };\n } catch (error) {\n if (!codexRuntime.enabled || preferredCodexFailed || isDeepSeekQuotaError(error)) throw error;\n logger.warn({ error: String(error) }, 'Agent model failed, falling back to DeepSeek direct');\n const result = callbacks.onToken\n ? await callDeepSeekStream(messages, { ...callbacks, signal, maxCompletionTokens })\n : await callDeepSeek(messages, { maxCompletionTokens });\n return { ...result, provider: 'deepseek', model: deepseekModel };\n }\n}\n\nfunction updateAgentRunStatus(run, status, fields = {}) {\n statements.updateAgentRunStatus.run({\n id: run.id,\n userId: run.user_id || run.userId,\n status,\n openClawRunId: fields.openClawRunId ?? null,\n inputTokens: fields.inputTokens ?? null,\n outputTokens: fields.outputTokens ?? null,\n toolCallCount: fields.toolCallCount ?? null,\n completedAt: status === 'completed' || status === 'cancelled' ? nowIso() : null,\n failedAt: status === 'failed' ? nowIso() : null,\n errorCode: fields.errorCode ?? null,\n errorMessage: fields.errorMessage ?? null,\n });\n}\n\nfunction buildAgentMessages(conversationId) {\n const historyRows = statements.listMessages.all(conversationId).slice(-14);\n return [{ role: 'system', content: systemPrompt }, ...normalizeMessages(historyRows)];\n}\n\nfunction agentUsagePayload(usage, user, budget) {\n const promptTokens = usage.promptTokens || usage.inputTokens || 0;\n const completionTokens = usage.completionTokens || usage.outputTokens || 0;\n const totalTokens = usage.totalTokens || promptTokens + completionTokens;\n const latestUsage = usageFor(user.userId);\n return {\n inputTokens: promptTokens,\n outputTokens: completionTokens,\n totalTokens,\n monthlyLimit: budget.limit,\n remainingTokens: Math.max(0, budget.limit - latestUsage.used),\n };\n}\n\nfunction openAiContentToText(content) {\n if (typeof content === 'string') return content;\n if (Array.isArray(content)) {\n return content\n .map((part) => {\n if (typeof part === 'string') return part;\n if (!part || typeof part !== 'object') return '';\n return readText(part.text || part.content || part.value);\n })\n .filter(Boolean)\n .join('\\n');\n }\n return readText(content);\n}\n\nfunction normalizeOpenAiMessages(value) {\n if (!Array.isArray(value)) return [];\n const allowedRoles = new Set(['system', 'user', 'assistant', 'tool']);\n return value\n .map((item) => {\n if (!item || typeof item !== 'object') return null;\n const role = allowedRoles.has(item.role) ? item.role : 'user';\n const content = openAiContentToText(item.content);\n if (!content) return null;\n return { role, content };\n })\n .filter(Boolean);\n}\n\nfunction truncateForDeepSeekMessage(text, maxChars = 12_000) {\n const value = readText(text);\n if (value.length <= maxChars) return value;\n return `${value.slice(0, maxChars)}\\n\\n[内容已截断]`;\n}\n\nfunction truncateTailForDeepSeekMessage(text, maxChars = 2400) {\n const value = readText(text);\n if (value.length <= maxChars) return value;\n return `[前文已省略]\\n\\n${value.slice(-maxChars)}`;\n}\n\nfunction stripOpenClawPromptNoise(text) {\n const value = readText(text);\n if (!value) return '';\n\n const finalPromptMatch = value.match(/finalPromptText[\"'\\s::]+([\\s\\S]{1,4000})$/i);\n if (finalPromptMatch?.[1]) {\n return readText(finalPromptMatch[1].replace(/^[\"'`]+|[\"'`,}\\]]+$/g, ''));\n }\n\n const markers = [\n 'final prompt:',\n 'final user prompt:',\n 'user prompt:',\n '用户问题:',\n '用户消息:',\n '用户输入:',\n ];\n const lower = value.toLowerCase();\n for (const marker of markers) {\n const index = lower.lastIndexOf(marker.toLowerCase());\n if (index >= 0) {\n const tail = value.slice(index + marker.length).trim();\n if (tail) return tail;\n }\n }\n\n return value;\n}\n\nfunction compactOpenClawMessagesForDeepSeek(messages) {\n const conversationalMessages = messages\n .filter((message) => message.role === 'user' || message.role === 'assistant')\n .map((message) => ({\n role: message.role,\n content: truncateTailForDeepSeekMessage(stripOpenClawPromptNoise(message.content)),\n }))\n .filter((message) => message.content);\n\n const recent = conversationalMessages.slice(-6);\n while (recent.length > 0 && recent[0].role !== 'user') {\n recent.shift();\n }\n\n if (recent.length === 0) {\n const fallback = messages\n .slice()\n .reverse()\n .map((message) => truncateTailForDeepSeekMessage(stripOpenClawPromptNoise(message.content), 2400))\n .find(Boolean);\n if (!fallback) return messages;\n recent.push({ role: 'user', content: fallback });\n }\n\n return [\n {\n role: 'system',\n content: '你是大乘 App 的桌面 AI 助手。请直接回答用户问题,默认使用简洁、准确的中文。',\n },\n ...recent,\n ];\n}\n\nfunction isDeepSeekBlockedError(error) {\n return (\n Number(error?.statusCode) === 403 &&\n /blocked|被拦截|forbidden/i.test(String(error?.message || error || ''))\n );\n}\n\nfunction shouldRetryWithCompactedOpenClawMessages(error, messages, compactedMessages) {\n if (!isDeepSeekBlockedError(error)) return false;\n return JSON.stringify(messages) !== JSON.stringify(compactedMessages);\n}\n\nfunction openClawBlockedFallbackText(messages) {\n const lastUser = messages\n .slice()\n .reverse()\n .find((message) => message.role === 'user')?.content || '';\n const localActionIntent =\n /(file|folder|directory|project|workspace|path|文件|文件夹|目录|项目|工程|路径|本地)/i.test(lastUser) &&\n /(create|make|mkdir|write|edit|modify|rename|delete|move|copy|save|download|read|list|生成|创建|新建|写入|编辑|修改|重命名|删除|移动|复制|保存|下载|读取|列出)/i.test(lastUser);\n\n if (localActionIntent) {\n return [\n '本机工具规划被模型服务拦截了,所以这次没有实际读取、创建、修改或下载本机文件。',\n '如果只是新建文件夹,请先在底部选择项目后重新发送“新建文件夹”;桌面端会直接在已选项目里创建。',\n '如果是更复杂的本地文件操作,请在“连应用”里重启本机 AI 后重试,或把要操作的文件路径和目标说得更具体。',\n ].join('\\n');\n }\n\n return '模型服务拦截了这次回复,未能生成结果。请换一种更具体、更短的表述后重试。';\n}\nfunction usageWithFallback(usage, messages, text) {\n const promptTokens = Number(usage?.promptTokens || usage?.inputTokens || 0);\n const completionTokens = Number(usage?.completionTokens || usage?.outputTokens || 0);\n const totalTokens = Number(usage?.totalTokens || 0);\n if (totalTokens > 0) {\n return {\n promptTokens,\n completionTokens,\n totalTokens,\n };\n }\n const estimatedPrompt = promptTokens || estimateTokens(JSON.stringify(messages));\n const estimatedCompletion = completionTokens || estimateTokens(text);\n return {\n promptTokens: estimatedPrompt,\n completionTokens: estimatedCompletion,\n totalTokens: estimatedPrompt + estimatedCompletion,\n };\n}\n\nfunction openAiUsagePayload(usage, user, budget) {\n const promptTokens = Number(usage.promptTokens || 0);\n const completionTokens = Number(usage.completionTokens || 0);\n const totalTokens = Number(usage.totalTokens || promptTokens + completionTokens);\n const latestUsage = usageFor(user.userId);\n const remainingTokens = Math.max(0, budget.limit - latestUsage.used);\n return {\n prompt_tokens: promptTokens,\n completion_tokens: completionTokens,\n total_tokens: totalTokens,\n promptTokens,\n completionTokens,\n totalTokens,\n monthlyLimit: budget.limit,\n remainingTokens,\n };\n}\n\nfunction writeOpenAiSse(res, payload) {\n res.write(`data: ${JSON.stringify(payload)}\\n\\n`);\n}\n\napp.post(\n '/api/openclaw/deepseek/v1/chat/completions',\n asyncHandler(async (req, res) => {\n const user = await resolveUser(req, req.body || {});\n const incomingMessages = normalizeOpenAiMessages(req.body?.messages);\n if (incomingMessages.length === 0) {\n res.status(400).json({ error: { message: 'messages is required' } });\n return;\n }\n\n const messages = compactOpenClawMessagesForDeepSeek(incomingMessages);\n const promptEstimate = estimateTokens(JSON.stringify(messages));\n const estimated = promptEstimate + 600;\n const budget = enforceTokenBudget(user, estimated);\n const requestCompletionLimit = deepseekMaxCompletionTokens;\n const maxCompletionTokens = Math.min(\n completionBudgetFor(budget, promptEstimate, requestCompletionLimit),\n normalizeMaxCompletionTokens(req.body?.max_tokens, requestCompletionLimit),\n );\n const model = normalizeDeepSeekModelName(req.body?.model);\n const wantsStream =\n req.body?.stream === true ||\n String(req.get('accept') || '').includes('text/event-stream');\n const id = `chatcmpl_${crypto.randomUUID().replaceAll('-', '')}`;\n const created = Math.floor(Date.now() / 1000);\n const compactedMessages = messages;\n\n if (!wantsStream) {\n let result;\n let usageMessages = messages;\n try {\n result = await callDeepSeek(messages, {\n model,\n maxCompletionTokens,\n maximumCompletionTokens: requestCompletionLimit,\n });\n } catch (error) {\n if (!shouldRetryWithCompactedOpenClawMessages(error, messages, compactedMessages)) {\n throw error;\n }\n logger.warn(\n {\n statusCode: error?.statusCode,\n messageCount: messages.length,\n compactedMessageCount: compactedMessages.length,\n },\n 'OpenClaw DeepSeek proxy retrying with compacted messages',\n );\n usageMessages = compactedMessages;\n result = await callDeepSeek(compactedMessages, {\n model,\n maxCompletionTokens,\n maximumCompletionTokens: requestCompletionLimit,\n });\n }\n const usage = usageWithFallback(result.usage, usageMessages, result.message);\n recordUsage(user.userId, usage.totalTokens);\n res.json({\n id,\n object: 'chat.completion',\n created,\n model: result.model || model,\n choices: [\n {\n index: 0,\n message: { role: 'assistant', content: result.message },\n finish_reason: 'stop',\n },\n ],\n usage: openAiUsagePayload(usage, user, budget),\n });\n return;\n }\n\n const abortController = new AbortController();\n req.on('close', () => abortController.abort());\n\n res.status(200);\n res.set({\n 'Content-Type': 'text/event-stream; charset=utf-8',\n 'Cache-Control': 'no-cache, no-transform',\n Connection: 'keep-alive',\n 'X-Accel-Buffering': 'no',\n });\n res.flushHeaders?.();\n\n const baseChunk = {\n id,\n object: 'chat.completion.chunk',\n created,\n model,\n system_fingerprint: 'fabushi-openclaw-deepseek-proxy',\n };\n writeOpenAiSse(res, {\n ...baseChunk,\n choices: [{ index: 0, delta: { role: 'assistant' }, finish_reason: null }],\n });\n\n let text = '';\n let usageMessages = messages;\n try {\n let result;\n try {\n result = await callDeepSeekStream(messages, {\n model,\n maxCompletionTokens,\n maximumCompletionTokens: requestCompletionLimit,\n signal: abortController.signal,\n onToken: (delta) => {\n text += delta;\n writeOpenAiSse(res, {\n ...baseChunk,\n choices: [{ index: 0, delta: { content: delta }, finish_reason: null }],\n });\n },\n });\n } catch (error) {\n if (!shouldRetryWithCompactedOpenClawMessages(error, messages, compactedMessages)) {\n throw error;\n }\n logger.warn(\n {\n statusCode: error?.statusCode,\n messageCount: messages.length,\n compactedMessageCount: compactedMessages.length,\n },\n 'OpenClaw DeepSeek proxy stream retrying with compacted messages',\n );\n text = '';\n usageMessages = compactedMessages;\n result = await callDeepSeekStream(compactedMessages, {\n model,\n maxCompletionTokens,\n maximumCompletionTokens: requestCompletionLimit,\n signal: abortController.signal,\n onToken: (delta) => {\n text += delta;\n writeOpenAiSse(res, {\n ...baseChunk,\n choices: [{ index: 0, delta: { content: delta }, finish_reason: null }],\n });\n },\n });\n }\n text = result.message || text;\n const usage = usageWithFallback(result.usage, usageMessages, text);\n recordUsage(user.userId, usage.totalTokens);\n writeOpenAiSse(res, {\n ...baseChunk,\n model: result.model || model,\n choices: [{ index: 0, delta: {}, finish_reason: 'stop' }],\n });\n writeOpenAiSse(res, {\n ...baseChunk,\n model: result.model || model,\n choices: [],\n usage: openAiUsagePayload(usage, user, budget),\n });\n res.write('data: [DONE]\\n\\n');\n } catch (error) {\n if (isDeepSeekBlockedError(error)) {\n const fallbackMessage = openClawBlockedFallbackText(usageMessages);\n const usage = usageWithFallback(null, usageMessages, fallbackMessage);\n recordUsage(user.userId, usage.totalTokens);\n logger.warn(\n { statusCode: error?.statusCode, code: error?.code },\n 'OpenClaw DeepSeek proxy stream blocked; returning local fallback message',\n );\n writeOpenAiSse(res, {\n ...baseChunk,\n choices: [{ index: 0, delta: { content: fallbackMessage }, finish_reason: null }],\n });\n writeOpenAiSse(res, {\n ...baseChunk,\n choices: [{ index: 0, delta: {}, finish_reason: 'stop' }],\n });\n writeOpenAiSse(res, {\n ...baseChunk,\n choices: [],\n usage: openAiUsagePayload(usage, user, budget),\n });\n res.write('data: [DONE]\\n\\n');\n return;\n }\n logger.error(\n { error: String(error), code: error?.code, statusCode: error?.statusCode },\n 'OpenClaw DeepSeek proxy stream failed',\n );\n writeOpenAiSse(res, {\n ...baseChunk,\n choices: [],\n error: {\n message: publicAiErrorMessage(error),\n code: error?.code || 'OPENCLAW_DEEPSEEK_PROXY_ERROR',\n },\n });\n res.write('data: [DONE]\\n\\n');\n } finally {\n res.end();\n }\n }),\n);\n\napp.post(\n '/codex-deepseek/v1/responses',\n asyncHandler(async (req, res) => {\n if (!deepseekApiKey) {\n res.status(401).json({\n error: { message: 'Unauthorized Codex DeepSeek adapter request' },\n });\n return;\n }\n\n const messages = codexResponsesMessages(req.body);\n if (messages.length === 0) {\n res.status(400).json({ error: { message: 'Responses input is required' } });\n return;\n }\n const { tools: responseTools, kinds: responseToolKinds } = codexResponsesTools(req.body);\n\n const {\n user,\n recordUsage: shouldRecordUsage,\n maxCompletionTokens: adapterMaxCompletionTokens,\n } = await resolveCodexResponsesBilling(req);\n const promptEstimate = estimateTokens(JSON.stringify({ messages, tools: responseTools }));\n const budget = enforceTokenBudget(user, promptEstimate + 600);\n const responseCompletionLimit = adapterMaxCompletionTokens || deepseekMaxCompletionTokens;\n const maxCompletionTokens = Math.min(\n completionBudgetFor(budget, promptEstimate, responseCompletionLimit),\n normalizeMaxCompletionTokens(\n req.body?.max_output_tokens || req.body?.max_tokens,\n responseCompletionLimit,\n ),\n );\n const model = normalizeDeepSeekModelName(req.body?.model);\n const responseId = `resp_${crypto.randomUUID().replaceAll('-', '')}`;\n const itemId = `msg_${crypto.randomUUID().replaceAll('-', '')}`;\n const wantsStream =\n req.body?.stream !== false ||\n String(req.get('accept') || '').includes('text/event-stream');\n\n if (!wantsStream) {\n const result = await callDeepSeek(messages, {\n model,\n maxCompletionTokens,\n maximumCompletionTokens: responseCompletionLimit,\n tools: responseTools,\n });\n const outputItems = deepSeekResultToResponseItems(\n result,\n responseToolKinds,\n (prefix) => `${prefix}_${crypto.randomUUID().replaceAll('-', '')}`,\n );\n const usage = usageWithFallback(\n result.usage,\n messages,\n result.message || JSON.stringify(result.toolCalls || []),\n );\n if (shouldRecordUsage) recordUsage(user.userId, usage.totalTokens);\n res.json(\n responsesPayload({\n responseId,\n itemId,\n status: 'completed',\n model: result.model || model,\n text: result.message,\n usage,\n outputItems,\n }),\n );\n return;\n }\n\n if (responseTools.length > 0) {\n res.status(200);\n res.set({\n 'Content-Type': 'text/event-stream; charset=utf-8',\n 'Cache-Control': 'no-cache, no-transform',\n Connection: 'keep-alive',\n 'X-Accel-Buffering': 'no',\n });\n res.flushHeaders?.();\n writeResponsesEvent(res, 'response.created', {\n type: 'response.created',\n response: responsesPayload({ responseId, itemId, status: 'in_progress', model }),\n });\n try {\n const result = await callDeepSeek(messages, {\n model,\n maxCompletionTokens,\n maximumCompletionTokens: responseCompletionLimit,\n tools: responseTools,\n });\n const outputItems = deepSeekResultToResponseItems(\n result,\n responseToolKinds,\n (prefix) => `${prefix}_${crypto.randomUUID().replaceAll('-', '')}`,\n );\n outputItems.forEach((item, outputIndex) => {\n const addedItem = item.type === 'message'\n ? { ...item, status: 'in_progress', content: [] }\n : { ...item, status: 'in_progress' };\n writeResponsesEvent(res, 'response.output_item.added', {\n type: 'response.output_item.added',\n output_index: outputIndex,\n item: addedItem,\n });\n if (item.type === 'message') {\n const text = item.content?.find((part) => part.type === 'output_text')?.text || '';\n if (text) {\n writeResponsesEvent(res, 'response.output_text.delta', {\n type: 'response.output_text.delta',\n item_id: item.id,\n output_index: outputIndex,\n content_index: 0,\n delta: text,\n });\n }\n }\n writeResponsesEvent(res, 'response.output_item.done', {\n type: 'response.output_item.done',\n output_index: outputIndex,\n item,\n });\n });\n const usage = usageWithFallback(\n result.usage,\n messages,\n result.message || JSON.stringify(result.toolCalls || []),\n );\n if (shouldRecordUsage) recordUsage(user.userId, usage.totalTokens);\n writeResponsesEvent(res, 'response.completed', {\n type: 'response.completed',\n response: responsesPayload({\n responseId,\n itemId,\n status: 'completed',\n model: result.model || model,\n text: result.message,\n usage,\n outputItems,\n }),\n });\n } catch (error) {\n writeResponsesEvent(res, 'response.failed', {\n type: 'response.failed',\n response: responsesPayload({\n responseId,\n itemId,\n status: 'failed',\n model,\n error,\n }),\n });\n }\n res.end();\n return;\n }\n\n res.status(200);\n res.set({\n 'Content-Type': 'text/event-stream; charset=utf-8',\n 'Cache-Control': 'no-cache, no-transform',\n Connection: 'keep-alive',\n 'X-Accel-Buffering': 'no',\n });\n res.flushHeaders?.();\n\n writeResponsesEvent(res, 'response.created', {\n type: 'response.created',\n response: responsesPayload({\n responseId,\n itemId,\n status: 'in_progress',\n model,\n }),\n });\n writeResponsesEvent(res, 'response.output_item.added', {\n type: 'response.output_item.added',\n output_index: 0,\n item: {\n id: itemId,\n type: 'message',\n role: 'assistant',\n status: 'in_progress',\n content: [],\n },\n });\n writeResponsesEvent(res, 'response.content_part.added', {\n type: 'response.content_part.added',\n item_id: itemId,\n output_index: 0,\n content_index: 0,\n part: { type: 'output_text', text: '' },\n });\n\n let text = '';\n try {\n const result = await callDeepSeekStream(messages, {\n model,\n maxCompletionTokens,\n maximumCompletionTokens: responseCompletionLimit,\n onToken: (delta) => {\n text += delta;\n writeResponsesEvent(res, 'response.output_text.delta', {\n type: 'response.output_text.delta',\n item_id: itemId,\n output_index: 0,\n content_index: 0,\n delta,\n });\n },\n });\n text = result.message || text;\n const usage = usageWithFallback(result.usage, messages, text);\n if (shouldRecordUsage) recordUsage(user.userId, usage.totalTokens);\n writeResponsesEvent(res, 'response.output_text.done', {\n type: 'response.output_text.done',\n item_id: itemId,\n output_index: 0,\n content_index: 0,\n text,\n });\n writeResponsesEvent(res, 'response.content_part.done', {\n type: 'response.content_part.done',\n item_id: itemId,\n output_index: 0,\n content_index: 0,\n part: { type: 'output_text', text },\n });\n writeResponsesEvent(res, 'response.output_item.done', {\n type: 'response.output_item.done',\n output_index: 0,\n item: {\n id: itemId,\n type: 'message',\n role: 'assistant',\n status: 'completed',\n content: [{ type: 'output_text', text }],\n },\n });\n writeResponsesEvent(res, 'response.completed', {\n type: 'response.completed',\n response: responsesPayload({\n responseId,\n itemId,\n status: 'completed',\n model: result.model || model,\n text,\n usage,\n }),\n });\n res.end();\n } catch (error) {\n writeResponsesEvent(res, 'response.failed', {\n type: 'response.failed',\n response: responsesPayload({\n responseId,\n itemId,\n status: 'failed',\n model,\n text,\n error,\n }),\n });\n res.end();\n }\n }),\n);\n\napp.use(\n '/api/openclaw/runtime/files',\n express.static(openClawRuntimeUpdatesDir, {\n fallthrough: false,\n immutable: true,\n maxAge: '7d',\n setHeaders(res) {\n res.setHeader('X-Content-Type-Options', 'nosniff');\n },\n }),\n);\n\napp.get(\n '/api/openclaw/runtime/manifest',\n asyncHandler(async (req, res) => {\n const platform = readText(req.query.platform);\n const currentVersion = readText(req.query.currentVersion);\n const appVersion = readText(req.query.appVersion);\n\n let manifest;\n try {\n manifest = await readJsonFile(openClawRuntimeManifestPath);\n } catch (error) {\n logger.info(\n { manifestPath: openClawRuntimeManifestPath, error: String(error) },\n 'OpenClaw runtime manifest unavailable',\n );\n return jsonResponse(res, 200, {\n success: true,\n schema: 1,\n updateAvailable: false,\n reason: 'manifest-unavailable',\n platform,\n currentVersion,\n appVersion,\n });\n }\n\n const latest = manifest.latest && typeof manifest.latest === 'object'\n ? manifest.latest\n : manifest;\n const platforms = latest.platforms && typeof latest.platforms === 'object'\n ? latest.platforms\n : {};\n const platformRelease = platform ? platforms[platform] : null;\n const version = readText(latest.version || manifest.version);\n\n if (!platform || !platformRelease || typeof platformRelease !== 'object') {\n return jsonResponse(res, 200, {\n success: true,\n schema: Number(manifest.schema || 1),\n channel: readText(manifest.channel || latest.channel || 'stable'),\n updateAvailable: false,\n reason: platform ? 'platform-unavailable' : 'platform-required',\n platform,\n currentVersion,\n appVersion,\n latestVersion: version,\n });\n }\n\n if (platform.startsWith('macos-') && !openClawRuntimeEnableMacosUpdates) {\n return jsonResponse(res, 200, {\n success: true,\n schema: Number(manifest.schema || 1),\n channel: readText(manifest.channel || latest.channel || 'stable'),\n updateAvailable: false,\n reason: 'macos-runtime-updates-disabled',\n platform,\n currentVersion,\n appVersion,\n latestVersion: version,\n });\n }\n\n const downloadUrl = absolutizeOpenClawRuntimeUrl(\n req,\n platformRelease.archiveUrl || platformRelease.url || platformRelease.fileName,\n );\n const release = {\n version,\n channel: readText(manifest.channel || latest.channel || 'stable'),\n minAppVersion: readText(latest.minAppVersion || platformRelease.minAppVersion),\n notes: readText(latest.notes || platformRelease.notes),\n nodeExecutable: readText(\n platformRelease.nodeExecutable || latest.nodeExecutable || 'node/bin/node',\n ),\n cliEntrypoint: readText(\n platformRelease.cliEntrypoint || latest.cliEntrypoint || 'openclaw/openclaw.mjs',\n ),\n defaultPort: Number(latest.defaultPort || manifest.defaultPort || 18789),\n defaultModel: readText(latest.defaultModel || manifest.defaultModel || 'openclaw/default'),\n defaultModelOverride: readText(latest.defaultModelOverride || manifest.defaultModelOverride),\n gatewayArgs: Array.isArray(latest.gatewayArgs)\n ? latest.gatewayArgs.map((item) => String(item))\n : ['gateway', '--port', '{port}', '--force'],\n archiveUrl: downloadUrl,\n sha256: readText(platformRelease.sha256),\n size: Number(platformRelease.size || 0),\n };\n\n jsonResponse(res, 200, {\n success: true,\n schema: Number(manifest.schema || 1),\n updateAvailable: Boolean(version && downloadUrl && version !== currentVersion),\n platform,\n currentVersion,\n appVersion,\n latest: release,\n generatedAt: readText(manifest.generatedAt || latest.generatedAt),\n });\n }),\n);\n\napp.get('/health', (_req, res) => {\n const codexRuntime = createCodexDeepSeekRuntime();\n const libreChatRuntime = createLibreChatAgentRuntime();\n const openClawRuntime = createOpenClawRuntime();\n jsonResponse(res, 200, {\n status: 'ok',\n service: 'dacheng-ai-backend',\n provider: libreChatRuntime.enabled ? libreChatRuntime.provider : 'deepseek',\n model: libreChatRuntime.enabled ? libreChatRuntime.model : deepseekModel,\n deepseekModel,\n deepseekAvailableModels,\n libreChatAgentChatEnabled: libreChatRuntime.enabled,\n libreChatAgentProvider: libreChatRuntime.provider,\n libreChatAgentReason: libreChatRuntime.enabled ? undefined : libreChatRuntime.reason,\n codexSdkAvailable: true,\n codexSdkChatEnabled: codexRuntime.enabled,\n codexSdkProvider: codexRuntime.provider,\n openClawAgentChatEnabled: openClawRuntime.enabled,\n openClawAgentProvider: openClawRuntime.provider,\n openClawAgentReason: openClawRuntime.enabled ? undefined : openClawRuntime.reason,\n requireMemberForAi,\n memberMonthlyLimit,\n freeMonthlyLimit,\n deepseekMaxCompletionTokens,\n botFatherMaxCompletionTokens,\n openClawRuntimeUpdatesDir,\n openClawRuntimeManifestConfigured: fs.existsSync(openClawRuntimeManifestPath),\n cbetaApiRoot,\n timestamp: nowIso(),\n });\n});\n\napp.get(\n '/v1/ai/usage',\n asyncHandler(async (req, res) => {\n const user = await resolveUser(req, {});\n const now = new Date();\n const windowStart = Math.floor(Date.UTC(\n now.getUTCFullYear(), now.getUTCMonth(), 1,\n ) / 1000);\n const windowEnd = Math.floor(Date.UTC(\n now.getUTCFullYear(), now.getUTCMonth() + 1, 1,\n ) / 1000);\n const tokenLimit = user.isTestAccount\n ? 999_999_999\n : user.isMember ? memberMonthlyLimit : freeMonthlyLimit;\n const usedTokens = user.isTestAccount ? 0 : usageFor(user.userId).used;\n jsonResponse(res, 200, {\n windowStart,\n windowEnd,\n tokenLimit,\n usedTokens,\n reservedTokens: 0,\n remainingTokens: Math.max(0, tokenLimit - usedTokens),\n });\n }),\n);\n\napp.get('/api/plugins/registry', (_req, res) => {\n jsonResponse(res, 200, {\n success: true,\n schemaVersion: 1,\n protocol: 'mcp',\n plugins: officialMcpApps,\n updatedAt: nowIso(),\n });\n});\n\napp.all('/api/mcp/apps/:pluginId', async (req, res) => {\n try {\n const user = await resolveUser(req, req.body || {});\n await handleOfficialMcpRequest(req.params.pluginId, req, res, user.userId);\n } catch (error) {\n logger.error({ error: error.stack || String(error) }, 'official MCP app request failed');\n if (!res.headersSent) {\n res.status(500).json({ error: 'MCP request failed', message: publicAiErrorMessage(error) });\n }\n }\n});\n\napp.post(\n '/api/codex/apps/:pluginId/turns',\n asyncHandler(async (req, res) => {\n const pluginId = safeUserText(req.params.pluginId).replace(/^official\\./, '');\n const plugin = officialMcpApps.find((candidate) => candidate.id === pluginId);\n if (!plugin) {\n return jsonResponse(res, 404, { success: false, message: 'plugin not found' });\n }\n\n const message = safeUserText(req.body?.message);\n if (!message) {\n return jsonResponse(res, 400, { success: false, message: 'message is required' });\n }\n\n const user = await resolveUser(req, req.body || {});\n const promptEstimate = estimateTokens(message);\n const budget = enforceTokenBudget(user, promptEstimate + 600);\n const maxCompletionTokens = completionBudgetFor(budget, promptEstimate);\n const namespace = pluginConversationNamespace(pluginId);\n const requestedConversationId = safeUserText(req.body?.conversationId);\n if (requestedConversationId && !requestedConversationId.startsWith(`${namespace}:`)) {\n return jsonResponse(res, 400, {\n success: false,\n message: 'conversation does not belong to the selected plugin namespace',\n });\n }\n\n const conversationId = requestedConversationId || `${namespace}:${crypto.randomUUID()}`;\n const createdAt = nowIso();\n let conversation = statements.getPluginConversation.get(\n conversationId,\n user.userId,\n namespace,\n );\n if (requestedConversationId && !conversation) {\n return jsonResponse(res, 404, {\n success: false,\n message: 'plugin conversation not found for this account',\n });\n }\n const isNewConversation = !conversation;\n if (isNewConversation) {\n conversation = {\n id: conversationId,\n user_id: user.userId,\n title: titleFromMessage(message),\n namespace,\n thread_id: null,\n };\n }\n\n const accountKey = sha256(user.userId).slice(0, 24);\n const pluginCodexHome = path.join(codexPluginSessionsDir, accountKey, pluginId);\n fs.mkdirSync(pluginCodexHome, { recursive: true });\n const mcpAccountToken = createCodexAdapterToken(user, {\n audience: 'mcp-plugin',\n recordUsage: false,\n });\n const mcpOrigin = (\n process.env.MCP_INTERNAL_BASE_URL || `http://127.0.0.1:${process.env.PORT || 8788}`\n ).replace(/\\/+$/, '');\n const isolationPolicy = createPluginCodexPolicy(pluginId, mcpOrigin);\n const firstTurnMessages = [\n {\n role: 'system',\n content:\n `你正在 ${plugin.title} 插件的隔离 Codex 会话中。` +\n '只能使用 current_plugin MCP Server 暴露的 Tools;禁止 shell、文件系统、Web 搜索和其他插件。' +\n '只读 Tool 可以按需调用。需要写入、外部操作或破坏性操作时,不要自行执行,明确告诉用户改用对应的 /Tool 命令,让宿主显示审批。',\n },\n { role: 'user', content: message },\n ];\n const aiResult = await callCodexSdkDeepSeek(\n conversation?.thread_id ? [{ role: 'user', content: message }] : firstTurnMessages,\n {\n user,\n threadId: conversation?.thread_id || undefined,\n maxCompletionTokens,\n sandboxMode: isolationPolicy.sandboxMode,\n approvalPolicy: isolationPolicy.approvalPolicy,\n networkAccessEnabled: isolationPolicy.networkAccessEnabled,\n webSearchMode: isolationPolicy.webSearchMode,\n codexHomeDir: pluginCodexHome,\n env: { [pluginMcpTokenEnv]: mcpAccountToken },\n config: isolationPolicy.config,\n },\n );\n const usage = {\n promptTokens: aiResult.usage.promptTokens || promptEstimate,\n completionTokens: aiResult.usage.completionTokens || estimateTokens(aiResult.message),\n totalTokens:\n aiResult.usage.totalTokens || promptEstimate + estimateTokens(aiResult.message),\n };\n if (!aiResult.usageAlreadyRecorded) recordUsage(user.userId, usage.totalTokens);\n\n const answeredAt = nowIso();\n if (isNewConversation) {\n statements.insertPluginConversation.run({\n id: conversationId,\n userId: user.userId,\n title: conversation.title,\n provider: codexDeepSeekProviderId,\n model: deepseekModel,\n namespace,\n createdAt,\n updatedAt: answeredAt,\n });\n }\n statements.insertMessage.run({\n id: crypto.randomUUID(),\n conversationId,\n role: 'user',\n content: message,\n promptTokens: 0,\n completionTokens: 0,\n totalTokens: 0,\n createdAt,\n });\n statements.insertMessage.run({\n id: crypto.randomUUID(),\n conversationId,\n role: 'assistant',\n content: aiResult.message,\n promptTokens: usage.promptTokens,\n completionTokens: usage.completionTokens,\n totalTokens: usage.totalTokens,\n createdAt: answeredAt,\n });\n statements.updatePluginConversation.run({\n id: conversationId,\n userId: user.userId,\n namespace,\n title: conversation.title,\n threadId: aiResult.threadId || conversation?.thread_id || null,\n updatedAt: answeredAt,\n });\n\n jsonResponse(res, 200, {\n success: true,\n pluginId,\n conversationId,\n message: aiResult.message,\n provider: codexDeepSeekProviderId,\n model: deepseekModel,\n usage: {\n ...usage,\n monthlyLimit: budget.limit,\n remainingTokens: Math.max(0, budget.limit - usageFor(user.userId).used),\n },\n });\n }),\n);\n\napp.get('/api/ai/models', (_req, res) => {\n jsonResponse(res, 200, {\n success: true,\n provider: 'deepseek',\n defaultModel: deepseekModel,\n models: deepseekAvailableModels.map((model) => ({\n id: model,\n label: model === 'deepseek-reasoner' ? 'DeepSeek Reasoner' : 'DeepSeek Chat',\n })),\n });\n});\napp.get(\n '/api/ai/quota',\n asyncHandler(async (req, res) => {\n const user = await resolveUser(req, { username: req.query.username });\n const budget = enforceTokenBudget(user, 0);\n const usage = usageFor(user.userId);\n jsonResponse(res, 200, {\n success: true,\n isMember: user.isMember,\n monthlyLimit: budget.limit,\n usedTokens: usage.used,\n remainingTokens: Math.max(0, budget.limit - usage.used),\n month: usage.month,\n });\n }),\n);\n\napp.post(\n '/api/agent/chat',\n asyncHandler(async (req, res) => {\n const message = safeUserText(req.body?.message);\n if (!message) {\n return jsonResponse(res, 400, { success: false, message: 'message is required' });\n }\n\n const user = await resolveUser(req, req.body || {});\n const promptEstimate = estimateTokens(message);\n const estimated = promptEstimate + 600;\n const budget = enforceTokenBudget(user, estimated);\n const maxCompletionTokens = completionBudgetFor(budget, promptEstimate);\n const createdAt = nowIso();\n const conversationId = safeUserText(req.body?.conversationId) || crypto.randomUUID();\n const mode = normalizeAgentMode(req.body?.mode);\n const stream = req.body?.stream === true;\n let conversation = statements.getConversation.get(conversationId, user.userId);\n const isNewConversation = !conversation;\n\n if (isNewConversation) {\n conversation = { id: conversationId, user_id: user.userId, title: titleFromMessage(message) };\n statements.insertConversation.run({\n id: conversationId,\n userId: user.userId,\n title: conversation.title,\n provider: 'agent',\n model: openClawAgentId,\n createdAt,\n updatedAt: createdAt,\n });\n }\n\n const messageId = safeUserText(req.body?.messageId) || crypto.randomUUID();\n statements.insertMessage.run({\n id: messageId,\n conversationId,\n role: 'user',\n content: message,\n promptTokens: 0,\n completionTokens: 0,\n totalTokens: 0,\n createdAt,\n });\n\n const runId = `run_${crypto.randomUUID().replaceAll('-', '')}`;\n const sessionKey = openClawSessionKey(user, conversationId);\n const openClawRuntime = createOpenClawRuntime();\n statements.insertAgentRun.run({\n id: runId,\n userId: user.userId,\n conversationId,\n messageId,\n openClawRunId: null,\n openClawSessionKey: sessionKey,\n status: stream ? 'queued' : 'running',\n mode,\n provider: openClawRuntime.enabled ? openClawRuntime.provider : 'dacheng-ai-fallback',\n model: openClawRuntime.enabled ? openClawAgentId : deepseekModel,\n startedAt: createdAt,\n });\n\n if (stream) {\n return jsonResponse(res, 200, {\n success: true,\n runId,\n conversationId,\n streamUrl: `/api/agent/runs/${runId}/events`,\n });\n }\n\n const controller = new AbortController();\n activeAgentRuns.set(runId, controller);\n try {\n const skillResult = await runResourceFinderSkill({ message, user });\n const skillContext = resourceContextMessage(skillResult);\n const historyRows = statements.listMessages.all(conversationId).slice(-14);\n const modelMessages = [\n { role: 'system', content: systemPrompt },\n ...(skillContext ? [{ role: 'system', content: skillContext }] : []),\n ...normalizeMessages(historyRows),\n ];\n\n const aiResult = await runAgentModel({\n messages: modelMessages,\n user,\n conversationId,\n mode,\n signal: controller.signal,\n maxCompletionTokens,\n });\n const usage = {\n promptTokens: aiResult.usage?.promptTokens || estimateTokens(JSON.stringify(modelMessages)),\n completionTokens: aiResult.usage?.completionTokens || estimateTokens(aiResult.message),\n totalTokens:\n aiResult.usage?.totalTokens ||\n estimateTokens(JSON.stringify(modelMessages)) + estimateTokens(aiResult.message),\n };\n if (!aiResult.usageAlreadyRecorded) recordUsage(user.userId, usage.totalTokens);\n const answeredAt = nowIso();\n const assistantMessageId = crypto.randomUUID();\n statements.insertMessage.run({\n id: assistantMessageId,\n conversationId,\n role: 'assistant',\n content: aiResult.message,\n promptTokens: usage.promptTokens,\n completionTokens: usage.completionTokens,\n totalTokens: usage.totalTokens,\n createdAt: answeredAt,\n });\n statements.updateConversation.run({\n id: conversationId,\n userId: user.userId,\n title: isNewConversation ? titleFromMessage(message) : conversation.title,\n updatedAt: answeredAt,\n });\n updateAgentRunStatus({ id: runId, user_id: user.userId }, 'completed', {\n openClawRunId: aiResult.runId,\n inputTokens: usage.promptTokens,\n outputTokens: usage.completionTokens,\n });\n const action = determineClientAction(skillResult, message);\n return jsonResponse(res, 200, {\n success: true,\n runId,\n conversationId,\n messageId: assistantMessageId,\n provider: aiResult.provider,\n model: aiResult.model,\n message: aiResult.message,\n usage: agentUsagePayload(usage, user, budget),\n ...(action ? { clientAction: action } : {}),\n });\n } catch (error) {\n const status = error.name === 'AbortError' ? 'cancelled' : 'failed';\n updateAgentRunStatus({ id: runId, user_id: user.userId }, status, {\n errorCode: error.name || 'AGENT_RUN_FAILED',\n errorMessage: publicAiErrorMessage(error),\n });\n if (status === 'cancelled') {\n return jsonResponse(res, 499, { success: false, runId, status: 'cancelled' });\n }\n throw error;\n } finally {\n activeAgentRuns.delete(runId);\n }\n }),\n);\n\napp.get(\n '/api/agent/runs/:runId/events',\n asyncHandler(async (req, res) => {\n const user = await resolveUser(req, {});\n const run = statements.getAgentRun.get(req.params.runId, user.userId);\n if (!run) {\n return jsonResponse(res, 404, { success: false, message: 'run not found' });\n }\n if (run.status === 'completed' || run.status === 'failed' || run.status === 'cancelled') {\n return jsonResponse(res, 409, { success: false, message: `run is already ${run.status}` });\n }\n\n sseHeaders(res);\n const controller = new AbortController();\n activeAgentRuns.set(run.id, controller);\n updateAgentRunStatus(run, 'running');\n writeSse(res, 'run.started', { runId: run.id, conversationId: run.conversation_id });\n\n try {\n let streamedText = '';\n const modelMessages = buildAgentMessages(run.conversation_id);\n const lastUserMsg = [...modelMessages].reverse().find(m => m.role === 'user');\n const userMsgText = lastUserMsg ? lastUserMsg.content : '';\n const skillResult = userMsgText ? await runResourceFinderSkill({\n message: userMsgText,\n user,\n onStep: (step) => writeSse(res, 'tool.call.completed', {\n tool: 'agent.step',\n displayName: step.title || 'Agent step',\n summary: step.message || '',\n }),\n }) : null;\n const skillContext = resourceContextMessage(skillResult);\n const finalMessages = skillContext ? [\n modelMessages[0],\n { role: 'system', content: skillContext },\n ...modelMessages.slice(1)\n ] : modelMessages;\n\n const promptEstimate = estimateTokens(JSON.stringify(finalMessages));\n const estimated = promptEstimate + 600;\n const budget = enforceTokenBudget(user, estimated);\n const maxCompletionTokens = completionBudgetFor(budget, promptEstimate);\n const aiResult = await runAgentModel({\n messages: finalMessages,\n user,\n conversationId: run.conversation_id,\n mode: run.mode,\n signal: controller.signal,\n maxCompletionTokens,\n callbacks: {\n onStep: (step) => writeSse(res, 'tool.call.completed', {\n tool: 'agent.step',\n displayName: step.title || 'Agent step',\n summary: step.message || '',\n }),\n onToken: (delta) => {\n streamedText += delta;\n writeSse(res, 'assistant.delta', { text: delta });\n },\n },\n });\n const message = aiResult.message || streamedText.trim();\n const usage = {\n promptTokens: aiResult.usage?.promptTokens || estimateTokens(JSON.stringify(finalMessages)),\n completionTokens: aiResult.usage?.completionTokens || estimateTokens(message),\n totalTokens:\n aiResult.usage?.totalTokens || estimateTokens(JSON.stringify(finalMessages)) + estimateTokens(message),\n };\n if (!aiResult.usageAlreadyRecorded) recordUsage(user.userId, usage.totalTokens);\n const assistantMessageId = crypto.randomUUID();\n const answeredAt = nowIso();\n statements.insertMessage.run({\n id: assistantMessageId,\n conversationId: run.conversation_id,\n role: 'assistant',\n content: message,\n promptTokens: usage.promptTokens,\n completionTokens: usage.completionTokens,\n totalTokens: usage.totalTokens,\n createdAt: answeredAt,\n });\n statements.updateConversation.run({\n id: run.conversation_id,\n userId: user.userId,\n title: titleFromMessage(message),\n updatedAt: answeredAt,\n });\n updateAgentRunStatus(run, 'completed', {\n openClawRunId: aiResult.runId,\n inputTokens: usage.promptTokens,\n outputTokens: usage.completionTokens,\n });\n const action = determineClientAction(skillResult, userMsgText);\n writeSse(res, 'assistant.message', { messageId: assistantMessageId, content: message });\n writeSse(res, 'run.completed', {\n usage: agentUsagePayload(usage, user, budget),\n ...(action ? { clientAction: action } : {}),\n });\n res.end();\n } catch (error) {\n const status = error.name === 'AbortError' ? 'cancelled' : 'failed';\n updateAgentRunStatus(run, status, {\n errorCode: error.name || 'AGENT_RUN_FAILED',\n errorMessage: publicAiErrorMessage(error),\n });\n writeSse(res, status === 'cancelled' ? 'run.cancelled' : 'run.failed', {\n runId: run.id,\n message: publicAiErrorMessage(error),\n });\n res.end();\n } finally {\n activeAgentRuns.delete(run.id);\n }\n }),\n);\n\napp.post(\n '/api/agent/runs/:runId/cancel',\n asyncHandler(async (req, res) => {\n const user = await resolveUser(req, req.body || {});\n const run = statements.getAgentRun.get(req.params.runId, user.userId);\n if (!run) {\n return jsonResponse(res, 404, { success: false, message: 'run not found' });\n }\n const controller = activeAgentRuns.get(run.id);\n if (controller) controller.abort();\n updateAgentRunStatus(run, 'cancelled');\n return jsonResponse(res, 200, { success: true, runId: run.id, status: 'cancelled' });\n }),\n);\n\napp.post(\n '/api/agent/messages/:messageId/feedback',\n asyncHandler(async (req, res) => {\n const user = await resolveUser(req, req.body || {});\n const rating = safeUserText(req.body?.rating);\n if (!['up', 'down'].includes(rating)) {\n return jsonResponse(res, 400, { success: false, message: 'rating must be up or down' });\n }\n statements.insertAgentFeedback.run({\n id: crypto.randomUUID(),\n messageId: req.params.messageId,\n userId: user.userId,\n rating,\n reason: safeUserText(req.body?.reason),\n comment: safeUserText(req.body?.comment),\n createdAt: nowIso(),\n });\n return jsonResponse(res, 200, { success: true });\n }),\n);\n\napp.post(\n '/api/ai/chat',\n asyncHandler(async (req, res) => {\n const message = safeUserText(req.body?.message);\n if (!message) {\n return jsonResponse(res, 400, { success: false, message: 'message is required' });\n }\n\n const user = await resolveUser(req, req.body || {});\n const promptEstimate = estimateTokens(message);\n const estimated = promptEstimate + 600;\n const budget = enforceTokenBudget(user, estimated);\n const maxCompletionTokens = completionBudgetFor(budget, promptEstimate);\n const createdAt = nowIso();\n const conversationId = safeUserText(req.body?.conversationId) || crypto.randomUUID();\n const requestedModel = normalizeDeepSeekModelName(req.body?.model);\n const clientContext = clientContextMessage(req.body?.client);\n let conversation = statements.getConversation.get(conversationId, user.userId);\n const isNewConversation = !conversation;\n\n if (isNewConversation) {\n conversation = {\n id: conversationId,\n user_id: user.userId,\n title: titleFromMessage(message),\n };\n statements.insertConversation.run({\n id: conversationId,\n userId: user.userId,\n title: conversation.title,\n provider: 'deepseek',\n model: requestedModel,\n createdAt,\n updatedAt: createdAt,\n });\n }\n\n statements.insertMessage.run({\n id: crypto.randomUUID(),\n conversationId,\n role: 'user',\n content: message,\n promptTokens: 0,\n completionTokens: 0,\n totalTokens: 0,\n createdAt,\n });\n\n const libreChatRuntime = createLibreChatAgentRuntime();\n const skillResult = await runResourceFinderSkill({ message, user });\n const skillContext = resourceContextMessage(skillResult);\n const historyRows = statements.listMessages.all(conversationId).slice(-14);\n const modelMessages = [\n { role: 'system', content: systemPrompt },\n ...(clientContext ? [{ role: 'system', content: clientContext }] : []),\n ...(skillContext ? [{ role: 'system', content: skillContext }] : []),\n ...normalizeMessages(historyRows),\n ];\n\n const openClawRuntime = createOpenClawRuntime();\n const codexRuntime = createCodexDeepSeekRuntime(user);\n let provider = openClawRuntime.enabled\n ? openClawRuntime.provider\n : libreChatRuntime.enabled\n ? libreChatRuntime.provider\n : 'deepseek';\n let responseModel = openClawRuntime.enabled\n ? openClawRuntime.model\n : libreChatRuntime.enabled\n ? libreChatRuntime.model\n : requestedModel;\n let aiResult;\n try {\n if (openClawRuntime.enabled) {\n aiResult = await callOpenClawAgent({\n messages: modelMessages,\n user,\n conversationId,\n mode: normalizeAgentMode(req.body?.mode),\n });\n provider = aiResult.provider || provider;\n responseModel = aiResult.model || responseModel;\n } else if (libreChatRuntime.enabled) {\n aiResult = await callLibreChatAgent(modelMessages);\n } else if (codexRuntime.enabled) {\n provider = codexRuntime.provider;\n aiResult = await callCodexSdkDeepSeek(modelMessages, { user, maxCompletionTokens });\n } else {\n aiResult = await callDeepSeek(modelMessages, { model: requestedModel, maxCompletionTokens });\n }\n } catch (error) {\n if (libreChatRuntime.enabled) {\n logger.warn({ error: String(error) }, 'LibreChat Agent chat failed, falling back to Codex SDK or DeepSeek direct');\n provider = codexRuntime.enabled ? codexRuntime.provider : 'deepseek';\n responseModel = requestedModel;\n if (codexRuntime.enabled) {\n try {\n aiResult = await callCodexSdkDeepSeek(modelMessages, { user, maxCompletionTokens });\n } catch (codexError) {\n if (isDeepSeekQuotaError(codexError)) throw codexError;\n logger.warn({ error: String(codexError) }, 'Codex SDK chat failed, falling back to DeepSeek direct');\n provider = 'deepseek';\n aiResult = await callDeepSeek(modelMessages, { model: requestedModel, maxCompletionTokens });\n }\n } else {\n aiResult = await callDeepSeek(modelMessages, { model: requestedModel, maxCompletionTokens });\n }\n } else {\n if (!codexRuntime.enabled || isDeepSeekQuotaError(error)) throw error;\n logger.warn({ error: String(error) }, 'Codex SDK chat failed, falling back to DeepSeek direct');\n provider = 'deepseek';\n responseModel = requestedModel;\n aiResult = await callDeepSeek(modelMessages, { model: requestedModel, maxCompletionTokens });\n }\n }\n const usage = {\n promptTokens: aiResult.usage.promptTokens || estimateTokens(JSON.stringify(modelMessages)),\n completionTokens: aiResult.usage.completionTokens || estimateTokens(aiResult.message),\n totalTokens:\n aiResult.usage.totalTokens ||\n estimateTokens(JSON.stringify(modelMessages)) + estimateTokens(aiResult.message),\n };\n if (!aiResult.usageAlreadyRecorded) recordUsage(user.userId, usage.totalTokens);\n\n const answeredAt = nowIso();\n statements.insertMessage.run({\n id: crypto.randomUUID(),\n conversationId,\n role: 'assistant',\n content: aiResult.message,\n promptTokens: usage.promptTokens,\n completionTokens: usage.completionTokens,\n totalTokens: usage.totalTokens,\n createdAt: answeredAt,\n });\n statements.updateConversation.run({\n id: conversationId,\n userId: user.userId,\n title: isNewConversation ? titleFromMessage(message) : conversation.title,\n updatedAt: answeredAt,\n });\n\n const latestUsage = usageFor(user.userId);\n const monthlyLimit = budget.limit;\n const action = determineClientAction(skillResult, message);\n jsonResponse(res, 200, {\n success: true,\n conversationId,\n provider,\n model: responseModel,\n message: aiResult.message,\n usage: {\n ...usage,\n monthlyLimit,\n remainingTokens: Math.max(0, monthlyLimit - latestUsage.used),\n },\n ...(action ? { clientAction: action } : {}),\n });\n }),\n);\n\napp.post('/api/ai/chat/stream', async (req, res) => {\n sseHeaders(res);\n\n try {\n const message = safeUserText(req.body?.message);\n if (!message) {\n writeSse(res, 'error', { message: 'message is required' });\n res.end();\n return;\n }\n\n const user = await resolveUser(req, req.body || {});\n const promptEstimate = estimateTokens(message);\n const estimated = promptEstimate + 600;\n const budget = enforceTokenBudget(user, estimated);\n const maxCompletionTokens = completionBudgetFor(budget, promptEstimate);\n const createdAt = nowIso();\n const conversationId = safeUserText(req.body?.conversationId) || crypto.randomUUID();\n const requestedModel = normalizeDeepSeekModelName(req.body?.model);\n const clientContext = clientContextMessage(req.body?.client);\n let conversation = statements.getConversation.get(conversationId, user.userId);\n const isNewConversation = !conversation;\n\n if (isNewConversation) {\n conversation = {\n id: conversationId,\n user_id: user.userId,\n title: titleFromMessage(message),\n };\n statements.insertConversation.run({\n id: conversationId,\n userId: user.userId,\n title: conversation.title,\n provider: 'deepseek',\n model: requestedModel,\n createdAt,\n updatedAt: createdAt,\n });\n }\n\n statements.insertMessage.run({\n id: crypto.randomUUID(),\n conversationId,\n role: 'user',\n content: message,\n promptTokens: 0,\n completionTokens: 0,\n totalTokens: 0,\n createdAt,\n });\n\n const codexRuntime = createCodexDeepSeekRuntime(user);\n const libreChatRuntime = createLibreChatAgentRuntime();\n const initialOpenClawRuntime = createOpenClawRuntime();\n let provider = initialOpenClawRuntime.enabled\n ? initialOpenClawRuntime.provider\n : libreChatRuntime.enabled\n ? libreChatRuntime.provider\n : codexRuntime.enabled\n ? codexRuntime.provider\n : 'deepseek';\n let responseModel = initialOpenClawRuntime.enabled\n ? initialOpenClawRuntime.model\n : libreChatRuntime.enabled\n ? libreChatRuntime.model\n : requestedModel;\n writeSse(res, 'meta', {\n conversationId,\n provider,\n model: responseModel,\n });\n const skillResult = await runResourceFinderSkill({\n message,\n user,\n onStep: (step) => writeSse(res, 'step', step),\n });\n const skillContext = resourceContextMessage(skillResult);\n const historyRows = statements.listMessages.all(conversationId).slice(-14);\n const modelMessages = [\n { role: 'system', content: systemPrompt },\n ...(clientContext ? [{ role: 'system', content: clientContext }] : []),\n ...(skillContext ? [{ role: 'system', content: skillContext }] : []),\n ...normalizeMessages(historyRows),\n ];\n\n let aiResult;\n const openClawRuntime = initialOpenClawRuntime;\n try {\n if (openClawRuntime.enabled) {\n aiResult = await callOpenClawAgent({\n messages: modelMessages,\n user,\n conversationId,\n mode: normalizeAgentMode(req.body?.mode),\n });\n provider = aiResult.provider || provider;\n responseModel = aiResult.model || responseModel;\n writeSse(res, 'delta', { text: aiResult.message });\n } else if (libreChatRuntime.enabled) {\n aiResult = await callLibreChatAgentStream(modelMessages, {\n onToken: (token) => writeSse(res, 'delta', { text: token }),\n });\n } else if (codexRuntime.enabled) {\n aiResult = await callCodexSdkDeepSeek(modelMessages, {\n user,\n maxCompletionTokens,\n onToken: (token) => writeSse(res, 'delta', { text: token }),\n onStep: (step) => writeSse(res, 'step', step),\n });\n } else {\n aiResult = await callDeepSeekStream(modelMessages, {\n model: requestedModel,\n maxCompletionTokens,\n onToken: (token) => writeSse(res, 'delta', { text: token }),\n });\n }\n } catch (error) {\n if (libreChatRuntime.enabled) {\n logger.warn({ error: String(error) }, 'LibreChat Agent streaming chat failed, falling back to Codex SDK or DeepSeek direct');\n provider = codexRuntime.enabled ? codexRuntime.provider : 'deepseek';\n responseModel = requestedModel;\n writeSse(res, 'meta', {\n conversationId,\n provider,\n model: responseModel,\n });\n if (codexRuntime.enabled) {\n try {\n aiResult = await callCodexSdkDeepSeek(modelMessages, {\n user,\n maxCompletionTokens,\n onToken: (token) => writeSse(res, 'delta', { text: token }),\n onStep: (step) => writeSse(res, 'step', step),\n });\n } catch (codexError) {\n if (isDeepSeekQuotaError(codexError)) throw codexError;\n logger.warn({ error: String(codexError) }, 'Codex SDK streaming chat failed, falling back to DeepSeek direct');\n provider = 'deepseek';\n responseModel = requestedModel;\n writeSse(res, 'meta', {\n conversationId,\n provider,\n model: responseModel,\n });\n aiResult = await callDeepSeekStream(modelMessages, {\n model: requestedModel,\n maxCompletionTokens,\n onToken: (token) => writeSse(res, 'delta', { text: token }),\n });\n }\n } else {\n aiResult = await callDeepSeekStream(modelMessages, {\n model: requestedModel,\n maxCompletionTokens,\n onToken: (token) => writeSse(res, 'delta', { text: token }),\n });\n }\n } else {\n if (!codexRuntime.enabled || isDeepSeekQuotaError(error)) throw error;\n logger.warn({ error: String(error) }, 'Codex SDK streaming chat failed, falling back to DeepSeek direct');\n provider = 'deepseek';\n responseModel = requestedModel;\n writeSse(res, 'meta', {\n conversationId,\n provider,\n model: responseModel,\n });\n aiResult = await callDeepSeekStream(modelMessages, {\n model: requestedModel,\n maxCompletionTokens,\n onToken: (token) => writeSse(res, 'delta', { text: token }),\n });\n }\n }\n\n const usage = {\n promptTokens: aiResult.usage.promptTokens || estimateTokens(JSON.stringify(modelMessages)),\n completionTokens: aiResult.usage.completionTokens || estimateTokens(aiResult.message),\n totalTokens:\n aiResult.usage.totalTokens ||\n estimateTokens(JSON.stringify(modelMessages)) + estimateTokens(aiResult.message),\n };\n if (!aiResult.usageAlreadyRecorded) recordUsage(user.userId, usage.totalTokens);\n\n const answeredAt = nowIso();\n statements.insertMessage.run({\n id: crypto.randomUUID(),\n conversationId,\n role: 'assistant',\n content: aiResult.message,\n promptTokens: usage.promptTokens,\n completionTokens: usage.completionTokens,\n totalTokens: usage.totalTokens,\n createdAt: answeredAt,\n });\n statements.updateConversation.run({\n id: conversationId,\n userId: user.userId,\n title: isNewConversation ? titleFromMessage(message) : conversation.title,\n updatedAt: answeredAt,\n });\n\n const latestUsage = usageFor(user.userId);\n const monthlyLimit = budget.limit;\n const action = determineClientAction(skillResult, message);\n writeSse(res, 'done', {\n conversationId,\n provider,\n model: responseModel,\n message: aiResult.message,\n usage: {\n ...usage,\n monthlyLimit,\n remainingTokens: Math.max(0, monthlyLimit - latestUsage.used),\n },\n ...(action ? { clientAction: action } : {}),\n });\n res.end();\n } catch (error) {\n logger.error({ error: error.stack || String(error) }, 'streaming chat failed');\n writeSse(res, 'error', {\n message: publicAiErrorMessage(error),\n details: error.details,\n });\n res.end();\n }\n});\n\napp.get(\n '/api/ai/conversations',\n asyncHandler(async (req, res) => {\n const user = await resolveUser(req, { username: req.query.username });\n const items = statements.listConversations.all(user.userId).map((item) => ({\n id: item.id,\n title: item.title,\n updatedAt: item.updatedAt,\n }));\n jsonResponse(res, 200, { success: true, items });\n }),\n);\n\napp.get(\n '/api/ai/conversations/:id',\n asyncHandler(async (req, res) => {\n const user = await resolveUser(req);\n const conversation = statements.getConversation.get(req.params.id, user.userId);\n if (!conversation) {\n return jsonResponse(res, 404, { success: false, message: 'conversation not found' });\n }\n const messages = statements.listMessages.all(req.params.id).map((item) => ({\n role: item.role,\n content: item.content,\n createdAt: item.createdAt,\n }));\n jsonResponse(res, 200, {\n success: true,\n conversation: {\n id: conversation.id,\n title: conversation.title,\n updatedAt: conversation.updated_at,\n },\n messages,\n });\n }),\n);\n\nfunction buildUrl(base, endpoint, params = {}) {\n const url = new URL(endpoint, `${base}/`);\n for (const [key, value] of Object.entries(params)) {\n if (Array.isArray(value)) {\n for (const item of value) {\n if (item !== undefined && item !== null && item !== '') {\n url.searchParams.append(key, String(item));\n }\n }\n } else if (value !== undefined && value !== null && value !== '') {\n url.searchParams.set(key, String(value));\n }\n }\n return url;\n}\n\nasync function fetchJson(url) {\n const response = await fetch(url, {\n headers: { Accept: 'application/json' },\n signal: AbortSignal.timeout(15_000),\n });\n if (!response.ok) {\n throw new Error(`Request failed: ${response.status}`);\n }\n return response.json();\n}\n\nfunction stripHtml(value) {\n return String(value || '')\n .replace(/<script[\\s\\S]*?<\\/script>/gi, ' ')\n .replace(/<style[\\s\\S]*?<\\/style>/gi, ' ')\n .replace(/<[^>]+>/g, ' ')\n .replace(/ /g, ' ')\n .replace(/</g, '<')\n .replace(/>/g, '>')\n .replace(/&/g, '&')\n .replace(/&#(\\d+);/g, (_match, code) => String.fromCharCode(Number(code)))\n .replace(/\\s+/g, ' ')\n .trim();\n}\n\nfunction cleanExtractedText(value) {\n let text = String(value || '')\n .replace(/\\b[A-Z]{1,3}\\d{2}n\\d{4,5}[a-z]?_p?\\d{4}[a-z]\\d{2}\\b/g, ' ')\n .replace(/\\uFFFD/g, '')\n .replace(/\\s+([,。;:!?、」』)》】])/g, '$1')\n .replace(/([「『《(【])\\s+/g, '$1');\n for (let i = 0; i < 3; i += 1) {\n text = text.replace(/([\\u3400-\\u9fff])\\s+([\\u3400-\\u9fff])/g, '$1$2');\n }\n return text.replace(/[ \\t]{2,}/g, ' ').trim();\n}\n\nfunction compactText(value, limit = 140) {\n const text = stripHtml(value);\n return text.length > limit ? `${text.slice(0, limit)}...` : text;\n}\n\nfunction collectSearchResults(data) {\n if (!data || typeof data !== 'object') return [];\n if (Array.isArray(data.results)) return data.results;\n if (Array.isArray(data?.results?.docs)) return data.results.docs;\n if (Array.isArray(data?.response?.docs)) return data.response.docs;\n if (Array.isArray(data.docs)) return data.docs;\n return [];\n}\n\nfunction firstText(item, keys) {\n for (const key of keys) {\n const value = item?.[key];\n if (Array.isArray(value)) {\n const first = value.map((entry) => safeUserText(String(entry))).find(Boolean);\n if (first) return first;\n }\n if (typeof value === 'string' && value.trim()) return value.trim();\n if (typeof value === 'number') return String(value);\n }\n return '';\n}\n\nfunction firstNumber(item, keys, fallback = undefined) {\n for (const key of keys) {\n const value = item?.[key];\n if (typeof value === 'number') return value;\n if (typeof value === 'string' && value.trim() && Number.isFinite(Number(value))) {\n return Number(value);\n }\n }\n return fallback;\n}\n\nasync function searchCbetaTitle(query, limit) {\n const url = buildUrl(cbetaApiRoot, 'search/title', { q: query, start: 0, rows: limit });\n const data = await fetchJson(url);\n return collectSearchResults(data).flatMap((item) => {\n const work = firstText(item, ['work', 'work_id', 'workId']);\n const title = firstText(item, ['content', 'title', 'work_title', 'workTitle']);\n if (!work || !title) return [];\n const juan = firstNumber(item, ['juan', 'juan_num', 'juanNum'], 1);\n return [\n {\n id: `cbeta-title-${work}-${juan}`,\n title,\n sourceName: 'CBETA 佛典',\n url: `cbeta:${work}:${juan}`,\n snippet: firstText(item, ['byline', 'creators', 'time_dynasty']) || '可下载经文正文并用于全球法布施。',\n resourceType: 'scripture',\n work,\n juan,\n },\n ];\n });\n}\n\nasync function searchCbetaContent(query, limit) {\n const endpoints = ['search/fulltext', 'search/content', 'search/all_in_one'];\n const settled = await Promise.allSettled(\n endpoints.map((endpoint) =>\n fetchJson(buildUrl(cbetaApiRoot, endpoint, { q: query, start: 0, rows: limit, field: 'content' })),\n ),\n );\n const results = [];\n for (const response of settled) {\n if (response.status !== 'fulfilled') continue;\n for (const item of collectSearchResults(response.value)) {\n const work = firstText(item, ['work', 'work_id', 'workId', 'sutra', 'id']);\n const snippet = compactText(firstText(item, ['kwic', 'highlight', 'snippet', 'content', 'text', 'body', 'p']));\n if (!work || !snippet) continue;\n const juan = firstNumber(item, ['juan', 'juan_num', 'juanNum', '卷'], 1);\n const title = firstText(item, ['title', 'work_title', 'workTitle', 'sutra_name', 'book', 'name']) || work;\n results.push({\n id: `cbeta-content-${work}-${juan}-${sha256(snippet).slice(0, 8)}`,\n title,\n sourceName: 'CBETA 佛典',\n url: `cbeta:${work}:${juan}`,\n snippet,\n resourceType: 'scripture',\n work,\n juan,\n });\n if (results.length >= limit) return results;\n }\n }\n return results;\n}\n\nfunction directUrlResource(query) {\n const url = safeUserText(query);\n let parsed;\n try {\n parsed = new URL(url);\n } catch {\n return null;\n }\n if (!['http:', 'https:'].includes(parsed.protocol)) return null;\n return {\n id: `url-${sha256(url).slice(0, 12)}`,\n title: parsed.hostname,\n sourceName: '网页资源',\n url,\n snippet: '直接下载并提取这个链接的可读正文。',\n resourceType: 'web',\n };\n}\n\nfunction looksLikeAudioQuery(query) {\n return /音乐|音樂|音频|音訊|mp3|audio|念诵|念誦|诵经|誦經|唱诵|唱誦|读诵|讀誦/.test(\n safeUserText(query).toLowerCase(),\n );\n}\n\nfunction archiveSearchTerm(query) {\n return (\n safeUserText(query)\n .replace(/下载|查找|寻找|找一?个|资源|音频|音訊|音乐|音樂|mp3|audio|念诵|念誦|诵经|誦經|唱诵|唱誦|读诵|讀誦/gi, ' ')\n .replace(/\\s+/g, ' ')\n .trim() || safeUserText(query)\n );\n}\n\nfunction solrPhrase(value) {\n return String(value).replace(/\\\\/g, '\\\\\\\\').replace(/\"/g, '\\\\\"');\n}\n\nasync function searchInternetArchiveAudio(query, limit) {\n if (!looksLikeAudioQuery(query)) return [];\n const term = archiveSearchTerm(query);\n if (!term) return [];\n const variants = Array.from(\n new Set([\n term,\n term.replaceAll('金刚经', '金剛經'),\n term.replaceAll('金剛經', '金刚经'),\n ].filter(Boolean)),\n );\n const phraseQuery = variants\n .flatMap((item) => [`title:\"${solrPhrase(item)}\"`, `description:\"${solrPhrase(item)}\"`])\n .join(' OR ');\n const url = buildUrl('https://archive.org', 'advancedsearch.php', {\n q: `(${phraseQuery}) AND mediatype:audio`,\n rows: Math.min(Math.max(limit, 1), 10),\n output: 'json',\n 'fl[]': ['identifier', 'title', 'description'],\n });\n const data = await fetchJson(url);\n const docs = Array.isArray(data?.response?.docs) ? data.response.docs : [];\n return docs\n .map((item) => {\n const identifier = firstText(item, ['identifier']);\n const title = firstText(item, ['title']) || identifier;\n if (!identifier || !title) return null;\n return {\n id: `ia-audio-${identifier}`,\n title,\n sourceName: 'Internet Archive 音频',\n url: `ia:${identifier}`,\n snippet: compactText(firstText(item, ['description'])) || '公开可访问的音频资源,将下载第一个可用 MP3 文件。',\n resourceType: 'audio',\n identifier,\n };\n })\n .filter(Boolean);\n}\n\nconst curatedCbetaWorks = [\n {\n work: 'T0235',\n juan: 1,\n title: '金剛般若波羅蜜經',\n keywords: ['金剛般若波羅蜜經', '金刚般若波罗蜜经', '金剛經', '金刚经'],\n snippet: '姚秦 鳩摩羅什譯。常用于读诵、共修与法布施的般若经典。',\n },\n {\n work: 'T0251',\n juan: 1,\n title: '般若波羅蜜多心經',\n keywords: ['般若波羅蜜多心經', '般若波罗蜜多心经', '心經', '心经'],\n snippet: '唐 玄奘譯。篇幅短,适合加入禅室功课本与日常持诵。',\n },\n {\n work: 'T0262',\n juan: 1,\n title: '妙法蓮華經',\n keywords: ['妙法蓮華經', '妙法莲华经', '法華經', '法华经'],\n snippet: '姚秦 鳩摩羅什譯。可按卷下载正文,用于长期功课与分享。',\n },\n {\n work: 'T0279',\n juan: 1,\n title: '大方廣佛華嚴經',\n keywords: ['大方廣佛華嚴經', '大方广佛华严经', '華嚴經', '华严经'],\n snippet: '唐 實叉難陀譯。可按卷下载正文,用于系统研读与法布施。',\n },\n {\n work: 'T0360',\n juan: 1,\n title: '佛說無量壽經',\n keywords: ['佛說無量壽經', '佛说无量寿经', '無量壽經', '无量寿经'],\n snippet: '净土经典。可下载正文,用于净土功课本与全球法布施。',\n },\n {\n work: 'T0365',\n juan: 1,\n title: '佛說觀無量壽佛經',\n keywords: ['佛說觀無量壽佛經', '佛说观无量寿佛经', '觀無量壽經', '观无量寿经'],\n snippet: '净土三经之一。适合查找下载后加入净土功课。',\n },\n {\n work: 'T0366',\n juan: 1,\n title: '佛說阿彌陀經',\n keywords: ['佛說阿彌陀經', '佛说阿弥陀经', '阿彌陀經', '阿弥陀经'],\n snippet: '姚秦 鳩摩羅什譯。篇幅适中,适合共修、持诵与分享。',\n },\n {\n work: 'T0475',\n juan: 1,\n title: '維摩詰所說經',\n keywords: ['維摩詰所說經', '维摩诘所说经', '維摩詰經', '维摩诘经'],\n snippet: '姚秦 鳩摩羅什譯。大乘经典,可按卷下载用于研读。',\n },\n {\n work: 'T0676',\n juan: 1,\n title: '解深密經',\n keywords: ['解深密經', '解深密经'],\n snippet: '唯识相关大乘经典。适合研读、整理笔记与法布施。',\n },\n {\n work: 'T1666',\n juan: 1,\n title: '大乘起信論',\n keywords: ['大乘起信論', '大乘起信论', '起信論', '起信论'],\n snippet: '大乘论典。适合下载后作为研习功课资料。',\n },\n];\n\nfunction localCbetaResults(query, limit) {\n const normalizedQuery = safeUserText(query).toLowerCase();\n if (!normalizedQuery) return [];\n return curatedCbetaWorks\n .filter((item) =>\n item.keywords.some((keyword) => {\n const normalizedKeyword = keyword.toLowerCase();\n return normalizedKeyword.includes(normalizedQuery) || normalizedQuery.includes(normalizedKeyword);\n }),\n )\n .slice(0, limit)\n .map((item) => ({\n id: `cbeta-curated-${item.work}-${item.juan}`,\n title: item.title,\n sourceName: 'CBETA 佛典',\n url: `cbeta:${item.work}:${item.juan}`,\n snippet: item.snippet,\n resourceType: 'scripture',\n work: item.work,\n juan: item.juan,\n }));\n}\n\nfunction uniqueResources(items, limit) {\n const seen = new Set();\n const results = [];\n for (const item of items) {\n const key = `${item.url}:${item.title}`;\n if (seen.has(key)) continue;\n seen.add(key);\n results.push(item);\n if (results.length >= limit) break;\n }\n return results;\n}\n\nasync function findResourceCandidates(query, limit) {\n const direct = directUrlResource(query);\n const results = [];\n if (direct) results.push(direct);\n try {\n results.push(...(await searchInternetArchiveAudio(query, limit)));\n } catch (error) {\n logger.warn({ error: String(error), query }, 'Internet Archive audio search skipped');\n }\n results.push(...localCbetaResults(query, limit));\n\n const [titleResults, contentResults] = await Promise.allSettled([\n searchCbetaTitle(query, limit),\n searchCbetaContent(query, limit),\n ]);\n if (titleResults.status === 'fulfilled') results.push(...titleResults.value);\n if (contentResults.status === 'fulfilled') results.push(...contentResults.value);\n\n return uniqueResources(results, limit);\n}\n\nfunction looksLikeResourceTask(message) {\n const text = safeUserText(message).toLowerCase();\n if (!text) return false;\n return /下载|查找|寻找|找一?个|资源|經文|经文|佛经|佛經|仪轨|儀軌|音乐|音樂|mp3|audio|念诵|念誦|诵经|誦經/.test(text);\n}\n\nfunction extractResourceQuery(message) {\n const text = safeUserText(message)\n .replace(/请|請|帮我|幫我|麻烦|麻煩|可以|能不能|需要|我要|想要/g, ' ')\n .replace(/下载|下載|查找|寻找|尋找|找一个|找一個|找|资源|資源|并|並|然后|然後|用于|用於|全球法布施|加入功课本|加入功課本/g, ' ')\n .replace(/\\s+/g, ' ')\n .trim();\n return text || safeUserText(message);\n}\n\nfunction resourceContextMessage(skillResult) {\n if (!skillResult) return '';\n const lines = [\n '本轮资源检索/下载已经由大乘 App 后端完成。',\n '不要再次调用 search_dharma_resources、download_dharma_resource 或其他资源 MCP 工具;请直接基于以下结果回答用户。',\n `后端已调用 skill: ${skillResult.skillName}`,\n `执行目标: ${skillResult.query}`,\n ];\n if (skillResult.selected) {\n lines.push(`候选资源: ${skillResult.selected.title}`);\n lines.push(`来源: ${skillResult.selected.sourceName}`);\n lines.push(`资源地址: ${skillResult.selected.url}`);\n }\n if (skillResult.downloaded) {\n lines.push(`下载标题: ${skillResult.downloaded.title}`);\n lines.push(`下载文件: ${skillResult.downloaded.fileName}`);\n const text = safeUserText(skillResult.downloaded.contentText);\n if (text) {\n lines.push('下载正文摘录:');\n lines.push(text.slice(0, 8000));\n }\n }\n return lines.join('\\n');\n}\n\nfunction determineClientAction(skillResult, message) {\n if (skillResult && skillResult.downloaded) {\n const text = safeUserText(message);\n if (/法布施|全球法布施|分享/.test(text)) {\n return {\n type: 'prepare_dharma_share_text',\n title: skillResult.downloaded.title,\n text: skillResult.downloaded.contentText || '',\n };\n }\n }\n return null;\n}\n\nasync function runResourceFinderSkill({ message, user, onStep }) {\n if (!looksLikeResourceTask(message)) return null;\n\n const skillName = 'resource-finder-downloader';\n const query = extractResourceQuery(message);\n const audioTask = looksLikeAudioQuery(message) || looksLikeAudioQuery(query);\n onStep?.({\n type: 'skill.step',\n skillName,\n stage: 'start',\n title: '调用资源查找下载 skill',\n message: `识别到资源任务: ${query}`,\n });\n\n onStep?.({\n type: 'skill.step',\n skillName,\n stage: 'search',\n title: '搜索可分享资源',\n message: audioTask\n ? '优先检索公开可访问音频资源,失败时再回落到佛典正文资源。'\n : '优先检索 CBETA 佛典正文接口,搜索索引不可用时使用内置经典映射。',\n });\n const candidates = await findResourceCandidates(query, 8);\n if (candidates.length === 0) {\n onStep?.({\n type: 'skill.step',\n skillName,\n stage: 'empty',\n title: '没有找到可下载资源',\n message: '后端没有找到明确可下载且可分享的资源。',\n });\n return {\n skillName,\n query,\n candidates: [],\n selected: null,\n downloaded: null,\n };\n }\n\n const selected = candidates[0];\n onStep?.({\n type: 'skill.step',\n skillName,\n stage: 'select',\n title: '选择可信候选资源',\n message: `${selected.title} / ${selected.sourceName}`,\n });\n\n onStep?.({\n type: 'skill.step',\n skillName,\n stage: 'download',\n title: '下载并提取资源',\n message: selected.url,\n });\n const cbeta = parseCbetaResource(selected);\n const internetArchive = parseInternetArchiveResource(selected);\n const downloaded = cbeta\n ? await downloadCbetaResource({ ...selected, ...cbeta })\n : internetArchive\n ? await downloadInternetArchiveAudio({ ...selected, ...internetArchive })\n : await downloadWebResource(selected);\n const persisted = persistDownloadedResource(user, downloaded);\n\n onStep?.({\n type: 'skill.step',\n skillName,\n stage: 'done',\n title: '资源已准备完成',\n message: downloaded.fileName,\n });\n\n return {\n skillName,\n query,\n candidates,\n selected,\n downloaded: {\n ...downloaded,\n downloadId: persisted.id,\n },\n };\n}\n\napp.post(\n '/api/resources/search',\n asyncHandler(async (req, res) => {\n const query = safeUserText(req.body?.query);\n const limit = Math.min(Math.max(Number(req.body?.limit || 12), 1), 20);\n if (query.length < 2) {\n return jsonResponse(res, 400, { success: false, message: 'query is required' });\n }\n\n const items = await findResourceCandidates(query, limit);\n\n jsonResponse(res, 200, {\n success: true,\n source: 'resource-index',\n items,\n });\n }),\n);\n\nfunction parseCbetaResource(input) {\n const work = safeUserText(input.work);\n const juan = Number(input.juan || 1);\n if (work) return { work, juan: Number.isFinite(juan) ? juan : 1 };\n\n const url = safeUserText(input.url);\n const match = /^cbeta:([^:]+):(\\d+)$/i.exec(url);\n if (!match) return null;\n return {\n work: match[1].toUpperCase(),\n juan: Number(match[2] || 1),\n };\n}\n\nfunction parseInternetArchiveResource(input) {\n const url = safeUserText(input.url);\n const match = /^ia:([^:]+)$/i.exec(url);\n const identifier = match?.[1] || safeUserText(input.identifier);\n if (!identifier) return null;\n return { identifier };\n}\n\nfunction safeFileName(value) {\n const normalized = String(value || 'dharma-resource')\n .replace(/[\\\\/:*?\"<>|]+/g, '-')\n .replace(/\\s+/g, '-')\n .replace(/-+/g, '-')\n .replace(/^-|-$/g, '')\n .slice(0, 80);\n return normalized || 'dharma-resource';\n}\n\nfunction extractCbetaHtml(data) {\n const first = Array.isArray(data?.results) ? data.results[0] : null;\n if (typeof first === 'string') return first;\n if (first && typeof first === 'object' && typeof first.html === 'string') return first.html;\n if (typeof data?.html === 'string') return data.html;\n return '';\n}\n\nasync function downloadCbetaResource(resource) {\n const url = buildUrl(cbetaApiRoot, 'juans', {\n work: resource.work,\n juan: resource.juan,\n work_info: 1,\n toc: 1,\n });\n const data = await fetchJson(url);\n const workInfo = data.work_info || {};\n const title = safeUserText(workInfo.title) || safeUserText(resource.title) || resource.work;\n const html = extractCbetaHtml(data);\n const text = cleanExtractedText(stripHtml(html));\n if (!text) throw new Error('CBETA resource returned empty content');\n return {\n title,\n sourceName: 'CBETA 佛典',\n url: `cbeta:${resource.work}:${resource.juan}`,\n contentText: text.slice(0, maxResourceTextChars),\n fileName: `${resource.work}_${resource.juan}_${safeFileName(title)}.txt`,\n };\n}\n\nasync function downloadInternetArchiveAudio(resource) {\n const identifier = safeUserText(resource.identifier);\n if (!identifier) throw new Error('Internet Archive identifier is required');\n const metadata = await fetchJson(`https://archive.org/metadata/${encodeURIComponent(identifier)}`);\n const title = safeUserText(metadata?.metadata?.title) || safeUserText(resource.title) || identifier;\n const files = Array.isArray(metadata?.files) ? metadata.files : [];\n const audioFile = files.find((file) => {\n const name = safeUserText(file?.name);\n const format = safeUserText(file?.format).toLowerCase();\n return /\\.(mp3|m4a|ogg)$/i.test(name) || format.includes('mp3');\n });\n if (!audioFile?.name) {\n throw new Error('Internet Archive audio item has no downloadable audio file');\n }\n const sourceFileName = audioFile.name;\n const fileUrl = `https://archive.org/download/${encodeURIComponent(identifier)}/${sourceFileName\n .split('/')\n .map((part) => encodeURIComponent(part))\n .join('/')}`;\n const response = await fetch(fileUrl, {\n headers: {\n Accept: 'audio/mpeg, audio/*;q=0.9, */*;q=0.5',\n 'User-Agent': 'DachengResourceDownloader/1.0 (+https://ombhrum.com)',\n },\n signal: AbortSignal.timeout(60_000),\n });\n if (!response.ok) {\n throw new Error(`Internet Archive audio download failed: ${response.status}`);\n }\n const binaryContent = Buffer.from(await response.arrayBuffer());\n if (!binaryContent.length) throw new Error('Internet Archive audio returned empty content');\n const sizeMb = (binaryContent.length / 1024 / 1024).toFixed(1);\n return {\n title,\n sourceName: 'Internet Archive 音频',\n url: fileUrl,\n contentText: `已下载音频资源: ${title}\\n文件名: ${sourceFileName}\\n大小: ${sizeMb} MB\\n来源: ${fileUrl}`,\n binaryContent,\n fileName: safeFileName(sourceFileName),\n };\n}\n\nasync function downloadWebResource(input) {\n const url = safeUserText(input.url);\n const parsed = new URL(url);\n if (!['http:', 'https:'].includes(parsed.protocol)) {\n throw new Error('Only http/https resources can be downloaded');\n }\n\n const response = await fetch(parsed, {\n headers: {\n Accept: 'text/html, text/plain, application/xhtml+xml, application/xml;q=0.8, */*;q=0.5',\n 'User-Agent': 'DachengResourceDownloader/1.0 (+https://ombhrum.com)',\n },\n signal: AbortSignal.timeout(25_000),\n });\n if (!response.ok) throw new Error(`Resource download failed: ${response.status}`);\n const contentType = response.headers.get('content-type') || '';\n const body = await response.text();\n const title = safeUserText(input.title) || parsed.hostname;\n const contentText = contentType.includes('html') ? cleanExtractedText(stripHtml(body)) : body.trim();\n if (!contentText) throw new Error('Resource returned empty content');\n return {\n title,\n sourceName: safeUserText(input.sourceName) || parsed.hostname,\n url,\n contentText: contentText.slice(0, maxResourceTextChars),\n fileName: `${safeFileName(title)}.txt`,\n };\n}\n\nfunction persistDownloadedResource(user, content) {\n const id = crypto.randomUUID();\n const fileName = `${id}-${safeFileName(content.fileName)}`;\n const filePath = path.join(resourcesDir, fileName);\n if (content.binaryContent) {\n fs.writeFileSync(filePath, content.binaryContent);\n } else {\n fs.writeFileSync(filePath, content.contentText, 'utf8');\n }\n statements.insertResourceDownload.run({\n id,\n userId: user.userId,\n title: content.title,\n sourceName: content.sourceName,\n sourceUrl: content.url,\n fileName: content.fileName,\n filePath,\n createdAt: nowIso(),\n });\n return { id, fileName, filePath };\n}\n\nfunction createLibreChatMcpServer() {\n const server = new McpServer({\n name: 'dacheng-ai-tools',\n version: '0.1.0',\n });\n\n server.registerTool(\n 'search_dharma_resources',\n {\n title: 'Search dharma resources',\n description:\n 'Search for legally shareable Buddhist scripture, text, web, or audio resources that can be prepared for Fabushi sharing.',\n inputSchema: {\n query: z.string().min(2).max(200),\n limit: z.number().int().min(1).max(12).optional(),\n },\n outputSchema: {\n query: z.string(),\n items: z.array(\n z.object({\n id: z.string(),\n title: z.string(),\n sourceName: z.string(),\n url: z.string(),\n snippet: z.string(),\n resourceType: z.string(),\n work: z.string().optional(),\n juan: z.number().optional(),\n }),\n ),\n },\n annotations: {\n readOnlyHint: true,\n destructiveHint: false,\n openWorldHint: true,\n },\n },\n async ({ query, limit }) => {\n const items = await findResourceCandidates(query, limit ?? 8);\n return textToolResult(`Found ${items.length} candidate resources for \"${query}\".`, {\n query,\n items,\n });\n },\n );\n\n server.registerTool(\n 'download_dharma_resource',\n {\n title: 'Download dharma resource',\n description:\n 'Download and extract a selected resource. Use the url/work/juan returned by search_dharma_resources whenever possible.',\n inputSchema: {\n url: z.string().min(1).max(1000),\n title: z.string().max(300).optional(),\n sourceName: z.string().max(120).optional(),\n work: z.string().max(40).optional(),\n juan: z.number().int().min(1).optional(),\n identifier: z.string().max(300).optional(),\n },\n outputSchema: {\n title: z.string(),\n sourceName: z.string(),\n url: z.string(),\n fileName: z.string(),\n downloadId: z.string(),\n contentText: z.string(),\n },\n annotations: {\n readOnlyHint: false,\n destructiveHint: false,\n openWorldHint: true,\n },\n },\n async (input) => {\n const user = { userId: 'librechat:mcp' };\n const cbeta = parseCbetaResource(input);\n const internetArchive = parseInternetArchiveResource(input);\n const content = cbeta\n ? await downloadCbetaResource({ ...input, ...cbeta })\n : internetArchive\n ? await downloadInternetArchiveAudio({ ...input, ...internetArchive })\n : await downloadWebResource(input);\n const persisted = persistDownloadedResource(user, content);\n const { binaryContent: _binaryContent, ...publicContent } = content;\n return textToolResult(`Downloaded ${publicContent.title} as ${publicContent.fileName}.`, {\n ...publicContent,\n downloadId: persisted.id,\n });\n },\n );\n\n server.registerTool(\n 'prepare_dharma_share_text',\n {\n title: 'Prepare dharma sharing text',\n description:\n 'Prepare a text payload for the Fabushi app to confirm and add to global dharma sharing. This returns a clientAction; it does not start sending.',\n inputSchema: {\n title: z.string().min(1).max(120),\n text: z.string().min(1).max(20000),\n },\n outputSchema: {\n clientAction: z.object({\n type: z.literal('prepare_dharma_share_text'),\n title: z.string(),\n text: z.string(),\n }),\n },\n annotations: {\n readOnlyHint: false,\n destructiveHint: false,\n openWorldHint: false,\n },\n },\n async ({ title, text }) =>\n textToolResult(`Prepared \"${title}\" for user confirmation in the Fabushi app.`, {\n clientAction: {\n type: 'prepare_dharma_share_text',\n title: safeUserText(title).slice(0, 120),\n text: String(text || '').trim().slice(0, 20000),\n },\n }),\n );\n\n server.registerTool(\n 'prepare_practice_book_item',\n {\n title: 'Prepare practice book item',\n description:\n 'Prepare a scripture, chant, or practice item for the Fabushi app practice book. This returns a clientAction; it does not mutate the app by itself.',\n inputSchema: {\n title: z.string().min(1).max(120),\n sourceName: z.string().max(120).optional(),\n text: z.string().min(1).max(20000),\n },\n outputSchema: {\n clientAction: z.object({\n type: z.literal('prepare_practice_book_item'),\n title: z.string(),\n sourceName: z.string(),\n text: z.string(),\n }),\n },\n annotations: {\n readOnlyHint: false,\n destructiveHint: false,\n openWorldHint: false,\n },\n },\n async ({ title, sourceName, text }) =>\n textToolResult(`Prepared \"${title}\" for the Fabushi practice book confirmation flow.`, {\n clientAction: {\n type: 'prepare_practice_book_item',\n title: safeUserText(title).slice(0, 120),\n sourceName: safeUserText(sourceName || '大乘 AI').slice(0, 120),\n text: String(text || '').trim().slice(0, 20000),\n },\n }),\n );\n\n return server;\n}\n\nasync function handleMcpRequest(req, res) {\n const server = createLibreChatMcpServer();\n const transport = new StreamableHTTPServerTransport({\n sessionIdGenerator: undefined,\n enableJsonResponse: true,\n });\n\n res.on('close', () => {\n transport.close();\n server.close();\n });\n\n await server.connect(transport);\n await transport.handleRequest(req, res, req.body);\n}\n\napp.all('/mcp', async (req, res) => {\n try {\n await handleMcpRequest(req, res);\n } catch (error) {\n logger.error({ error: error.stack || String(error) }, 'MCP request failed');\n if (!res.headersSent) {\n res.status(500).json({\n error: 'MCP request failed',\n message: publicAiErrorMessage(error),\n });\n }\n }\n});\n\napp.post(\n '/api/resources/download',\n asyncHandler(async (req, res) => {\n const user = await resolveUser(req, req.body || {});\n const cbeta = parseCbetaResource(req.body || {});\n const internetArchive = parseInternetArchiveResource(req.body || {});\n const content = cbeta\n ? await downloadCbetaResource({ ...req.body, ...cbeta })\n : internetArchive\n ? await downloadInternetArchiveAudio({ ...req.body, ...internetArchive })\n : await downloadWebResource(req.body || {});\n const persisted = persistDownloadedResource(user, content);\n const { binaryContent: _binaryContent, ...publicContent } = content;\n jsonResponse(res, 200, {\n success: true,\n downloadId: persisted.id,\n ...publicContent,\n });\n }),\n);\n\napp.post(\n '/api/codex/resource-task',\n asyncHandler(async (req, res) => {\n if (process.env.ENABLE_CODEX_SDK !== 'true') {\n return jsonResponse(res, 403, {\n success: false,\n message: 'Codex SDK task runner is disabled. Set ENABLE_CODEX_SDK=true on the server to enable it.',\n });\n }\n const prompt = safeUserText(req.body?.prompt);\n if (!prompt) {\n return jsonResponse(res, 400, { success: false, message: 'prompt is required' });\n }\n const user = await resolveUser(req, req.body || {});\n const { Codex } = await import('@openai/codex-sdk');\n const codexRuntime = createCodexDeepSeekRuntime(user);\n const codex = new Codex(codexRuntime.enabled ? codexRuntime.options : {});\n const thread = codex.startThread(\n codexRuntime.enabled\n ? codexRuntime.threadOptions\n : { skipGitRepoCheck: true, networkAccessEnabled: true },\n );\n const result = await thread.run(prompt);\n const usage = {\n promptTokens: Number(result.usage?.input_tokens || 0),\n completionTokens: Number(result.usage?.output_tokens || 0),\n totalTokens: Number((result.usage?.input_tokens || 0) + (result.usage?.output_tokens || 0)),\n };\n if (!codexRuntime.enabled) {\n recordUsage(user.userId, usage.totalTokens || estimateTokens(JSON.stringify(result)));\n }\n jsonResponse(res, 200, {\n success: true,\n threadId: thread.id,\n provider: codexRuntime.provider,\n model: deepseekModel,\n usage,\n result,\n });\n }),\n);\n\nregisterPlatformApi({ app, db, resolveUser, asyncHandler });\n\napp.use((error, _req, res, _next) => {\n const status = Number(error.statusCode || error.status || 500);\n const safeStatus = status >= 400 && status < 600 ? status : 500;\n logger.error({ error: error.stack || String(error) }, 'request failed');\n jsonResponse(res, safeStatus, {\n success: false,\n message: error.message || 'Internal Server Error',\n details: error.details,\n });\n});\n\nconst port = Number(process.env.PORT || 8788);\napp.listen(port, '0.0.0.0', () => {\n logger.info(\n {\n port,\n dbPath,\n dataDir,\n deepseekModel,\n cbetaApiRoot,\n },\n 'Dacheng AI backend started',\n );\n});\n"}, "1c898b6bcae39967522aa063b3f9b9672db5124b": {"t": 1786068489.6810653, "n": 144923, "h": "55cea71410263f42", "c": "import crypto from 'node:crypto';\nimport fs from 'node:fs';\nimport path from 'node:path';\nimport process from 'node:process';\nimport { fileURLToPath } from 'node:url';\n\nimport { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';\nimport { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';\nimport Database from 'better-sqlite3';\nimport cors from 'cors';\nimport dotenv from 'dotenv';\nimport express from 'express';\nimport rateLimit from 'express-rate-limit';\nimport helmet from 'helmet';\nimport pino from 'pino';\nimport { z } from 'zod';\n\nimport { registerPlatformApi } from './platform_api.js';\nimport {\n codexResponsesMessages,\n codexResponsesTools,\n deepSeekResultToResponseItems,\n} from './codex_deepseek_adapter.js';\nimport {\n createPluginCodexPolicy,\n pluginConversationNamespace,\n pluginMcpTokenEnv,\n} from './plugin_codex_policy.js';\nimport {\n handleOfficialMcpRequest,\n officialMcpApps,\n} from './official_mcp_apps.js';\n\ndotenv.config();\n\nconst __dirname = path.dirname(fileURLToPath(import.meta.url));\nconst rootDir = path.resolve(__dirname, '..');\nconst dataDir = process.env.DATA_DIR || path.join(rootDir, 'data');\nconst resourcesDir = path.join(dataDir, 'resources');\nconst openClawRuntimeUpdatesDir =\n process.env.OPENCLAW_RUNTIME_UPDATES_DIR || path.join(dataDir, 'openclaw-runtime');\nconst openClawRuntimeEnableMacosUpdates =\n process.env.OPENCLAW_RUNTIME_ENABLE_MACOS_UPDATES === 'true';\nconst codexHomeDir = process.env.CODEX_HOME || path.join(dataDir, 'codex-home');\nconst codexPluginSessionsDir = path.join(dataDir, 'codex-plugin-sessions');\nconst codexTempDir = process.env.CODEX_TMPDIR || path.join(dataDir, 'codex-tmp');\nconst codexRuntimeDir = process.env.XDG_RUNTIME_DIR || path.join(dataDir, 'codex-runtime');\nconst dbPath = process.env.SQLITE_PATH || path.join(dataDir, 'dacheng-ai.sqlite');\n\nfs.mkdirSync(resourcesDir, { recursive: true });\nfs.mkdirSync(openClawRuntimeUpdatesDir, { recursive: true });\nfs.mkdirSync(codexHomeDir, { recursive: true });\nfs.mkdirSync(codexPluginSessionsDir, { recursive: true });\nfs.mkdirSync(codexTempDir, { recursive: true });\nfs.mkdirSync(codexRuntimeDir, { recursive: true });\n\nconst logger = pino({\n level: process.env.LOG_LEVEL || 'info',\n});\n\nconst app = express();\napp.disable('x-powered-by');\napp.set('trust proxy', Number(process.env.TRUST_PROXY_HOPS || 1));\napp.use(helmet());\nconst corsOrigin =\n !process.env.CORS_ORIGIN || process.env.CORS_ORIGIN.trim() === '*'\n ? true\n : process.env.CORS_ORIGIN.split(',').map((item) => item.trim()).filter(Boolean);\napp.use(cors({\n origin: corsOrigin,\n credentials: true,\n exposedHeaders: ['mcp-session-id', 'mcp-protocol-version'],\n}));\napp.use(express.json({ limit: '1mb' }));\napp.use(\n rateLimit({\n windowMs: 60_000,\n limit: Number(process.env.RATE_LIMIT_PER_MINUTE || 90),\n standardHeaders: true,\n legacyHeaders: false,\n }),\n);\n\nconst db = new Database(dbPath);\ndb.pragma('journal_mode = WAL');\ndb.pragma('foreign_keys = ON');\n\ndb.exec(`\n CREATE TABLE IF NOT EXISTS conversations (\n id TEXT PRIMARY KEY,\n user_id TEXT NOT NULL,\n title TEXT NOT NULL,\n provider TEXT NOT NULL,\n model TEXT NOT NULL,\n created_at TEXT NOT NULL,\n updated_at TEXT NOT NULL\n );\n\n CREATE INDEX IF NOT EXISTS idx_conversations_user_updated\n ON conversations (user_id, updated_at DESC);\n\n CREATE TABLE IF NOT EXISTS messages (\n id TEXT PRIMARY KEY,\n conversation_id TEXT NOT NULL,\n role TEXT NOT NULL,\n content TEXT NOT NULL,\n prompt_tokens INTEGER NOT NULL DEFAULT 0,\n completion_tokens INTEGER NOT NULL DEFAULT 0,\n total_tokens INTEGER NOT NULL DEFAULT 0,\n created_at TEXT NOT NULL,\n FOREIGN KEY (conversation_id) REFERENCES conversations(id) ON DELETE CASCADE\n );\n\n CREATE INDEX IF NOT EXISTS idx_messages_conversation_created\n ON messages (conversation_id, created_at ASC);\n\n CREATE TABLE IF NOT EXISTS usage_months (\n user_id TEXT NOT NULL,\n month TEXT NOT NULL,\n total_tokens INTEGER NOT NULL DEFAULT 0,\n updated_at TEXT NOT NULL,\n PRIMARY KEY (user_id, month)\n );\n\n CREATE TABLE IF NOT EXISTS resource_downloads (\n id TEXT PRIMARY KEY,\n user_id TEXT NOT NULL,\n title TEXT NOT NULL,\n source_name TEXT NOT NULL,\n source_url TEXT NOT NULL,\n file_name TEXT NOT NULL,\n file_path TEXT NOT NULL,\n created_at TEXT NOT NULL\n );\n\n CREATE TABLE IF NOT EXISTS agent_runs (\n id TEXT PRIMARY KEY,\n user_id TEXT NOT NULL,\n conversation_id TEXT NOT NULL,\n message_id TEXT NOT NULL,\n openclaw_run_id TEXT,\n openclaw_session_key TEXT NOT NULL,\n status TEXT NOT NULL,\n mode TEXT NOT NULL,\n provider TEXT NOT NULL,\n model TEXT,\n input_tokens INTEGER NOT NULL DEFAULT 0,\n output_tokens INTEGER NOT NULL DEFAULT 0,\n tool_call_count INTEGER NOT NULL DEFAULT 0,\n cost_microusd INTEGER NOT NULL DEFAULT 0,\n started_at TEXT NOT NULL,\n completed_at TEXT,\n failed_at TEXT,\n error_code TEXT,\n error_message TEXT\n );\n\n CREATE INDEX IF NOT EXISTS idx_agent_runs_user_started\n ON agent_runs (user_id, started_at DESC);\n\n CREATE INDEX IF NOT EXISTS idx_agent_runs_status_started\n ON agent_runs (status, started_at DESC);\n\n CREATE TABLE IF NOT EXISTS agent_message_feedback (\n id TEXT PRIMARY KEY,\n message_id TEXT NOT NULL,\n user_id TEXT NOT NULL,\n rating TEXT NOT NULL,\n reason TEXT,\n comment TEXT,\n created_at TEXT NOT NULL\n );\n`);\n\nconst conversationColumns = new Set(\n db.prepare('PRAGMA table_info(conversations)').all().map((column) => column.name),\n);\nif (!conversationColumns.has('namespace')) {\n db.exec('ALTER TABLE conversations ADD COLUMN namespace TEXT');\n}\nif (!conversationColumns.has('thread_id')) {\n db.exec('ALTER TABLE conversations ADD COLUMN thread_id TEXT');\n}\ndb.exec(`\n CREATE INDEX IF NOT EXISTS idx_conversations_user_namespace_updated\n ON conversations (user_id, namespace, updated_at DESC)\n`);\n\nconst statements = {\n insertConversation: db.prepare(`\n INSERT INTO conversations (id, user_id, title, provider, model, created_at, updated_at)\n VALUES (@id, @userId, @title, @provider, @model, @createdAt, @updatedAt)\n `),\n updateConversation: db.prepare(`\n UPDATE conversations\n SET title = @title, updated_at = @updatedAt\n WHERE id = @id AND user_id = @userId\n `),\n getConversation: db.prepare(`\n SELECT * FROM conversations WHERE id = ? AND user_id = ? LIMIT 1\n `),\n listConversations: db.prepare(`\n SELECT id, title, updated_at AS updatedAt\n FROM conversations\n WHERE user_id = ? AND (namespace IS NULL OR namespace = '')\n ORDER BY updated_at DESC\n LIMIT 30\n `),\n insertPluginConversation: db.prepare(`\n INSERT INTO conversations (\n id, user_id, title, provider, model, namespace, thread_id, created_at, updated_at\n )\n VALUES (\n @id, @userId, @title, @provider, @model, @namespace, NULL, @createdAt, @updatedAt\n )\n `),\n getPluginConversation: db.prepare(`\n SELECT * FROM conversations\n WHERE id = ? AND user_id = ? AND namespace = ?\n LIMIT 1\n `),\n updatePluginConversation: db.prepare(`\n UPDATE conversations\n SET title = @title, thread_id = @threadId, updated_at = @updatedAt\n WHERE id = @id AND user_id = @userId AND namespace = @namespace\n `),\n insertMessage: db.prepare(`\n INSERT INTO messages (\n id, conversation_id, role, content,\n prompt_tokens, completion_tokens, total_tokens, created_at\n )\n VALUES (\n @id, @conversationId, @role, @content,\n @promptTokens, @completionTokens, @totalTokens, @createdAt\n )\n `),\n listMessages: db.prepare(`\n SELECT role, content, prompt_tokens AS promptTokens,\n completion_tokens AS completionTokens, total_tokens AS totalTokens,\n created_at AS createdAt\n FROM messages\n WHERE conversation_id = ?\n ORDER BY created_at ASC\n `),\n getUsage: db.prepare(`\n SELECT total_tokens AS totalTokens FROM usage_months WHERE user_id = ? AND month = ?\n `),\n upsertUsage: db.prepare(`\n INSERT INTO usage_months (user_id, month, total_tokens, updated_at)\n VALUES (@userId, @month, @totalTokens, @updatedAt)\n ON CONFLICT(user_id, month)\n DO UPDATE SET total_tokens = usage_months.total_tokens + excluded.total_tokens,\n updated_at = excluded.updated_at\n `),\n insertResourceDownload: db.prepare(`\n INSERT INTO resource_downloads (\n id, user_id, title, source_name, source_url, file_name, file_path, created_at\n )\n VALUES (\n @id, @userId, @title, @sourceName, @sourceUrl, @fileName, @filePath, @createdAt\n )\n `),\n insertAgentRun: db.prepare(`\n INSERT INTO agent_runs (\n id, user_id, conversation_id, message_id, openclaw_run_id, openclaw_session_key,\n status, mode, provider, model, started_at\n )\n VALUES (\n @id, @userId, @conversationId, @messageId, @openClawRunId, @openClawSessionKey,\n @status, @mode, @provider, @model, @startedAt\n )\n `),\n getAgentRun: db.prepare(`\n SELECT * FROM agent_runs WHERE id = ? AND user_id = ? LIMIT 1\n `),\n updateAgentRunStatus: db.prepare(`\n UPDATE agent_runs\n SET status = @status,\n openclaw_run_id = COALESCE(@openClawRunId, openclaw_run_id),\n input_tokens = COALESCE(@inputTokens, input_tokens),\n output_tokens = COALESCE(@outputTokens, output_tokens),\n tool_call_count = COALESCE(@toolCallCount, tool_call_count),\n completed_at = @completedAt,\n failed_at = @failedAt,\n error_code = @errorCode,\n error_message = @errorMessage\n WHERE id = @id AND user_id = @userId\n `),\n insertAgentFeedback: db.prepare(`\n INSERT INTO agent_message_feedback (id, message_id, user_id, rating, reason, comment, created_at)\n VALUES (@id, @messageId, @userId, @rating, @reason, @comment, @createdAt)\n `),\n};\n\nconst deepseekApiKey = process.env.DEEPSEEK_API_KEY || '';\nconst deepseekBaseUrl = (process.env.DEEPSEEK_BASE_URL || 'https://api.deepseek.com').replace(/\\/+$/, '');\nconst deepseekModel = process.env.DEEPSEEK_MODEL || 'deepseek-chat';\nconst deepseekAvailableModels = Array.from(\n new Set(\n (process.env.DEEPSEEK_MODELS || `${deepseekModel},deepseek-chat,deepseek-reasoner`)\n .split(',')\n .map((item) => normalizeDeepSeekModelName(item))\n .filter(Boolean),\n ),\n);\nconst cbetaApiRoot = (process.env.CBETA_API_ROOT || 'https://api.ombhrum.com/api/cbeta').replace(/\\/+$/, '');\nconst fabushiApiBaseUrl = (process.env.FABUSHI_API_BASE_URL || 'https://api.ombhrum.com').replace(/\\/+$/, '');\nconst memberMonthlyLimit = Number(process.env.MEMBER_MONTHLY_TOKEN_LIMIT || 1_000_000);\nconst freeMonthlyLimit = Number(process.env.FREE_MONTHLY_TOKEN_LIMIT || 1_000);\nconst requireMemberForAi = process.env.REQUIRE_MEMBER_FOR_AI === 'true';\nconst deepseekMaxCompletionTokens = Math.max(1, Number(process.env.DEEPSEEK_MAX_COMPLETION_TOKENS || 1400));\nconst botFatherMaxCompletionTokens = Math.max(\n deepseekMaxCompletionTokens,\n Math.min(12_000, Number(process.env.BOT_FATHER_MAX_COMPLETION_TOKENS || 6000)),\n);\nconst maxResourceTextChars = Number(process.env.MAX_RESOURCE_TEXT_CHARS || 80_000);\nconst openClawRuntimeManifestPath =\n process.env.OPENCLAW_RUNTIME_MANIFEST_PATH || path.join(openClawRuntimeUpdatesDir, 'manifest.json');\nconst openClawRuntimePublicBaseUrl = (process.env.OPENCLAW_RUNTIME_PUBLIC_BASE_URL || '').replace(/\\/+$/, '');\nconst enableLibreChatAgentChat = process.env.ENABLE_LIBRECHAT_AGENT_CHAT === 'true';\nconst libreChatAgentsBaseUrl = (\n process.env.LIBRECHAT_AGENTS_BASE_URL || 'http://127.0.0.1:3080/api/agents/v1'\n).replace(/\\/+$/, '');\nconst libreChatAgentApiKey = process.env.LIBRECHAT_AGENT_API_KEY || '';\nconst libreChatAgentId = process.env.LIBRECHAT_AGENT_ID || '';\nconst enableCodexSdkChat = process.env.ENABLE_CODEX_SDK_CHAT === 'true';\nconst enableOpenClawAgentChat = process.env.ENABLE_OPENCLAW_AGENT_CHAT === 'true';\nconst openClawGatewayUrl = (process.env.OPENCLAW_GATEWAY_URL || 'http://127.0.0.1:18789').replace(/\\/+$/, '');\nconst openClawGatewayToken = process.env.OPENCLAW_GATEWAY_TOKEN || '';\nconst openClawAgentId = process.env.OPENCLAW_AGENT_ID || 'fabushi-public-agent';\nconst openClawRunsEndpoint = process.env.OPENCLAW_RUNS_ENDPOINT || '';\nconst codexDeepSeekProviderId = 'dacheng-deepseek-proxy';\nconst codexResponsesBaseUrl = (\n process.env.CODEX_DEEPSEEK_RESPONSES_BASE_URL ||\n `http://127.0.0.1:${process.env.PORT || 8788}/codex-deepseek/v1`\n).replace(/\\/+$/, '');\nconst codexAdapterTokenPrefix = 'dacheng-codex-proxy.';\nconst codexAdapterTokenTtlMs = Math.max(\n 60_000,\n Number(process.env.CODEX_DEEPSEEK_ADAPTER_TOKEN_TTL_MS || 15 * 60_000),\n);\nconst codexAdapterSecret =\n process.env.CODEX_DEEPSEEK_ADAPTER_SECRET ||\n deepseekApiKey ||\n openClawGatewayToken ||\n 'dacheng-codex-local-dev-secret';\n\nconst systemPrompt = [\n '你是“大乘”App 的 AI 助手。',\n '你的核心任务是帮助用户查找、整理、理解并全球法布施合法可分享的佛法资源。',\n '回答要庄重、简洁、可执行;涉及经典、仪轨或资源时,提醒用户尊重版权、来源和当地法规。',\n '如果用户想找资源、下载经文、寻找音频或准备可分享资料,你必须先在后端自动执行搜索、验证和下载步骤;不要要求用户去前端 + 菜单手动查找。',\n '你可以把执行进度总结给用户,但不要输出隐藏推理原文。',\n].join('\\n');\n\nfunction nowIso() {\n return new Date().toISOString();\n}\n\nfunction monthKey(date = new Date()) {\n return `${date.getUTCFullYear()}-${String(date.getUTCMonth() + 1).padStart(2, '0')}`;\n}\n\nfunction jsonResponse(res, status, payload) {\n res.status(status).json(payload);\n}\n\nasync function readJsonFile(filePath) {\n const raw = await fs.promises.readFile(filePath, 'utf8');\n return JSON.parse(raw);\n}\n\nfunction requestOrigin(req) {\n if (openClawRuntimePublicBaseUrl) return openClawRuntimePublicBaseUrl;\n const proto = req.get('X-Forwarded-Proto') || req.protocol || 'https';\n const host = req.get('X-Forwarded-Host') || req.get('Host');\n return host ? `${proto}://${host}` : '';\n}\n\nfunction absolutizeOpenClawRuntimeUrl(req, value) {\n const text = readText(value);\n if (!text) return '';\n if (/^https?:\\/\\//i.test(text)) return text;\n const normalized = text.startsWith('/')\n ? text\n : `/api/openclaw/runtime/files/${text.replace(/^\\/+/, '')}`;\n const origin = requestOrigin(req);\n return origin ? new URL(normalized, `${origin}/`).toString() : normalized;\n}\n\nfunction textToolResult(text, structuredContent = {}) {\n return {\n structuredContent,\n content: [{ type: 'text', text }],\n };\n}\n\nfunction sseHeaders(res) {\n res.status(200);\n res.setHeader('Content-Type', 'text/event-stream; charset=utf-8');\n res.setHeader('Cache-Control', 'no-cache, no-transform');\n res.setHeader('Connection', 'keep-alive');\n res.setHeader('X-Accel-Buffering', 'no');\n if (typeof res.flushHeaders === 'function') res.flushHeaders();\n}\n\nfunction writeSse(res, event, payload) {\n res.write(`event: ${event}\\n`);\n res.write(`data: ${JSON.stringify(payload)}\\n\\n`);\n}\n\nfunction asyncHandler(fn) {\n return (req, res, next) => {\n Promise.resolve(fn(req, res, next)).catch(next);\n };\n}\n\nfunction bearerToken(req) {\n const delegated = readText(req.get('x-dacheng-auth-token'));\n if (\n delegated &&\n delegated !== 'DACHENG_AUTH_TOKEN' &&\n !delegated.startsWith('secretref-env:') &&\n delegated !== 'dacheng-openclaw-proxy'\n ) {\n return delegated;\n }\n const header = req.get('Authorization') || '';\n if (!header.startsWith('Bearer ')) return '';\n const token = header.slice('Bearer '.length).trim();\n return token === 'dacheng-openclaw-proxy' ? '' : token;\n}\n\nfunction sha256(value) {\n return crypto.createHash('sha256').update(value).digest('hex');\n}\n\nfunction safeUserText(value, fallback = '') {\n if (typeof value !== 'string') return fallback;\n return value.replace(/\\u0000/g, '').trim();\n}\n\nfunction estimateTokens(text) {\n const normalized = String(text || '').trim();\n if (!normalized) return 0;\n return Math.max(1, Math.ceil(normalized.length / 3));\n}\n\nfunction readText(value) {\n if (value === undefined || value === null) return '';\n return String(value).replace(/\\u0000/g, '').trim();\n}\n\nfunction clientContextMessage(client) {\n if (!client || typeof client !== 'object') return '';\n const selectedModel = readText(client.selectedModel || client.model);\n const project = client.project && typeof client.project === 'object' ? client.project : {};\n const projectName = readText(project.name);\n const projectPath = readText(project.path);\n const lines = ['桌面端上下文:'];\n if (selectedModel) lines.push(`用户选择的 DeepSeek 模型:${normalizeDeepSeekModelName(selectedModel)}`);\n if (projectPath) {\n lines.push(`当前项目名称:${projectName || projectPath}`);\n lines.push(`当前项目路径:${projectPath}`);\n lines.push('如果本轮任务涉及读取、修改、运行、分析或整理项目,请优先围绕当前项目路径工作;不要擅自操作其他目录。');\n }\n return lines.length > 1 ? lines.join('\\n') : '';\n}\nfunction unwrapPayload(payload) {\n if (!payload || typeof payload !== 'object') return {};\n return payload.data && typeof payload.data === 'object' ? payload.data : payload;\n}\n\nfunction readFirstText(values) {\n for (const value of values) {\n const text = readText(value);\n if (text) return text;\n }\n return '';\n}\n\nfunction activeFlag(value) {\n if (value === true) return true;\n if (value === 1) return true;\n if (typeof value !== 'string') return false;\n return ['true', '1', 'yes', 'active'].includes(value.trim().toLowerCase());\n}\n\nfunction futureDate(value) {\n const text = readText(value);\n if (!text) return false;\n const timestamp = Date.parse(text);\n return Number.isFinite(timestamp) && timestamp > Date.now();\n}\n\nfunction normalizeMembershipPayload(payload) {\n if (!payload || typeof payload !== 'object') return false;\n const data = unwrapPayload(payload);\n const membership = data.membership && typeof data.membership === 'object' ? data.membership : data;\n const type = readText(membership.type || membership.membershipType || data.membershipType).toLowerCase();\n const status = readText(membership.status || data.status).toLowerCase();\n const expiry = membership.expiresAt ||\n membership.expires_at ||\n membership.membershipExpiry ||\n membership.membership_expires_at ||\n data.membershipExpiry ||\n data.membership_expires_at;\n const hasExplicitActiveFlag =\n membership.isActive !== undefined ||\n membership.active !== undefined ||\n membership.hasPremiumAccess !== undefined ||\n membership.hasPremiumMembership !== undefined ||\n membership.isPremium !== undefined;\n\n if (hasExplicitActiveFlag) {\n return Boolean(\n activeFlag(membership.isActive) ||\n activeFlag(membership.active) ||\n activeFlag(membership.hasPremiumAccess) ||\n activeFlag(membership.hasPremiumMembership) ||\n activeFlag(membership.isPremium),\n );\n }\n\n return Boolean(\n status === 'active' ||\n type === 'paid' ||\n type === 'premium' ||\n type === 'trial' ||\n futureDate(expiry),\n );\n}\n\nfunction extractRemoteAccount(payload) {\n const data = unwrapPayload(payload);\n const nestedUser = data.user && typeof data.user === 'object' ? data.user : {};\n const id = readFirstText([\n data.userId,\n data.user_id,\n data.userNo,\n data.user_no,\n data.id,\n nestedUser.userId,\n nestedUser.user_id,\n nestedUser.userNo,\n nestedUser.user_no,\n nestedUser.id,\n ]);\n const username = readFirstText([data.username, data.name, nestedUser.username, nestedUser.name]);\n const email = readFirstText([data.email, nestedUser.email]);\n const membership = data.membership && typeof data.membership === 'object' ? data.membership : null;\n return {\n authenticated: Boolean(id || username || email || membership || data.success === true),\n id,\n username,\n email,\n isMember: normalizeMembershipPayload(payload),\n membership,\n };\n}\n\nasync function resolveRemoteAccount(token) {\n const fallback = {\n authenticated: false,\n id: '',\n username: '',\n email: '',\n isMember: false,\n membership: null,\n };\n if (!token || !fabushiApiBaseUrl) return fallback;\n\n const endpoints = [\n '/api/stripe/membership-status',\n '/api/alipay/check-membership',\n '/api/auth/user-info',\n ];\n\n for (const endpoint of endpoints) {\n try {\n const response = await fetch(`${fabushiApiBaseUrl}${endpoint}`, {\n headers: {\n Accept: 'application/json',\n Authorization: `Bearer ${token}`,\n },\n signal: AbortSignal.timeout(8_000),\n });\n if (response.status === 401 || response.status === 403) return fallback;\n if (!response.ok) continue;\n const payload = await response.json();\n const account = extractRemoteAccount(payload);\n if (account.authenticated || account.isMember) {\n return {\n ...fallback,\n ...account,\n authenticated: true,\n };\n }\n } catch (error) {\n logger.debug({ endpoint, error: String(error) }, 'membership lookup skipped');\n }\n }\n\n return fallback;\n}\n\nasync function resolveUser(req, body = {}) {\n const token = bearerToken(req);\n const usernameHint = safeUserText(body.username || req.query.username);\n\n const internalMcpAccount = resolveCodexAdapterToken(token);\n if (\n internalMcpAccount?.audience === 'mcp-plugin' &&\n req.path.startsWith('/api/mcp/apps/')\n ) {\n return internalMcpAccount.user;\n }\n\n const testToken = readText(process.env.TEST_ACCOUNT_TOKEN);\n if (testToken && token && token === testToken) {\n return {\n userId: 'user:test_account',\n username: 'TestAccount',\n tokenHash: sha256(token).slice(0, 24),\n isAuthenticated: true,\n isMember: true,\n isTestAccount: true,\n membership: { type: 'lifetime', active: true },\n };\n }\n\n const tokenHash = token ? sha256(token).slice(0, 24) : '';\n const anonymousHash = sha256(`${req.ip}|${req.get('User-Agent') || ''}`).slice(0, 24);\n const remoteAccount = await resolveRemoteAccount(token);\n const remoteIdentity = remoteAccount.id ||\n remoteAccount.username ||\n remoteAccount.email ||\n (remoteAccount.authenticated ? tokenHash : '');\n const userId = remoteAccount.authenticated\n ? `user:${remoteIdentity}`\n : tokenHash\n ? `token:${tokenHash}`\n : `anon:${anonymousHash}`;\n return {\n userId,\n username: remoteAccount.username || usernameHint,\n tokenHash,\n isAuthenticated: remoteAccount.authenticated,\n isMember: remoteAccount.isMember,\n membership: remoteAccount.membership,\n };\n}\n\nfunction base64UrlJson(value) {\n return Buffer.from(JSON.stringify(value), 'utf8').toString('base64url');\n}\n\nfunction timingSafeEqualText(left, right) {\n const leftBuffer = Buffer.from(String(left || ''));\n const rightBuffer = Buffer.from(String(right || ''));\n if (leftBuffer.length !== rightBuffer.length) return false;\n return crypto.timingSafeEqual(leftBuffer, rightBuffer);\n}\n\nfunction signCodexAdapterPayload(encodedPayload) {\n return crypto\n .createHmac('sha256', codexAdapterSecret)\n .update(encodedPayload)\n .digest('base64url');\n}\n\nfunction createCodexAdapterToken(user, options = {}) {\n const requestedMaxCompletionTokens = Number(options.maxCompletionTokens || 0);\n const payload = {\n sub: user.userId,\n username: user.username || '',\n tokenHash: user.tokenHash || '',\n isAuthenticated: Boolean(user.isAuthenticated),\n isMember: Boolean(user.isMember),\n isTestAccount: Boolean(user.isTestAccount),\n membership: user.membership || {},\n aud: safeUserText(options.audience) || 'codex-responses',\n recordUsage: options.recordUsage !== false,\n maxCompletionTokens:\n Number.isFinite(requestedMaxCompletionTokens) && requestedMaxCompletionTokens > 0\n ? Math.floor(requestedMaxCompletionTokens)\n : null,\n exp: Date.now() + codexAdapterTokenTtlMs,\n };\n const encodedPayload = base64UrlJson(payload);\n return `${codexAdapterTokenPrefix}${encodedPayload}.${signCodexAdapterPayload(encodedPayload)}`;\n}\n\nfunction resolveCodexAdapterToken(token) {\n const raw = readText(token);\n if (!raw.startsWith(codexAdapterTokenPrefix)) return null;\n\n const signed = raw.slice(codexAdapterTokenPrefix.length);\n const separator = signed.lastIndexOf('.');\n if (separator < 1) return null;\n\n const encodedPayload = signed.slice(0, separator);\n const signature = signed.slice(separator + 1);\n const expectedSignature = signCodexAdapterPayload(encodedPayload);\n if (!timingSafeEqualText(signature, expectedSignature)) return null;\n\n let payload;\n try {\n payload = JSON.parse(Buffer.from(encodedPayload, 'base64url').toString('utf8'));\n } catch {\n return null;\n }\n\n if (!payload || typeof payload !== 'object') return null;\n if (Number(payload.exp || 0) < Date.now()) return null;\n\n const userId = safeUserText(payload.sub);\n if (!userId) return null;\n\n return {\n user: {\n userId,\n username: safeUserText(payload.username),\n tokenHash: safeUserText(payload.tokenHash),\n isAuthenticated: Boolean(payload.isAuthenticated),\n isMember: Boolean(payload.isMember),\n isTestAccount: Boolean(payload.isTestAccount),\n membership:\n payload.membership && typeof payload.membership === 'object'\n ? payload.membership\n : {},\n },\n audience: safeUserText(payload.aud) || 'codex-responses',\n recordUsage: payload.recordUsage !== false,\n maxCompletionTokens:\n Number.isFinite(Number(payload.maxCompletionTokens)) && Number(payload.maxCompletionTokens) > 0\n ? Math.floor(Number(payload.maxCompletionTokens))\n : null,\n };\n}\n\nasync function resolveCodexResponsesBilling(req) {\n const token = bearerToken(req);\n const adapter = resolveCodexAdapterToken(token);\n if (adapter) return adapter;\n\n if (deepseekApiKey && token === deepseekApiKey) {\n const error = new Error('Codex DeepSeek adapter now requires first-party user billing');\n error.statusCode = 401;\n throw error;\n }\n\n return {\n user: await resolveUser(req, req.body || {}),\n recordUsage: true,\n };\n}\n\nfunction usageFor(userId) {\n const month = monthKey();\n const row = statements.getUsage.get(userId, month);\n return {\n month,\n used: Number(row?.totalTokens || 0),\n };\n}\n\nfunction enforceTokenBudget(user, estimatedTokensForRequest) {\n if (user && user.isTestAccount) {\n return { limit: 999999999, usage: { month: monthKey(), used: 0 } };\n }\n if (requireMemberForAi && !user.isMember) {\n const error = new Error('大乘 AI 需要开通会员后使用');\n error.statusCode = 403;\n error.code = 'AI_MEMBERSHIP_REQUIRED';\n error.details = {\n requireMemberForAi,\n monthlyLimit: memberMonthlyLimit,\n remainingTokens: 0,\n };\n throw error;\n }\n\n const limit = user.isMember ? memberMonthlyLimit : freeMonthlyLimit;\n const usage = usageFor(user.userId);\n if (usage.used + estimatedTokensForRequest > limit) {\n const error = new Error('本月 AI token 额度已不足');\n error.statusCode = 429;\n error.code = 'USER_TOKEN_QUOTA_EXCEEDED';\n error.details = {\n monthlyLimit: limit,\n usedTokens: usage.used,\n remainingTokens: Math.max(0, limit - usage.used),\n };\n throw error;\n }\n return { limit, usage };\n}\n\nfunction completionBudgetFor(\n budget,\n estimatedPromptTokens,\n maximumCompletionTokens = deepseekMaxCompletionTokens,\n) {\n const remainingAfterPrompt = budget.limit - budget.usage.used - estimatedPromptTokens;\n return Math.max(1, Math.min(maximumCompletionTokens, Math.floor(remainingAfterPrompt)));\n}\n\nfunction recordUsage(userId, totalTokens) {\n if (!totalTokens || totalTokens < 1) return;\n statements.upsertUsage.run({\n userId,\n month: monthKey(),\n totalTokens,\n updatedAt: nowIso(),\n });\n}\n\nfunction titleFromMessage(message) {\n const compact = message.replace(/\\s+/g, ' ').trim();\n if (!compact) return '新对话';\n return compact.length > 22 ? `${compact.slice(0, 22)}...` : compact;\n}\n\nfunction normalizeMessages(rows) {\n return rows.map((row) => ({\n role: row.role,\n content: row.content,\n }));\n}\n\nfunction normalizeMaxCompletionTokens(\n value,\n maximumCompletionTokens = deepseekMaxCompletionTokens,\n) {\n const numeric = Number(value || maximumCompletionTokens);\n if (!Number.isFinite(numeric) || numeric < 1) return maximumCompletionTokens;\n return Math.max(1, Math.min(maximumCompletionTokens, Math.floor(numeric)));\n}\n\nfunction normalizeDeepSeekModelName(value) {\n const raw = readText(value || deepseekModel);\n if (!raw) return deepseekModel;\n const modelId = raw.includes('/') ? raw.split('/').pop() : raw;\n return modelId || deepseekModel;\n}\n\nasync function callDeepSeek(messages, options = {}) {\n if (!deepseekApiKey) {\n const error = new Error('DeepSeek API key is not configured');\n error.statusCode = 503;\n throw error;\n }\n\n const model = normalizeDeepSeekModelName(options.model);\n const response = await fetch(`${deepseekBaseUrl}/chat/completions`, {\n method: 'POST',\n headers: {\n Accept: 'application/json',\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${deepseekApiKey}`,\n },\n body: JSON.stringify({\n model,\n messages,\n temperature: 0.4,\n max_tokens: normalizeMaxCompletionTokens(\n options.maxCompletionTokens,\n options.maximumCompletionTokens || deepseekMaxCompletionTokens,\n ),\n stream: false,\n ...(options.tools?.length ? { tools: options.tools, tool_choice: 'auto' } : {}),\n }),\n signal: AbortSignal.timeout(60_000),\n });\n\n const bodyText = await response.text();\n let payload;\n try {\n payload = bodyText ? JSON.parse(bodyText) : {};\n } catch {\n payload = { error: bodyText };\n }\n\n if (!response.ok) {\n const upstreamMessage = payload?.error?.message || payload?.message || '';\n const friendlyMessage =\n response.status === 402 || /insufficient\\s+balance/i.test(upstreamMessage)\n ? '大乘 AI 服务额度暂不可用,请联系管理员充值 DeepSeek 账户后重试。'\n : upstreamMessage || `DeepSeek request failed: ${response.status}`;\n const error = new Error(friendlyMessage);\n error.statusCode = response.status;\n if (response.status === 402 || /insufficient\\s+balance/i.test(upstreamMessage)) {\n error.code = 'DEEPSEEK_PROVIDER_QUOTA';\n }\n throw error;\n }\n\n const upstreamMessage = payload?.choices?.[0]?.message || {};\n const message = typeof upstreamMessage.content === 'string'\n ? upstreamMessage.content.trim()\n : '';\n const toolCalls = Array.isArray(upstreamMessage.tool_calls)\n ? upstreamMessage.tool_calls\n : [];\n if (!message && toolCalls.length === 0) {\n const error = new Error('DeepSeek returned an empty response');\n error.statusCode = 502;\n throw error;\n }\n\n return {\n message,\n toolCalls,\n model,\n usage: {\n promptTokens: Number(payload?.usage?.prompt_tokens || 0),\n completionTokens: Number(payload?.usage?.completion_tokens || 0),\n totalTokens: Number(payload?.usage?.total_tokens || 0),\n },\n };\n}\n\nasync function callDeepSeekStream(messages, callbacks = {}) {\n if (!deepseekApiKey) {\n const error = new Error('DeepSeek API key is not configured');\n error.statusCode = 503;\n throw error;\n }\n\n const model = normalizeDeepSeekModelName(callbacks.model);\n const response = await fetch(`${deepseekBaseUrl}/chat/completions`, {\n method: 'POST',\n headers: {\n Accept: 'text/event-stream',\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${deepseekApiKey}`,\n },\n body: JSON.stringify({\n model,\n messages,\n temperature: 0.4,\n max_tokens: normalizeMaxCompletionTokens(\n callbacks.maxCompletionTokens,\n callbacks.maximumCompletionTokens || deepseekMaxCompletionTokens,\n ),\n stream: true,\n stream_options: { include_usage: true },\n }),\n signal: callbacks.signal || AbortSignal.timeout(90_000),\n });\n\n if (!response.ok) {\n const bodyText = await response.text();\n let payload;\n try {\n payload = bodyText ? JSON.parse(bodyText) : {};\n } catch {\n payload = { error: bodyText };\n }\n const upstreamMessage = payload?.error?.message || payload?.message || '';\n const friendlyMessage =\n response.status === 402 || /insufficient\\s+balance/i.test(upstreamMessage)\n ? '大乘 AI 服务额度暂不可用,请联系管理员充值 DeepSeek 账户后重试。'\n : upstreamMessage || `DeepSeek request failed: ${response.status}`;\n const error = new Error(friendlyMessage);\n error.statusCode = response.status;\n if (response.status === 402 || /insufficient\\s+balance/i.test(upstreamMessage)) {\n error.code = 'DEEPSEEK_PROVIDER_QUOTA';\n }\n throw error;\n }\n\n let buffer = '';\n let message = '';\n let usage = {\n promptTokens: 0,\n completionTokens: 0,\n totalTokens: 0,\n };\n const decoder = new TextDecoder();\n\n for await (const chunk of response.body) {\n buffer += decoder.decode(chunk, { stream: true });\n const lines = buffer.split(/\\r?\\n/);\n buffer = lines.pop() || '';\n\n for (const rawLine of lines) {\n const line = rawLine.trim();\n if (!line.startsWith('data:')) continue;\n const data = line.slice('data:'.length).trim();\n if (!data || data === '[DONE]') continue;\n\n let payload;\n try {\n payload = JSON.parse(data);\n } catch {\n continue;\n }\n\n const delta = payload?.choices?.[0]?.delta?.content || '';\n if (delta) {\n message += delta;\n callbacks.onToken?.(delta);\n }\n\n if (payload?.usage) {\n usage = {\n promptTokens: Number(payload.usage.prompt_tokens || usage.promptTokens || 0),\n completionTokens: Number(payload.usage.completion_tokens || usage.completionTokens || 0),\n totalTokens: Number(payload.usage.total_tokens || usage.totalTokens || 0),\n };\n }\n }\n }\n\n message = message.trim();\n if (!message) {\n const error = new Error('DeepSeek returned an empty response');\n error.statusCode = 502;\n throw error;\n }\n\n return { message, model, usage };\n}\n\nfunction createCodexDeepSeekRuntime(user = null, runtimeOptions = {}) {\n if (!enableCodexSdkChat || !deepseekApiKey) {\n return {\n enabled: false,\n provider: codexDeepSeekProviderId,\n reason: enableCodexSdkChat ? 'DeepSeek API key is not configured' : 'ENABLE_CODEX_SDK_CHAT is not true',\n };\n }\n\n const adapterToken = user\n ? createCodexAdapterToken(user, {\n maxCompletionTokens: runtimeOptions.maxCompletionTokens,\n })\n : '';\n return {\n enabled: true,\n provider: codexDeepSeekProviderId,\n options: {\n apiKey: adapterToken,\n baseUrl: codexResponsesBaseUrl,\n env: {\n ...process.env,\n DEEPSEEK_API_KEY: adapterToken,\n HOME: runtimeOptions.codexHomeDir || codexHomeDir,\n CODEX_HOME: runtimeOptions.codexHomeDir || codexHomeDir,\n TMPDIR: codexTempDir,\n XDG_RUNTIME_DIR: codexRuntimeDir,\n XDG_CACHE_HOME: path.join(codexHomeDir, '.cache'),\n XDG_CONFIG_HOME: path.join(codexHomeDir, '.config'),\n XDG_DATA_HOME: path.join(codexHomeDir, '.local', 'share'),\n PATH: process.env.PATH || '/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin',\n ...(runtimeOptions.env || {}),\n },\n config: {\n model_provider: codexDeepSeekProviderId,\n model_providers: {\n [codexDeepSeekProviderId]: {\n name: 'Dacheng DeepSeek Proxy',\n base_url: codexResponsesBaseUrl,\n env_key: 'DEEPSEEK_API_KEY',\n wire_api: 'responses',\n query_params: {},\n },\n },\n ...(runtimeOptions.config || {}),\n },\n },\n threadOptions: {\n model: deepseekModel,\n approvalPolicy: runtimeOptions.approvalPolicy || 'never',\n sandboxMode: runtimeOptions.sandboxMode || 'read-only',\n ...(runtimeOptions.workingDirectory\n ? { workingDirectory: runtimeOptions.workingDirectory }\n : {}),\n skipGitRepoCheck: true,\n networkAccessEnabled: runtimeOptions.networkAccessEnabled ?? true,\n webSearchMode: runtimeOptions.webSearchMode || 'disabled',\n },\n };\n}\n\nfunction codexPromptFromMessages(messages) {\n return messages\n .map((message) => {\n const role = String(message.role || 'user').toUpperCase();\n return `${role}:\\n${message.content}`;\n })\n .join('\\n\\n');\n}\n\nfunction responseContentText(content) {\n if (typeof content === 'string') return content;\n if (!Array.isArray(content)) return '';\n return content\n .map((part) => {\n if (typeof part === 'string') return part;\n if (!part || typeof part !== 'object') return '';\n return String(part.text || part.input_text || part.output_text || '');\n })\n .filter(Boolean)\n .join('\\n');\n}\n\nfunction codexResponsesPrompt(body) {\n const input = Array.isArray(body?.input) ? body.input : [];\n const userItems = input.filter((item) => item?.role === 'user');\n const source = userItems[userItems.length - 1] || input[input.length - 1];\n const prompt = responseContentText(source?.content).trim();\n if (prompt) return prompt;\n if (typeof body?.input === 'string') return body.input.trim();\n return '';\n}\n\nfunction writeResponsesEvent(res, event, data) {\n res.write(`event: ${event}\\n`);\n res.write(`data: ${JSON.stringify(data)}\\n\\n`);\n}\n\nfunction responseUsage(usage = {}) {\n return {\n input_tokens: Number(usage.promptTokens || usage.input_tokens || 0),\n cached_input_tokens: 0,\n output_tokens: Number(usage.completionTokens || usage.output_tokens || 0),\n reasoning_output_tokens: 0,\n total_tokens: Number(usage.totalTokens || usage.total_tokens || 0),\n };\n}\n\nfunction createLibreChatAgentRuntime() {\n if (!enableLibreChatAgentChat || !libreChatAgentApiKey || !libreChatAgentId) {\n return {\n enabled: false,\n provider: 'librechat-agent',\n reason: !enableLibreChatAgentChat\n ? 'ENABLE_LIBRECHAT_AGENT_CHAT is not true'\n : !libreChatAgentApiKey\n ? 'LIBRECHAT_AGENT_API_KEY is not configured'\n : 'LIBRECHAT_AGENT_ID is not configured',\n };\n }\n\n return {\n enabled: true,\n provider: 'librechat-agent',\n model: libreChatAgentId,\n chatCompletionsUrl: `${libreChatAgentsBaseUrl}/chat/completions`,\n };\n}\n\nfunction libreChatUsage(usage = {}) {\n return {\n promptTokens: Number(usage.prompt_tokens || usage.input_tokens || usage.promptTokens || 0),\n completionTokens: Number(usage.completion_tokens || usage.output_tokens || usage.completionTokens || 0),\n totalTokens: Number(usage.total_tokens || usage.totalTokens || 0),\n };\n}\n\nasync function callLibreChatAgent(messages) {\n const runtime = createLibreChatAgentRuntime();\n if (!runtime.enabled) {\n const error = new Error(runtime.reason || 'LibreChat Agent chat is not enabled');\n error.statusCode = 503;\n throw error;\n }\n\n const response = await fetch(runtime.chatCompletionsUrl, {\n method: 'POST',\n headers: {\n Accept: 'application/json',\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${libreChatAgentApiKey}`,\n },\n body: JSON.stringify({\n model: runtime.model,\n messages,\n temperature: 0.4,\n stream: false,\n }),\n signal: AbortSignal.timeout(120_000),\n });\n\n const bodyText = await response.text();\n let payload;\n try {\n payload = bodyText ? JSON.parse(bodyText) : {};\n } catch {\n payload = { error: bodyText };\n }\n\n if (!response.ok) {\n const upstreamMessage = payload?.error?.message || payload?.message || bodyText || '';\n const error = new Error(upstreamMessage || `LibreChat Agent request failed: ${response.status}`);\n error.statusCode = response.status;\n throw error;\n }\n\n const message = payload?.choices?.[0]?.message?.content?.trim();\n if (!message) {\n const error = new Error('LibreChat Agent returned an empty response');\n error.statusCode = 502;\n throw error;\n }\n\n return {\n message,\n usage: libreChatUsage(payload?.usage),\n };\n}\n\nasync function callLibreChatAgentStream(messages, callbacks = {}) {\n const runtime = createLibreChatAgentRuntime();\n if (!runtime.enabled) {\n const error = new Error(runtime.reason || 'LibreChat Agent chat is not enabled');\n error.statusCode = 503;\n throw error;\n }\n\n const response = await fetch(runtime.chatCompletionsUrl, {\n method: 'POST',\n headers: {\n Accept: 'text/event-stream',\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${libreChatAgentApiKey}`,\n },\n body: JSON.stringify({\n model: runtime.model,\n messages,\n temperature: 0.4,\n stream: true,\n }),\n signal: callbacks.signal || AbortSignal.timeout(180_000),\n });\n\n if (!response.ok) {\n const bodyText = await response.text();\n let payload;\n try {\n payload = bodyText ? JSON.parse(bodyText) : {};\n } catch {\n payload = { error: bodyText };\n }\n const upstreamMessage = payload?.error?.message || payload?.message || bodyText || '';\n const error = new Error(upstreamMessage || `LibreChat Agent request failed: ${response.status}`);\n error.statusCode = response.status;\n throw error;\n }\n\n let buffer = '';\n let message = '';\n let usage = { promptTokens: 0, completionTokens: 0, totalTokens: 0 };\n const decoder = new TextDecoder();\n\n for await (const chunk of response.body) {\n buffer += decoder.decode(chunk, { stream: true });\n const lines = buffer.split(/\\r?\\n/);\n buffer = lines.pop() || '';\n\n for (const rawLine of lines) {\n const line = rawLine.trim();\n if (!line.startsWith('data:')) continue;\n const data = line.slice('data:'.length).trim();\n if (!data || data === '[DONE]') continue;\n\n let payload;\n try {\n payload = JSON.parse(data);\n } catch {\n continue;\n }\n\n const delta = payload?.choices?.[0]?.delta?.content || '';\n if (delta) {\n message += delta;\n callbacks.onToken?.(delta);\n }\n\n if (payload?.usage) {\n usage = libreChatUsage(payload.usage);\n }\n }\n }\n\n message = message.trim();\n if (!message) {\n const error = new Error('LibreChat Agent returned an empty response');\n error.statusCode = 502;\n throw error;\n }\n\n return { message, usage };\n}\n\nfunction isDeepSeekQuotaError(error) {\n if (error?.code === 'DEEPSEEK_PROVIDER_QUOTA') return true;\n if (error?.code === 'USER_TOKEN_QUOTA_EXCEEDED' || error?.code === 'AI_MEMBERSHIP_REQUIRED') return false;\n const message = String(error?.message || error || '');\n return /insufficient\\s+balance|balance/i.test(message);\n}\n\nfunction publicAiErrorMessage(error) {\n if (isDeepSeekQuotaError(error)) {\n return '大乘 AI 服务额度暂不可用,请联系管理员充值 DeepSeek 账户后重试。';\n }\n return error?.message || 'Internal Server Error';\n}\n\nfunction responsesPayload({\n responseId,\n itemId,\n status,\n model,\n text = '',\n usage = null,\n error = null,\n outputItems = null,\n}) {\n return {\n id: responseId,\n object: 'response',\n status,\n model,\n output: status === 'completed'\n ? outputItems || [\n {\n id: itemId,\n type: 'message',\n role: 'assistant',\n status: 'completed',\n content: [{ type: 'output_text', text }],\n },\n ]\n : [],\n ...(usage ? { usage: responseUsage(usage) } : {}),\n ...(error\n ? {\n error: {\n message: error.message || String(error),\n type: 'server_error',\n },\n }\n : {}),\n };\n}\n\nasync function callCodexSdkDeepSeek(messages, callbacks = {}) {\n const codexRuntime = createCodexDeepSeekRuntime(callbacks.user, {\n maxCompletionTokens: callbacks.maxCompletionTokens,\n workingDirectory: callbacks.workingDirectory,\n sandboxMode: callbacks.sandboxMode,\n approvalPolicy: callbacks.approvalPolicy,\n networkAccessEnabled: callbacks.networkAccessEnabled,\n webSearchMode: callbacks.webSearchMode,\n codexHomeDir: callbacks.codexHomeDir,\n env: callbacks.env,\n config: callbacks.config,\n });\n if (!codexRuntime.enabled) {\n const error = new Error(codexRuntime.reason || 'Codex SDK chat is not enabled');\n error.statusCode = 503;\n throw error;\n }\n if (!callbacks.user) {\n const error = new Error('Codex SDK DeepSeek proxy requires a resolved user for billing');\n error.statusCode = 401;\n throw error;\n }\n\n const { Codex } = await import('@openai/codex-sdk');\n const codex = new Codex(codexRuntime.options);\n const thread = callbacks.threadId\n ? codex.resumeThread(callbacks.threadId, codexRuntime.threadOptions)\n : codex.startThread(codexRuntime.threadOptions);\n const prompt = codexPromptFromMessages(messages);\n const turnOptions = {\n ...(callbacks.outputSchema ? { outputSchema: callbacks.outputSchema } : {}),\n ...(callbacks.signal ? { signal: callbacks.signal } : {}),\n };\n\n if (callbacks.onToken || callbacks.onStep) {\n const { events } = await thread.runStreamed(prompt, turnOptions);\n let finalResponse = '';\n let usage = null;\n let threadId = callbacks.threadId || null;\n\n for await (const event of events) {\n if (event.type === 'thread.started') {\n threadId = event.thread_id || threadId;\n } else if (event.type === 'item.completed') {\n const item = event.item;\n if (item.type === 'agent_message') {\n finalResponse = item.text || finalResponse;\n callbacks.onToken?.(item.text || '');\n } else if (item.type === 'todo_list') {\n callbacks.onStep?.({\n title: 'Codex SDK 执行计划',\n message: item.items.map((todo) => `${todo.completed ? '✓' : '•'} ${todo.text}`).join('\\n'),\n });\n } else if (item.type === 'command_execution') {\n callbacks.onStep?.({\n title: 'Codex SDK 执行命令',\n message: item.command,\n });\n } else if (item.type === 'file_change') {\n callbacks.onStep?.({\n title: 'Codex SDK 修改文件',\n message: item.changes.map((change) => `${change.kind}: ${change.path}`).join('\\n'),\n });\n } else if (item.type === 'mcp_tool_call') {\n callbacks.onStep?.({\n title: 'Codex SDK 调用工具',\n message: `${item.server}.${item.tool}`,\n });\n }\n } else if (event.type === 'turn.completed') {\n usage = event.usage;\n } else if (event.type === 'turn.failed') {\n throw new Error(event.error?.message || 'Codex SDK turn failed');\n } else if (event.type === 'error') {\n throw new Error(event.message || 'Codex SDK stream failed');\n }\n }\n\n if (!finalResponse.trim()) {\n const error = new Error('Codex SDK returned an empty response');\n error.statusCode = 502;\n throw error;\n }\n\n return {\n message: finalResponse.trim(),\n usage: {\n promptTokens: Number(usage?.input_tokens || 0),\n completionTokens: Number(usage?.output_tokens || 0),\n totalTokens: Number((usage?.input_tokens || 0) + (usage?.output_tokens || 0)),\n },\n usageAlreadyRecorded: true,\n threadId: thread.id || threadId,\n };\n }\n\n const turn = await thread.run(prompt, turnOptions);\n if (!turn.finalResponse?.trim()) {\n const error = new Error('Codex SDK returned an empty response');\n error.statusCode = 502;\n throw error;\n }\n\n return {\n message: turn.finalResponse.trim(),\n usage: {\n promptTokens: Number(turn.usage?.input_tokens || 0),\n completionTokens: Number(turn.usage?.output_tokens || 0),\n totalTokens: Number((turn.usage?.input_tokens || 0) + (turn.usage?.output_tokens || 0)),\n },\n usageAlreadyRecorded: true,\n threadId: thread.id || callbacks.threadId || null,\n };\n}\n\nconst activeAgentRuns = new Map();\n\nfunction normalizeAgentMode(value) {\n const mode = safeUserText(value || 'dharma_guide');\n return /^[a-z][a-z0-9_:-]{0,48}$/i.test(mode) ? mode : 'dharma_guide';\n}\n\nfunction openClawSessionKey(user, conversationId) {\n const env = process.env.NODE_ENV === 'production' ? 'prod' : 'dev';\n return `fabushi:env:${env}:user:${user.userId}:conversation:${conversationId}`;\n}\n\nfunction createOpenClawRuntime() {\n if (!enableOpenClawAgentChat || !openClawGatewayToken || !openClawGatewayUrl) {\n return {\n enabled: false,\n provider: 'openclaw-gateway',\n reason: !enableOpenClawAgentChat\n ? 'ENABLE_OPENCLAW_AGENT_CHAT is not true'\n : !openClawGatewayToken\n ? 'OPENCLAW_GATEWAY_TOKEN is not configured'\n : 'OPENCLAW_GATEWAY_URL is not configured',\n };\n }\n\n return {\n enabled: true,\n provider: 'openclaw-gateway',\n model: openClawAgentId,\n runsUrl: openClawRunsEndpoint || `${openClawGatewayUrl}/v1/agents/${encodeURIComponent(openClawAgentId)}/runs`,\n };\n}\n\nfunction extractOpenClawText(payload) {\n if (!payload || typeof payload !== 'object') return '';\n const candidates = [\n payload.message,\n payload.content,\n payload.output_text,\n payload.finalResponse,\n payload.final_response,\n payload.result?.message,\n payload.result?.content,\n payload.output?.message,\n payload.output?.content,\n payload.choices?.[0]?.message?.content,\n ];\n for (const candidate of candidates) {\n if (typeof candidate === 'string' && candidate.trim()) return candidate.trim();\n }\n return '';\n}\n\nfunction extractOpenClawUsage(payload) {\n const usage = payload?.usage || payload?.result?.usage || {};\n return {\n promptTokens: Number(usage.input_tokens || usage.prompt_tokens || usage.promptTokens || 0),\n completionTokens: Number(usage.output_tokens || usage.completion_tokens || usage.completionTokens || 0),\n totalTokens: Number(usage.total_tokens || usage.totalTokens || 0),\n };\n}\n\nasync function callOpenClawAgent({ messages, user, conversationId, mode, signal }) {\n const runtime = createOpenClawRuntime();\n if (!runtime.enabled) {\n const error = new Error(runtime.reason || 'OpenClaw Agent chat is not enabled');\n error.statusCode = 503;\n throw error;\n }\n\n const sessionKey = openClawSessionKey(user, conversationId);\n const response = await fetch(runtime.runsUrl, {\n method: 'POST',\n headers: {\n Accept: 'application/json',\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${openClawGatewayToken}`,\n },\n body: JSON.stringify({\n agentId: openClawAgentId,\n sessionKey,\n mode,\n stream: false,\n input: { messages },\n messages,\n metadata: {\n app: 'fabushi',\n userId: user.userId,\n conversationId,\n toolPolicy: 'public-user-read-mostly',\n },\n }),\n signal: signal || AbortSignal.timeout(180_000),\n });\n\n const bodyText = await response.text();\n let payload;\n try {\n payload = bodyText ? JSON.parse(bodyText) : {};\n } catch {\n payload = { message: bodyText };\n }\n\n if (!response.ok) {\n const error = new Error(payload?.error?.message || payload?.message || `OpenClaw request failed: ${response.status}`);\n error.statusCode = response.status;\n throw error;\n }\n\n const message = extractOpenClawText(payload);\n if (!message) {\n const error = new Error('OpenClaw returned an empty response');\n error.statusCode = 502;\n throw error;\n }\n\n return {\n message,\n runId: payload.runId || payload.id || payload.result?.runId || null,\n provider: runtime.provider,\n model: runtime.model,\n usage: extractOpenClawUsage(payload),\n };\n}\n\nasync function runAgentModel({\n messages,\n user,\n conversationId,\n mode,\n callbacks = {},\n signal,\n maxCompletionTokens,\n preferCodex = false,\n requireCodex = false,\n workingDirectory,\n sandboxMode,\n outputSchema,\n threadId,\n}) {\n const codexRuntime = createCodexDeepSeekRuntime(user, {\n workingDirectory,\n sandboxMode,\n maxCompletionTokens,\n });\n if (requireCodex && !codexRuntime.enabled) {\n const error = new Error(codexRuntime.reason || 'Codex SDK chat is not enabled');\n error.statusCode = 503;\n throw error;\n }\n\n let preferredCodexFailed = false;\n if ((preferCodex || requireCodex) && codexRuntime.enabled) {\n try {\n const result = await callCodexSdkDeepSeek(messages, {\n ...callbacks,\n user,\n signal,\n maxCompletionTokens,\n workingDirectory,\n sandboxMode,\n outputSchema,\n threadId,\n });\n return {\n ...result,\n provider: codexRuntime.provider,\n model: deepseekModel,\n };\n } catch (error) {\n if (requireCodex || isDeepSeekQuotaError(error)) throw error;\n logger.warn(\n { error: String(error), mode },\n 'Preferred Codex SDK model failed, continuing through agent fallbacks',\n );\n preferredCodexFailed = true;\n }\n }\n\n const openClawRuntime = createOpenClawRuntime();\n if (openClawRuntime.enabled && !callbacks.onToken) {\n return await callOpenClawAgent({ messages, user, conversationId, mode, signal });\n }\n\n const libreChatRuntime = createLibreChatAgentRuntime();\n let provider = libreChatRuntime.enabled\n ? libreChatRuntime.provider\n : codexRuntime.enabled && !preferredCodexFailed\n ? codexRuntime.provider\n : 'deepseek';\n let model = libreChatRuntime.enabled ? libreChatRuntime.model : deepseekModel;\n\n try {\n if (libreChatRuntime.enabled) {\n const result = callbacks.onToken\n ? await callLibreChatAgentStream(messages, { ...callbacks, signal })\n : await callLibreChatAgent(messages);\n return { ...result, provider, model };\n }\n if (codexRuntime.enabled && !preferredCodexFailed) {\n const result = await callCodexSdkDeepSeek(messages, {\n ...callbacks,\n user,\n signal,\n maxCompletionTokens,\n workingDirectory,\n sandboxMode,\n outputSchema,\n threadId,\n });\n return { ...result, provider, model };\n }\n const result = callbacks.onToken\n ? await callDeepSeekStream(messages, { ...callbacks, signal, maxCompletionTokens })\n : await callDeepSeek(messages, { maxCompletionTokens });\n return { ...result, provider, model };\n } catch (error) {\n if (!codexRuntime.enabled || preferredCodexFailed || isDeepSeekQuotaError(error)) throw error;\n logger.warn({ error: String(error) }, 'Agent model failed, falling back to DeepSeek direct');\n const result = callbacks.onToken\n ? await callDeepSeekStream(messages, { ...callbacks, signal, maxCompletionTokens })\n : await callDeepSeek(messages, { maxCompletionTokens });\n return { ...result, provider: 'deepseek', model: deepseekModel };\n }\n}\n\nfunction updateAgentRunStatus(run, status, fields = {}) {\n statements.updateAgentRunStatus.run({\n id: run.id,\n userId: run.user_id || run.userId,\n status,\n openClawRunId: fields.openClawRunId ?? null,\n inputTokens: fields.inputTokens ?? null,\n outputTokens: fields.outputTokens ?? null,\n toolCallCount: fields.toolCallCount ?? null,\n completedAt: status === 'completed' || status === 'cancelled' ? nowIso() : null,\n failedAt: status === 'failed' ? nowIso() : null,\n errorCode: fields.errorCode ?? null,\n errorMessage: fields.errorMessage ?? null,\n });\n}\n\nfunction buildAgentMessages(conversationId) {\n const historyRows = statements.listMessages.all(conversationId).slice(-14);\n return [{ role: 'system', content: systemPrompt }, ...normalizeMessages(historyRows)];\n}\n\nfunction agentUsagePayload(usage, user, budget) {\n const promptTokens = usage.promptTokens || usage.inputTokens || 0;\n const completionTokens = usage.completionTokens || usage.outputTokens || 0;\n const totalTokens = usage.totalTokens || promptTokens + completionTokens;\n const latestUsage = usageFor(user.userId);\n return {\n inputTokens: promptTokens,\n outputTokens: completionTokens,\n totalTokens,\n monthlyLimit: budget.limit,\n remainingTokens: Math.max(0, budget.limit - latestUsage.used),\n };\n}\n\nfunction openAiContentToText(content) {\n if (typeof content === 'string') return content;\n if (Array.isArray(content)) {\n return content\n .map((part) => {\n if (typeof part === 'string') return part;\n if (!part || typeof part !== 'object') return '';\n return readText(part.text || part.content || part.value);\n })\n .filter(Boolean)\n .join('\\n');\n }\n return readText(content);\n}\n\nfunction normalizeOpenAiMessages(value) {\n if (!Array.isArray(value)) return [];\n const allowedRoles = new Set(['system', 'user', 'assistant', 'tool']);\n return value\n .map((item) => {\n if (!item || typeof item !== 'object') return null;\n const role = allowedRoles.has(item.role) ? item.role : 'user';\n const content = openAiContentToText(item.content);\n if (!content) return null;\n return { role, content };\n })\n .filter(Boolean);\n}\n\nfunction truncateForDeepSeekMessage(text, maxChars = 12_000) {\n const value = readText(text);\n if (value.length <= maxChars) return value;\n return `${value.slice(0, maxChars)}\\n\\n[内容已截断]`;\n}\n\nfunction truncateTailForDeepSeekMessage(text, maxChars = 2400) {\n const value = readText(text);\n if (value.length <= maxChars) return value;\n return `[前文已省略]\\n\\n${value.slice(-maxChars)}`;\n}\n\nfunction stripOpenClawPromptNoise(text) {\n const value = readText(text);\n if (!value) return '';\n\n const finalPromptMatch = value.match(/finalPromptText[\"'\\s::]+([\\s\\S]{1,4000})$/i);\n if (finalPromptMatch?.[1]) {\n return readText(finalPromptMatch[1].replace(/^[\"'`]+|[\"'`,}\\]]+$/g, ''));\n }\n\n const markers = [\n 'final prompt:',\n 'final user prompt:',\n 'user prompt:',\n '用户问题:',\n '用户消息:',\n '用户输入:',\n ];\n const lower = value.toLowerCase();\n for (const marker of markers) {\n const index = lower.lastIndexOf(marker.toLowerCase());\n if (index >= 0) {\n const tail = value.slice(index + marker.length).trim();\n if (tail) return tail;\n }\n }\n\n return value;\n}\n\nfunction compactOpenClawMessagesForDeepSeek(messages) {\n const conversationalMessages = messages\n .filter((message) => message.role === 'user' || message.role === 'assistant')\n .map((message) => ({\n role: message.role,\n content: truncateTailForDeepSeekMessage(stripOpenClawPromptNoise(message.content)),\n }))\n .filter((message) => message.content);\n\n const recent = conversationalMessages.slice(-6);\n while (recent.length > 0 && recent[0].role !== 'user') {\n recent.shift();\n }\n\n if (recent.length === 0) {\n const fallback = messages\n .slice()\n .reverse()\n .map((message) => truncateTailForDeepSeekMessage(stripOpenClawPromptNoise(message.content), 2400))\n .find(Boolean);\n if (!fallback) return messages;\n recent.push({ role: 'user', content: fallback });\n }\n\n return [\n {\n role: 'system',\n content: '你是大乘 App 的桌面 AI 助手。请直接回答用户问题,默认使用简洁、准确的中文。',\n },\n ...recent,\n ];\n}\n\nfunction isDeepSeekBlockedError(error) {\n return (\n Number(error?.statusCode) === 403 &&\n /blocked|被拦截|forbidden/i.test(String(error?.message || error || ''))\n );\n}\n\nfunction shouldRetryWithCompactedOpenClawMessages(error, messages, compactedMessages) {\n if (!isDeepSeekBlockedError(error)) return false;\n return JSON.stringify(messages) !== JSON.stringify(compactedMessages);\n}\n\nfunction openClawBlockedFallbackText(messages) {\n const lastUser = messages\n .slice()\n .reverse()\n .find((message) => message.role === 'user')?.content || '';\n const localActionIntent =\n /(file|folder|directory|project|workspace|path|文件|文件夹|目录|项目|工程|路径|本地)/i.test(lastUser) &&\n /(create|make|mkdir|write|edit|modify|rename|delete|move|copy|save|download|read|list|生成|创建|新建|写入|编辑|修改|重命名|删除|移动|复制|保存|下载|读取|列出)/i.test(lastUser);\n\n if (localActionIntent) {\n return [\n '本机工具规划被模型服务拦截了,所以这次没有实际读取、创建、修改或下载本机文件。',\n '如果只是新建文件夹,请先在底部选择项目后重新发送“新建文件夹”;桌面端会直接在已选项目里创建。',\n '如果是更复杂的本地文件操作,请在“连应用”里重启本机 AI 后重试,或把要操作的文件路径和目标说得更具体。',\n ].join('\\n');\n }\n\n return '模型服务拦截了这次回复,未能生成结果。请换一种更具体、更短的表述后重试。';\n}\nfunction usageWithFallback(usage, messages, text) {\n const promptTokens = Number(usage?.promptTokens || usage?.inputTokens || 0);\n const completionTokens = Number(usage?.completionTokens || usage?.outputTokens || 0);\n const totalTokens = Number(usage?.totalTokens || 0);\n if (totalTokens > 0) {\n return {\n promptTokens,\n completionTokens,\n totalTokens,\n };\n }\n const estimatedPrompt = promptTokens || estimateTokens(JSON.stringify(messages));\n const estimatedCompletion = completionTokens || estimateTokens(text);\n return {\n promptTokens: estimatedPrompt,\n completionTokens: estimatedCompletion,\n totalTokens: estimatedPrompt + estimatedCompletion,\n };\n}\n\nfunction openAiUsagePayload(usage, user, budget) {\n const promptTokens = Number(usage.promptTokens || 0);\n const completionTokens = Number(usage.completionTokens || 0);\n const totalTokens = Number(usage.totalTokens || promptTokens + completionTokens);\n const latestUsage = usageFor(user.userId);\n const remainingTokens = Math.max(0, budget.limit - latestUsage.used);\n return {\n prompt_tokens: promptTokens,\n completion_tokens: completionTokens,\n total_tokens: totalTokens,\n promptTokens,\n completionTokens,\n totalTokens,\n monthlyLimit: budget.limit,\n remainingTokens,\n };\n}\n\nfunction writeOpenAiSse(res, payload) {\n res.write(`data: ${JSON.stringify(payload)}\\n\\n`);\n}\n\napp.post(\n '/api/openclaw/deepseek/v1/chat/completions',\n asyncHandler(async (req, res) => {\n const user = await resolveUser(req, req.body || {});\n const incomingMessages = normalizeOpenAiMessages(req.body?.messages);\n if (incomingMessages.length === 0) {\n res.status(400).json({ error: { message: 'messages is required' } });\n return;\n }\n\n const messages = compactOpenClawMessagesForDeepSeek(incomingMessages);\n const promptEstimate = estimateTokens(JSON.stringify(messages));\n const estimated = promptEstimate + 600;\n const budget = enforceTokenBudget(user, estimated);\n const requestCompletionLimit = deepseekMaxCompletionTokens;\n const maxCompletionTokens = Math.min(\n completionBudgetFor(budget, promptEstimate, requestCompletionLimit),\n normalizeMaxCompletionTokens(req.body?.max_tokens, requestCompletionLimit),\n );\n const model = normalizeDeepSeekModelName(req.body?.model);\n const wantsStream =\n req.body?.stream === true ||\n String(req.get('accept') || '').includes('text/event-stream');\n const id = `chatcmpl_${crypto.randomUUID().replaceAll('-', '')}`;\n const created = Math.floor(Date.now() / 1000);\n const compactedMessages = messages;\n\n if (!wantsStream) {\n let result;\n let usageMessages = messages;\n try {\n result = await callDeepSeek(messages, {\n model,\n maxCompletionTokens,\n maximumCompletionTokens: requestCompletionLimit,\n });\n } catch (error) {\n if (!shouldRetryWithCompactedOpenClawMessages(error, messages, compactedMessages)) {\n throw error;\n }\n logger.warn(\n {\n statusCode: error?.statusCode,\n messageCount: messages.length,\n compactedMessageCount: compactedMessages.length,\n },\n 'OpenClaw DeepSeek proxy retrying with compacted messages',\n );\n usageMessages = compactedMessages;\n result = await callDeepSeek(compactedMessages, {\n model,\n maxCompletionTokens,\n maximumCompletionTokens: requestCompletionLimit,\n });\n }\n const usage = usageWithFallback(result.usage, usageMessages, result.message);\n recordUsage(user.userId, usage.totalTokens);\n res.json({\n id,\n object: 'chat.completion',\n created,\n model: result.model || model,\n choices: [\n {\n index: 0,\n message: { role: 'assistant', content: result.message },\n finish_reason: 'stop',\n },\n ],\n usage: openAiUsagePayload(usage, user, budget),\n });\n return;\n }\n\n const abortController = new AbortController();\n req.on('close', () => abortController.abort());\n\n res.status(200);\n res.set({\n 'Content-Type': 'text/event-stream; charset=utf-8',\n 'Cache-Control': 'no-cache, no-transform',\n Connection: 'keep-alive',\n 'X-Accel-Buffering': 'no',\n });\n res.flushHeaders?.();\n\n const baseChunk = {\n id,\n object: 'chat.completion.chunk',\n created,\n model,\n system_fingerprint: 'fabushi-openclaw-deepseek-proxy',\n };\n writeOpenAiSse(res, {\n ...baseChunk,\n choices: [{ index: 0, delta: { role: 'assistant' }, finish_reason: null }],\n });\n\n let text = '';\n let usageMessages = messages;\n try {\n let result;\n try {\n result = await callDeepSeekStream(messages, {\n model,\n maxCompletionTokens,\n maximumCompletionTokens: requestCompletionLimit,\n signal: abortController.signal,\n onToken: (delta) => {\n text += delta;\n writeOpenAiSse(res, {\n ...baseChunk,\n choices: [{ index: 0, delta: { content: delta }, finish_reason: null }],\n });\n },\n });\n } catch (error) {\n if (!shouldRetryWithCompactedOpenClawMessages(error, messages, compactedMessages)) {\n throw error;\n }\n logger.warn(\n {\n statusCode: error?.statusCode,\n messageCount: messages.length,\n compactedMessageCount: compactedMessages.length,\n },\n 'OpenClaw DeepSeek proxy stream retrying with compacted messages',\n );\n text = '';\n usageMessages = compactedMessages;\n result = await callDeepSeekStream(compactedMessages, {\n model,\n maxCompletionTokens,\n maximumCompletionTokens: requestCompletionLimit,\n signal: abortController.signal,\n onToken: (delta) => {\n text += delta;\n writeOpenAiSse(res, {\n ...baseChunk,\n choices: [{ index: 0, delta: { content: delta }, finish_reason: null }],\n });\n },\n });\n }\n text = result.message || text;\n const usage = usageWithFallback(result.usage, usageMessages, text);\n recordUsage(user.userId, usage.totalTokens);\n writeOpenAiSse(res, {\n ...baseChunk,\n model: result.model || model,\n choices: [{ index: 0, delta: {}, finish_reason: 'stop' }],\n });\n writeOpenAiSse(res, {\n ...baseChunk,\n model: result.model || model,\n choices: [],\n usage: openAiUsagePayload(usage, user, budget),\n });\n res.write('data: [DONE]\\n\\n');\n } catch (error) {\n if (isDeepSeekBlockedError(error)) {\n const fallbackMessage = openClawBlockedFallbackText(usageMessages);\n const usage = usageWithFallback(null, usageMessages, fallbackMessage);\n recordUsage(user.userId, usage.totalTokens);\n logger.warn(\n { statusCode: error?.statusCode, code: error?.code },\n 'OpenClaw DeepSeek proxy stream blocked; returning local fallback message',\n );\n writeOpenAiSse(res, {\n ...baseChunk,\n choices: [{ index: 0, delta: { content: fallbackMessage }, finish_reason: null }],\n });\n writeOpenAiSse(res, {\n ...baseChunk,\n choices: [{ index: 0, delta: {}, finish_reason: 'stop' }],\n });\n writeOpenAiSse(res, {\n ...baseChunk,\n choices: [],\n usage: openAiUsagePayload(usage, user, budget),\n });\n res.write('data: [DONE]\\n\\n');\n return;\n }\n logger.error(\n { error: String(error), code: error?.code, statusCode: error?.statusCode },\n 'OpenClaw DeepSeek proxy stream failed',\n );\n writeOpenAiSse(res, {\n ...baseChunk,\n choices: [],\n error: {\n message: publicAiErrorMessage(error),\n code: error?.code || 'OPENCLAW_DEEPSEEK_PROXY_ERROR',\n },\n });\n res.write('data: [DONE]\\n\\n');\n } finally {\n res.end();\n }\n }),\n);\n\napp.post(\n '/codex-deepseek/v1/responses',\n asyncHandler(async (req, res) => {\n if (!deepseekApiKey) {\n res.status(401).json({\n error: { message: 'Unauthorized Codex DeepSeek adapter request' },\n });\n return;\n }\n\n const messages = codexResponsesMessages(req.body);\n if (messages.length === 0) {\n res.status(400).json({ error: { message: 'Responses input is required' } });\n return;\n }\n const { tools: responseTools, kinds: responseToolKinds } = codexResponsesTools(req.body);\n\n const {\n user,\n recordUsage: shouldRecordUsage,\n maxCompletionTokens: adapterMaxCompletionTokens,\n } = await resolveCodexResponsesBilling(req);\n const promptEstimate = estimateTokens(JSON.stringify({ messages, tools: responseTools }));\n const budget = enforceTokenBudget(user, promptEstimate + 600);\n const responseCompletionLimit = adapterMaxCompletionTokens || deepseekMaxCompletionTokens;\n const maxCompletionTokens = Math.min(\n completionBudgetFor(budget, promptEstimate, responseCompletionLimit),\n normalizeMaxCompletionTokens(\n req.body?.max_output_tokens || req.body?.max_tokens,\n responseCompletionLimit,\n ),\n );\n const model = normalizeDeepSeekModelName(req.body?.model);\n const responseId = `resp_${crypto.randomUUID().replaceAll('-', '')}`;\n const itemId = `msg_${crypto.randomUUID().replaceAll('-', '')}`;\n const wantsStream =\n req.body?.stream !== false ||\n String(req.get('accept') || '').includes('text/event-stream');\n\n if (!wantsStream) {\n const result = await callDeepSeek(messages, {\n model,\n maxCompletionTokens,\n maximumCompletionTokens: responseCompletionLimit,\n tools: responseTools,\n });\n const outputItems = deepSeekResultToResponseItems(\n result,\n responseToolKinds,\n (prefix) => `${prefix}_${crypto.randomUUID().replaceAll('-', '')}`,\n );\n const usage = usageWithFallback(\n result.usage,\n messages,\n result.message || JSON.stringify(result.toolCalls || []),\n );\n if (shouldRecordUsage) recordUsage(user.userId, usage.totalTokens);\n res.json(\n responsesPayload({\n responseId,\n itemId,\n status: 'completed',\n model: result.model || model,\n text: result.message,\n usage,\n outputItems,\n }),\n );\n return;\n }\n\n if (responseTools.length > 0) {\n res.status(200);\n res.set({\n 'Content-Type': 'text/event-stream; charset=utf-8',\n 'Cache-Control': 'no-cache, no-transform',\n Connection: 'keep-alive',\n 'X-Accel-Buffering': 'no',\n });\n res.flushHeaders?.();\n writeResponsesEvent(res, 'response.created', {\n type: 'response.created',\n response: responsesPayload({ responseId, itemId, status: 'in_progress', model }),\n });\n try {\n const result = await callDeepSeek(messages, {\n model,\n maxCompletionTokens,\n maximumCompletionTokens: responseCompletionLimit,\n tools: responseTools,\n });\n const outputItems = deepSeekResultToResponseItems(\n result,\n responseToolKinds,\n (prefix) => `${prefix}_${crypto.randomUUID().replaceAll('-', '')}`,\n );\n outputItems.forEach((item, outputIndex) => {\n const addedItem = item.type === 'message'\n ? { ...item, status: 'in_progress', content: [] }\n : { ...item, status: 'in_progress' };\n writeResponsesEvent(res, 'response.output_item.added', {\n type: 'response.output_item.added',\n output_index: outputIndex,\n item: addedItem,\n });\n if (item.type === 'message') {\n const text = item.content?.find((part) => part.type === 'output_text')?.text || '';\n if (text) {\n writeResponsesEvent(res, 'response.output_text.delta', {\n type: 'response.output_text.delta',\n item_id: item.id,\n output_index: outputIndex,\n content_index: 0,\n delta: text,\n });\n }\n }\n writeResponsesEvent(res, 'response.output_item.done', {\n type: 'response.output_item.done',\n output_index: outputIndex,\n item,\n });\n });\n const usage = usageWithFallback(\n result.usage,\n messages,\n result.message || JSON.stringify(result.toolCalls || []),\n );\n if (shouldRecordUsage) recordUsage(user.userId, usage.totalTokens);\n writeResponsesEvent(res, 'response.completed', {\n type: 'response.completed',\n response: responsesPayload({\n responseId,\n itemId,\n status: 'completed',\n model: result.model || model,\n text: result.message,\n usage,\n outputItems,\n }),\n });\n } catch (error) {\n writeResponsesEvent(res, 'response.failed', {\n type: 'response.failed',\n response: responsesPayload({\n responseId,\n itemId,\n status: 'failed',\n model,\n error,\n }),\n });\n }\n res.end();\n return;\n }\n\n res.status(200);\n res.set({\n 'Content-Type': 'text/event-stream; charset=utf-8',\n 'Cache-Control': 'no-cache, no-transform',\n Connection: 'keep-alive',\n 'X-Accel-Buffering': 'no',\n });\n res.flushHeaders?.();\n\n writeResponsesEvent(res, 'response.created', {\n type: 'response.created',\n response: responsesPayload({\n responseId,\n itemId,\n status: 'in_progress',\n model,\n }),\n });\n writeResponsesEvent(res, 'response.output_item.added', {\n type: 'response.output_item.added',\n output_index: 0,\n item: {\n id: itemId,\n type: 'message',\n role: 'assistant',\n status: 'in_progress',\n content: [],\n },\n });\n writeResponsesEvent(res, 'response.content_part.added', {\n type: 'response.content_part.added',\n item_id: itemId,\n output_index: 0,\n content_index: 0,\n part: { type: 'output_text', text: '' },\n });\n\n let text = '';\n try {\n const result = await callDeepSeekStream(messages, {\n model,\n maxCompletionTokens,\n maximumCompletionTokens: responseCompletionLimit,\n onToken: (delta) => {\n text += delta;\n writeResponsesEvent(res, 'response.output_text.delta', {\n type: 'response.output_text.delta',\n item_id: itemId,\n output_index: 0,\n content_index: 0,\n delta,\n });\n },\n });\n text = result.message || text;\n const usage = usageWithFallback(result.usage, messages, text);\n if (shouldRecordUsage) recordUsage(user.userId, usage.totalTokens);\n writeResponsesEvent(res, 'response.output_text.done', {\n type: 'response.output_text.done',\n item_id: itemId,\n output_index: 0,\n content_index: 0,\n text,\n });\n writeResponsesEvent(res, 'response.content_part.done', {\n type: 'response.content_part.done',\n item_id: itemId,\n output_index: 0,\n content_index: 0,\n part: { type: 'output_text', text },\n });\n writeResponsesEvent(res, 'response.output_item.done', {\n type: 'response.output_item.done',\n output_index: 0,\n item: {\n id: itemId,\n type: 'message',\n role: 'assistant',\n status: 'completed',\n content: [{ type: 'output_text', text }],\n },\n });\n writeResponsesEvent(res, 'response.completed', {\n type: 'response.completed',\n response: responsesPayload({\n responseId,\n itemId,\n status: 'completed',\n model: result.model || model,\n text,\n usage,\n }),\n });\n res.end();\n } catch (error) {\n writeResponsesEvent(res, 'response.failed', {\n type: 'response.failed',\n response: responsesPayload({\n responseId,\n itemId,\n status: 'failed',\n model,\n text,\n error,\n }),\n });\n res.end();\n }\n }),\n);\n\napp.use(\n '/api/openclaw/runtime/files',\n express.static(openClawRuntimeUpdatesDir, {\n fallthrough: false,\n immutable: true,\n maxAge: '7d',\n setHeaders(res) {\n res.setHeader('X-Content-Type-Options', 'nosniff');\n },\n }),\n);\n\napp.get(\n '/api/openclaw/runtime/manifest',\n asyncHandler(async (req, res) => {\n const platform = readText(req.query.platform);\n const currentVersion = readText(req.query.currentVersion);\n const appVersion = readText(req.query.appVersion);\n\n let manifest;\n try {\n manifest = await readJsonFile(openClawRuntimeManifestPath);\n } catch (error) {\n logger.info(\n { manifestPath: openClawRuntimeManifestPath, error: String(error) },\n 'OpenClaw runtime manifest unavailable',\n );\n return jsonResponse(res, 200, {\n success: true,\n schema: 1,\n updateAvailable: false,\n reason: 'manifest-unavailable',\n platform,\n currentVersion,\n appVersion,\n });\n }\n\n const latest = manifest.latest && typeof manifest.latest === 'object'\n ? manifest.latest\n : manifest;\n const platforms = latest.platforms && typeof latest.platforms === 'object'\n ? latest.platforms\n : {};\n const platformRelease = platform ? platforms[platform] : null;\n const version = readText(latest.version || manifest.version);\n\n if (!platform || !platformRelease || typeof platformRelease !== 'object') {\n return jsonResponse(res, 200, {\n success: true,\n schema: Number(manifest.schema || 1),\n channel: readText(manifest.channel || latest.channel || 'stable'),\n updateAvailable: false,\n reason: platform ? 'platform-unavailable' : 'platform-required',\n platform,\n currentVersion,\n appVersion,\n latestVersion: version,\n });\n }\n\n if (platform.startsWith('macos-') && !openClawRuntimeEnableMacosUpdates) {\n return jsonResponse(res, 200, {\n success: true,\n schema: Number(manifest.schema || 1),\n channel: readText(manifest.channel || latest.channel || 'stable'),\n updateAvailable: false,\n reason: 'macos-runtime-updates-disabled',\n platform,\n currentVersion,\n appVersion,\n latestVersion: version,\n });\n }\n\n const downloadUrl = absolutizeOpenClawRuntimeUrl(\n req,\n platformRelease.archiveUrl || platformRelease.url || platformRelease.fileName,\n );\n const release = {\n version,\n channel: readText(manifest.channel || latest.channel || 'stable'),\n minAppVersion: readText(latest.minAppVersion || platformRelease.minAppVersion),\n notes: readText(latest.notes || platformRelease.notes),\n nodeExecutable: readText(\n platformRelease.nodeExecutable || latest.nodeExecutable || 'node/bin/node',\n ),\n cliEntrypoint: readText(\n platformRelease.cliEntrypoint || latest.cliEntrypoint || 'openclaw/openclaw.mjs',\n ),\n defaultPort: Number(latest.defaultPort || manifest.defaultPort || 18789),\n defaultModel: readText(latest.defaultModel || manifest.defaultModel || 'openclaw/default'),\n defaultModelOverride: readText(latest.defaultModelOverride || manifest.defaultModelOverride),\n gatewayArgs: Array.isArray(latest.gatewayArgs)\n ? latest.gatewayArgs.map((item) => String(item))\n : ['gateway', '--port', '{port}', '--force'],\n archiveUrl: downloadUrl,\n sha256: readText(platformRelease.sha256),\n size: Number(platformRelease.size || 0),\n };\n\n jsonResponse(res, 200, {\n success: true,\n schema: Number(manifest.schema || 1),\n updateAvailable: Boolean(version && downloadUrl && version !== currentVersion),\n platform,\n currentVersion,\n appVersion,\n latest: release,\n generatedAt: readText(manifest.generatedAt || latest.generatedAt),\n });\n }),\n);\n\napp.get('/health', (_req, res) => {\n const codexRuntime = createCodexDeepSeekRuntime();\n const libreChatRuntime = createLibreChatAgentRuntime();\n const openClawRuntime = createOpenClawRuntime();\n jsonResponse(res, 200, {\n status: 'ok',\n service: 'dacheng-ai-backend',\n provider: libreChatRuntime.enabled ? libreChatRuntime.provider : 'deepseek',\n model: libreChatRuntime.enabled ? libreChatRuntime.model : deepseekModel,\n deepseekModel,\n deepseekAvailableModels,\n libreChatAgentChatEnabled: libreChatRuntime.enabled,\n libreChatAgentProvider: libreChatRuntime.provider,\n libreChatAgentReason: libreChatRuntime.enabled ? undefined : libreChatRuntime.reason,\n codexSdkAvailable: true,\n codexSdkChatEnabled: codexRuntime.enabled,\n codexSdkProvider: codexRuntime.provider,\n openClawAgentChatEnabled: openClawRuntime.enabled,\n openClawAgentProvider: openClawRuntime.provider,\n openClawAgentReason: openClawRuntime.enabled ? undefined : openClawRuntime.reason,\n requireMemberForAi,\n memberMonthlyLimit,\n freeMonthlyLimit,\n deepseekMaxCompletionTokens,\n botFatherMaxCompletionTokens,\n openClawRuntimeUpdatesDir,\n openClawRuntimeManifestConfigured: fs.existsSync(openClawRuntimeManifestPath),\n cbetaApiRoot,\n timestamp: nowIso(),\n });\n});\n\napp.get(\n '/v1/ai/usage',\n asyncHandler(async (req, res) => {\n const user = await resolveUser(req, {});\n const now = new Date();\n const windowStart = Math.floor(Date.UTC(\n now.getUTCFullYear(), now.getUTCMonth(), 1,\n ) / 1000);\n const windowEnd = Math.floor(Date.UTC(\n now.getUTCFullYear(), now.getUTCMonth() + 1, 1,\n ) / 1000);\n const tokenLimit = user.isTestAccount\n ? 999_999_999\n : user.isMember ? memberMonthlyLimit : freeMonthlyLimit;\n const usedTokens = user.isTestAccount ? 0 : usageFor(user.userId).used;\n jsonResponse(res, 200, {\n windowStart,\n windowEnd,\n tokenLimit,\n usedTokens,\n reservedTokens: 0,\n remainingTokens: Math.max(0, tokenLimit - usedTokens),\n });\n }),\n);\n\napp.get('/api/plugins/registry', (_req, res) => {\n jsonResponse(res, 200, {\n success: true,\n schemaVersion: 1,\n protocol: 'mcp',\n plugins: officialMcpApps,\n updatedAt: nowIso(),\n });\n});\n\napp.all('/api/mcp/apps/:pluginId', async (req, res) => {\n try {\n const user = await resolveUser(req, req.body || {});\n await handleOfficialMcpRequest(req.params.pluginId, req, res, user.userId);\n } catch (error) {\n logger.error({ error: error.stack || String(error) }, 'official MCP app request failed');\n if (!res.headersSent) {\n res.status(500).json({ error: 'MCP request failed', message: publicAiErrorMessage(error) });\n }\n }\n});\n\napp.post(\n '/api/codex/apps/:pluginId/turns',\n asyncHandler(async (req, res) => {\n const pluginId = safeUserText(req.params.pluginId).replace(/^official\\./, '');\n const plugin = officialMcpApps.find((candidate) => candidate.id === pluginId);\n if (!plugin) {\n return jsonResponse(res, 404, { success: false, message: 'plugin not found' });\n }\n\n const message = safeUserText(req.body?.message);\n if (!message) {\n return jsonResponse(res, 400, { success: false, message: 'message is required' });\n }\n\n const user = await resolveUser(req, req.body || {});\n const promptEstimate = estimateTokens(message);\n const budget = enforceTokenBudget(user, promptEstimate + 600);\n const maxCompletionTokens = completionBudgetFor(budget, promptEstimate);\n const namespace = pluginConversationNamespace(pluginId);\n const requestedConversationId = safeUserText(req.body?.conversationId);\n if (requestedConversationId && !requestedConversationId.startsWith(`${namespace}:`)) {\n return jsonResponse(res, 400, {\n success: false,\n message: 'conversation does not belong to the selected plugin namespace',\n });\n }\n\n const conversationId = requestedConversationId || `${namespace}:${crypto.randomUUID()}`;\n const createdAt = nowIso();\n let conversation = statements.getPluginConversation.get(\n conversationId,\n user.userId,\n namespace,\n );\n if (requestedConversationId && !conversation) {\n return jsonResponse(res, 404, {\n success: false,\n message: 'plugin conversation not found for this account',\n });\n }\n const isNewConversation = !conversation;\n if (isNewConversation) {\n conversation = {\n id: conversationId,\n user_id: user.userId,\n title: titleFromMessage(message),\n namespace,\n thread_id: null,\n };\n }\n\n const accountKey = sha256(user.userId).slice(0, 24);\n const pluginCodexHome = path.join(codexPluginSessionsDir, accountKey, pluginId);\n fs.mkdirSync(pluginCodexHome, { recursive: true });\n const mcpAccountToken = createCodexAdapterToken(user, {\n audience: 'mcp-plugin',\n recordUsage: false,\n });\n const mcpOrigin = (\n process.env.MCP_INTERNAL_BASE_URL || `http://127.0.0.1:${process.env.PORT || 8788}`\n ).replace(/\\/+$/, '');\n const isolationPolicy = createPluginCodexPolicy(pluginId, mcpOrigin);\n const firstTurnMessages = [\n {\n role: 'system',\n content:\n `你正在 ${plugin.title} 插件的隔离 Codex 会话中。` +\n '只能使用 current_plugin MCP Server 暴露的 Tools;禁止 shell、文件系统、Web 搜索和其他插件。' +\n '只读 Tool 可以按需调用。需要写入、外部操作或破坏性操作时,不要自行执行,明确告诉用户改用对应的 /Tool 命令,让宿主显示审批。',\n },\n { role: 'user', content: message },\n ];\n const aiResult = await callCodexSdkDeepSeek(\n conversation?.thread_id ? [{ role: 'user', content: message }] : firstTurnMessages,\n {\n user,\n threadId: conversation?.thread_id || undefined,\n maxCompletionTokens,\n sandboxMode: isolationPolicy.sandboxMode,\n approvalPolicy: isolationPolicy.approvalPolicy,\n networkAccessEnabled: isolationPolicy.networkAccessEnabled,\n webSearchMode: isolationPolicy.webSearchMode,\n codexHomeDir: pluginCodexHome,\n env: { [pluginMcpTokenEnv]: mcpAccountToken },\n config: isolationPolicy.config,\n },\n );\n const usage = {\n promptTokens: aiResult.usage.promptTokens || promptEstimate,\n completionTokens: aiResult.usage.completionTokens || estimateTokens(aiResult.message),\n totalTokens:\n aiResult.usage.totalTokens || promptEstimate + estimateTokens(aiResult.message),\n };\n if (!aiResult.usageAlreadyRecorded) recordUsage(user.userId, usage.totalTokens);\n\n const answeredAt = nowIso();\n if (isNewConversation) {\n statements.insertPluginConversation.run({\n id: conversationId,\n userId: user.userId,\n title: conversation.title,\n provider: codexDeepSeekProviderId,\n model: deepseekModel,\n namespace,\n createdAt,\n updatedAt: answeredAt,\n });\n }\n statements.insertMessage.run({\n id: crypto.randomUUID(),\n conversationId,\n role: 'user',\n content: message,\n promptTokens: 0,\n completionTokens: 0,\n totalTokens: 0,\n createdAt,\n });\n statements.insertMessage.run({\n id: crypto.randomUUID(),\n conversationId,\n role: 'assistant',\n content: aiResult.message,\n promptTokens: usage.promptTokens,\n completionTokens: usage.completionTokens,\n totalTokens: usage.totalTokens,\n createdAt: answeredAt,\n });\n statements.updatePluginConversation.run({\n id: conversationId,\n userId: user.userId,\n namespace,\n title: conversation.title,\n threadId: aiResult.threadId || conversation?.thread_id || null,\n updatedAt: answeredAt,\n });\n\n jsonResponse(res, 200, {\n success: true,\n pluginId,\n conversationId,\n message: aiResult.message,\n provider: codexDeepSeekProviderId,\n model: deepseekModel,\n usage: {\n ...usage,\n monthlyLimit: budget.limit,\n remainingTokens: Math.max(0, budget.limit - usageFor(user.userId).used),\n },\n });\n }),\n);\n\napp.get('/api/ai/models', (_req, res) => {\n jsonResponse(res, 200, {\n success: true,\n provider: 'deepseek',\n defaultModel: deepseekModel,\n models: deepseekAvailableModels.map((model) => ({\n id: model,\n label: model === 'deepseek-reasoner' ? 'DeepSeek Reasoner' : 'DeepSeek Chat',\n })),\n });\n});\napp.get(\n '/api/ai/quota',\n asyncHandler(async (req, res) => {\n const user = await resolveUser(req, { username: req.query.username });\n const budget = enforceTokenBudget(user, 0);\n const usage = usageFor(user.userId);\n jsonResponse(res, 200, {\n success: true,\n isMember: user.isMember,\n monthlyLimit: budget.limit,\n usedTokens: usage.used,\n remainingTokens: Math.max(0, budget.limit - usage.used),\n month: usage.month,\n });\n }),\n);\n\napp.post(\n '/api/agent/chat',\n asyncHandler(async (req, res) => {\n const message = safeUserText(req.body?.message);\n if (!message) {\n return jsonResponse(res, 400, { success: false, message: 'message is required' });\n }\n\n const user = await resolveUser(req, req.body || {});\n const promptEstimate = estimateTokens(message);\n const estimated = promptEstimate + 600;\n const budget = enforceTokenBudget(user, estimated);\n const maxCompletionTokens = completionBudgetFor(budget, promptEstimate);\n const createdAt = nowIso();\n const conversationId = safeUserText(req.body?.conversationId) || crypto.randomUUID();\n const mode = normalizeAgentMode(req.body?.mode);\n const stream = req.body?.stream === true;\n let conversation = statements.getConversation.get(conversationId, user.userId);\n const isNewConversation = !conversation;\n\n if (isNewConversation) {\n conversation = { id: conversationId, user_id: user.userId, title: titleFromMessage(message) };\n statements.insertConversation.run({\n id: conversationId,\n userId: user.userId,\n title: conversation.title,\n provider: 'agent',\n model: openClawAgentId,\n createdAt,\n updatedAt: createdAt,\n });\n }\n\n const messageId = safeUserText(req.body?.messageId) || crypto.randomUUID();\n statements.insertMessage.run({\n id: messageId,\n conversationId,\n role: 'user',\n content: message,\n promptTokens: 0,\n completionTokens: 0,\n totalTokens: 0,\n createdAt,\n });\n\n const runId = `run_${crypto.randomUUID().replaceAll('-', '')}`;\n const sessionKey = openClawSessionKey(user, conversationId);\n const openClawRuntime = createOpenClawRuntime();\n statements.insertAgentRun.run({\n id: runId,\n userId: user.userId,\n conversationId,\n messageId,\n openClawRunId: null,\n openClawSessionKey: sessionKey,\n status: stream ? 'queued' : 'running',\n mode,\n provider: openClawRuntime.enabled ? openClawRuntime.provider : 'dacheng-ai-fallback',\n model: openClawRuntime.enabled ? openClawAgentId : deepseekModel,\n startedAt: createdAt,\n });\n\n if (stream) {\n return jsonResponse(res, 200, {\n success: true,\n runId,\n conversationId,\n streamUrl: `/api/agent/runs/${runId}/events`,\n });\n }\n\n const controller = new AbortController();\n activeAgentRuns.set(runId, controller);\n try {\n const skillResult = await runResourceFinderSkill({ message, user });\n const skillContext = resourceContextMessage(skillResult);\n const historyRows = statements.listMessages.all(conversationId).slice(-14);\n const modelMessages = [\n { role: 'system', content: systemPrompt },\n ...(skillContext ? [{ role: 'system', content: skillContext }] : []),\n ...normalizeMessages(historyRows),\n ];\n\n const aiResult = await runAgentModel({\n messages: modelMessages,\n user,\n conversationId,\n mode,\n signal: controller.signal,\n maxCompletionTokens,\n });\n const usage = {\n promptTokens: aiResult.usage?.promptTokens || estimateTokens(JSON.stringify(modelMessages)),\n completionTokens: aiResult.usage?.completionTokens || estimateTokens(aiResult.message),\n totalTokens:\n aiResult.usage?.totalTokens ||\n estimateTokens(JSON.stringify(modelMessages)) + estimateTokens(aiResult.message),\n };\n if (!aiResult.usageAlreadyRecorded) recordUsage(user.userId, usage.totalTokens);\n const answeredAt = nowIso();\n const assistantMessageId = crypto.randomUUID();\n statements.insertMessage.run({\n id: assistantMessageId,\n conversationId,\n role: 'assistant',\n content: aiResult.message,\n promptTokens: usage.promptTokens,\n completionTokens: usage.completionTokens,\n totalTokens: usage.totalTokens,\n createdAt: answeredAt,\n });\n statements.updateConversation.run({\n id: conversationId,\n userId: user.userId,\n title: isNewConversation ? titleFromMessage(message) : conversation.title,\n updatedAt: answeredAt,\n });\n updateAgentRunStatus({ id: runId, user_id: user.userId }, 'completed', {\n openClawRunId: aiResult.runId,\n inputTokens: usage.promptTokens,\n outputTokens: usage.completionTokens,\n });\n const action = determineClientAction(skillResult, message);\n return jsonResponse(res, 200, {\n success: true,\n runId,\n conversationId,\n messageId: assistantMessageId,\n provider: aiResult.provider,\n model: aiResult.model,\n message: aiResult.message,\n usage: agentUsagePayload(usage, user, budget),\n ...(action ? { clientAction: action } : {}),\n });\n } catch (error) {\n const status = error.name === 'AbortError' ? 'cancelled' : 'failed';\n updateAgentRunStatus({ id: runId, user_id: user.userId }, status, {\n errorCode: error.name || 'AGENT_RUN_FAILED',\n errorMessage: publicAiErrorMessage(error),\n });\n if (status === 'cancelled') {\n return jsonResponse(res, 499, { success: false, runId, status: 'cancelled' });\n }\n throw error;\n } finally {\n activeAgentRuns.delete(runId);\n }\n }),\n);\n\napp.get(\n '/api/agent/runs/:runId/events',\n asyncHandler(async (req, res) => {\n const user = await resolveUser(req, {});\n const run = statements.getAgentRun.get(req.params.runId, user.userId);\n if (!run) {\n return jsonResponse(res, 404, { success: false, message: 'run not found' });\n }\n if (run.status === 'completed' || run.status === 'failed' || run.status === 'cancelled') {\n return jsonResponse(res, 409, { success: false, message: `run is already ${run.status}` });\n }\n\n sseHeaders(res);\n const controller = new AbortController();\n activeAgentRuns.set(run.id, controller);\n updateAgentRunStatus(run, 'running');\n writeSse(res, 'run.started', { runId: run.id, conversationId: run.conversation_id });\n\n try {\n let streamedText = '';\n const modelMessages = buildAgentMessages(run.conversation_id);\n const lastUserMsg = [...modelMessages].reverse().find(m => m.role === 'user');\n const userMsgText = lastUserMsg ? lastUserMsg.content : '';\n const skillResult = userMsgText ? await runResourceFinderSkill({\n message: userMsgText,\n user,\n onStep: (step) => writeSse(res, 'tool.call.completed', {\n tool: 'agent.step',\n displayName: step.title || 'Agent step',\n summary: step.message || '',\n }),\n }) : null;\n const skillContext = resourceContextMessage(skillResult);\n const finalMessages = skillContext ? [\n modelMessages[0],\n { role: 'system', content: skillContext },\n ...modelMessages.slice(1)\n ] : modelMessages;\n\n const promptEstimate = estimateTokens(JSON.stringify(finalMessages));\n const estimated = promptEstimate + 600;\n const budget = enforceTokenBudget(user, estimated);\n const maxCompletionTokens = completionBudgetFor(budget, promptEstimate);\n const aiResult = await runAgentModel({\n messages: finalMessages,\n user,\n conversationId: run.conversation_id,\n mode: run.mode,\n signal: controller.signal,\n maxCompletionTokens,\n callbacks: {\n onStep: (step) => writeSse(res, 'tool.call.completed', {\n tool: 'agent.step',\n displayName: step.title || 'Agent step',\n summary: step.message || '',\n }),\n onToken: (delta) => {\n streamedText += delta;\n writeSse(res, 'assistant.delta', { text: delta });\n },\n },\n });\n const message = aiResult.message || streamedText.trim();\n const usage = {\n promptTokens: aiResult.usage?.promptTokens || estimateTokens(JSON.stringify(finalMessages)),\n completionTokens: aiResult.usage?.completionTokens || estimateTokens(message),\n totalTokens:\n aiResult.usage?.totalTokens || estimateTokens(JSON.stringify(finalMessages)) + estimateTokens(message),\n };\n if (!aiResult.usageAlreadyRecorded) recordUsage(user.userId, usage.totalTokens);\n const assistantMessageId = crypto.randomUUID();\n const answeredAt = nowIso();\n statements.insertMessage.run({\n id: assistantMessageId,\n conversationId: run.conversation_id,\n role: 'assistant',\n content: message,\n promptTokens: usage.promptTokens,\n completionTokens: usage.completionTokens,\n totalTokens: usage.totalTokens,\n createdAt: answeredAt,\n });\n statements.updateConversation.run({\n id: run.conversation_id,\n userId: user.userId,\n title: titleFromMessage(message),\n updatedAt: answeredAt,\n });\n updateAgentRunStatus(run, 'completed', {\n openClawRunId: aiResult.runId,\n inputTokens: usage.promptTokens,\n outputTokens: usage.completionTokens,\n });\n const action = determineClientAction(skillResult, userMsgText);\n writeSse(res, 'assistant.message', { messageId: assistantMessageId, content: message });\n writeSse(res, 'run.completed', {\n usage: agentUsagePayload(usage, user, budget),\n ...(action ? { clientAction: action } : {}),\n });\n res.end();\n } catch (error) {\n const status = error.name === 'AbortError' ? 'cancelled' : 'failed';\n updateAgentRunStatus(run, status, {\n errorCode: error.name || 'AGENT_RUN_FAILED',\n errorMessage: publicAiErrorMessage(error),\n });\n writeSse(res, status === 'cancelled' ? 'run.cancelled' : 'run.failed', {\n runId: run.id,\n message: publicAiErrorMessage(error),\n });\n res.end();\n } finally {\n activeAgentRuns.delete(run.id);\n }\n }),\n);\n\napp.post(\n '/api/agent/runs/:runId/cancel',\n asyncHandler(async (req, res) => {\n const user = await resolveUser(req, req.body || {});\n const run = statements.getAgentRun.get(req.params.runId, user.userId);\n if (!run) {\n return jsonResponse(res, 404, { success: false, message: 'run not found' });\n }\n const controller = activeAgentRuns.get(run.id);\n if (controller) controller.abort();\n updateAgentRunStatus(run, 'cancelled');\n return jsonResponse(res, 200, { success: true, runId: run.id, status: 'cancelled' });\n }),\n);\n\napp.post(\n '/api/agent/messages/:messageId/feedback',\n asyncHandler(async (req, res) => {\n const user = await resolveUser(req, req.body || {});\n const rating = safeUserText(req.body?.rating);\n if (!['up', 'down'].includes(rating)) {\n return jsonResponse(res, 400, { success: false, message: 'rating must be up or down' });\n }\n statements.insertAgentFeedback.run({\n id: crypto.randomUUID(),\n messageId: req.params.messageId,\n userId: user.userId,\n rating,\n reason: safeUserText(req.body?.reason),\n comment: safeUserText(req.body?.comment),\n createdAt: nowIso(),\n });\n return jsonResponse(res, 200, { success: true });\n }),\n);\n\napp.post(\n '/api/ai/chat',\n asyncHandler(async (req, res) => {\n const message = safeUserText(req.body?.message);\n if (!message) {\n return jsonResponse(res, 400, { success: false, message: 'message is required' });\n }\n\n const user = await resolveUser(req, req.body || {});\n const promptEstimate = estimateTokens(message);\n const estimated = promptEstimate + 600;\n const budget = enforceTokenBudget(user, estimated);\n const maxCompletionTokens = completionBudgetFor(budget, promptEstimate);\n const createdAt = nowIso();\n const conversationId = safeUserText(req.body?.conversationId) || crypto.randomUUID();\n const requestedModel = normalizeDeepSeekModelName(req.body?.model);\n const clientContext = clientContextMessage(req.body?.client);\n let conversation = statements.getConversation.get(conversationId, user.userId);\n const isNewConversation = !conversation;\n\n if (isNewConversation) {\n conversation = {\n id: conversationId,\n user_id: user.userId,\n title: titleFromMessage(message),\n };\n statements.insertConversation.run({\n id: conversationId,\n userId: user.userId,\n title: conversation.title,\n provider: 'deepseek',\n model: requestedModel,\n createdAt,\n updatedAt: createdAt,\n });\n }\n\n statements.insertMessage.run({\n id: crypto.randomUUID(),\n conversationId,\n role: 'user',\n content: message,\n promptTokens: 0,\n completionTokens: 0,\n totalTokens: 0,\n createdAt,\n });\n\n const libreChatRuntime = createLibreChatAgentRuntime();\n const skillResult = await runResourceFinderSkill({ message, user });\n const skillContext = resourceContextMessage(skillResult);\n const historyRows = statements.listMessages.all(conversationId).slice(-14);\n const modelMessages = [\n { role: 'system', content: systemPrompt },\n ...(clientContext ? [{ role: 'system', content: clientContext }] : []),\n ...(skillContext ? [{ role: 'system', content: skillContext }] : []),\n ...normalizeMessages(historyRows),\n ];\n\n const openClawRuntime = createOpenClawRuntime();\n const codexRuntime = createCodexDeepSeekRuntime(user);\n let provider = openClawRuntime.enabled\n ? openClawRuntime.provider\n : libreChatRuntime.enabled\n ? libreChatRuntime.provider\n : 'deepseek';\n let responseModel = openClawRuntime.enabled\n ? openClawRuntime.model\n : libreChatRuntime.enabled\n ? libreChatRuntime.model\n : requestedModel;\n let aiResult;\n try {\n if (openClawRuntime.enabled) {\n aiResult = await callOpenClawAgent({\n messages: modelMessages,\n user,\n conversationId,\n mode: normalizeAgentMode(req.body?.mode),\n });\n provider = aiResult.provider || provider;\n responseModel = aiResult.model || responseModel;\n } else if (libreChatRuntime.enabled) {\n aiResult = await callLibreChatAgent(modelMessages);\n } else if (codexRuntime.enabled) {\n provider = codexRuntime.provider;\n aiResult = await callCodexSdkDeepSeek(modelMessages, { user, maxCompletionTokens });\n } else {\n aiResult = await callDeepSeek(modelMessages, { model: requestedModel, maxCompletionTokens });\n }\n } catch (error) {\n if (libreChatRuntime.enabled) {\n logger.warn({ error: String(error) }, 'LibreChat Agent chat failed, falling back to Codex SDK or DeepSeek direct');\n provider = codexRuntime.enabled ? codexRuntime.provider : 'deepseek';\n responseModel = requestedModel;\n if (codexRuntime.enabled) {\n try {\n aiResult = await callCodexSdkDeepSeek(modelMessages, { user, maxCompletionTokens });\n } catch (codexError) {\n if (isDeepSeekQuotaError(codexError)) throw codexError;\n logger.warn({ error: String(codexError) }, 'Codex SDK chat failed, falling back to DeepSeek direct');\n provider = 'deepseek';\n aiResult = await callDeepSeek(modelMessages, { model: requestedModel, maxCompletionTokens });\n }\n } else {\n aiResult = await callDeepSeek(modelMessages, { model: requestedModel, maxCompletionTokens });\n }\n } else {\n if (!codexRuntime.enabled || isDeepSeekQuotaError(error)) throw error;\n logger.warn({ error: String(error) }, 'Codex SDK chat failed, falling back to DeepSeek direct');\n provider = 'deepseek';\n responseModel = requestedModel;\n aiResult = await callDeepSeek(modelMessages, { model: requestedModel, maxCompletionTokens });\n }\n }\n const usage = {\n promptTokens: aiResult.usage.promptTokens || estimateTokens(JSON.stringify(modelMessages)),\n completionTokens: aiResult.usage.completionTokens || estimateTokens(aiResult.message),\n totalTokens:\n aiResult.usage.totalTokens ||\n estimateTokens(JSON.stringify(modelMessages)) + estimateTokens(aiResult.message),\n };\n if (!aiResult.usageAlreadyRecorded) recordUsage(user.userId, usage.totalTokens);\n\n const answeredAt = nowIso();\n statements.insertMessage.run({\n id: crypto.randomUUID(),\n conversationId,\n role: 'assistant',\n content: aiResult.message,\n promptTokens: usage.promptTokens,\n completionTokens: usage.completionTokens,\n totalTokens: usage.totalTokens,\n createdAt: answeredAt,\n });\n statements.updateConversation.run({\n id: conversationId,\n userId: user.userId,\n title: isNewConversation ? titleFromMessage(message) : conversation.title,\n updatedAt: answeredAt,\n });\n\n const latestUsage = usageFor(user.userId);\n const monthlyLimit = budget.limit;\n const action = determineClientAction(skillResult, message);\n jsonResponse(res, 200, {\n success: true,\n conversationId,\n provider,\n model: responseModel,\n message: aiResult.message,\n usage: {\n ...usage,\n monthlyLimit,\n remainingTokens: Math.max(0, monthlyLimit - latestUsage.used),\n },\n ...(action ? { clientAction: action } : {}),\n });\n }),\n);\n\napp.post('/api/ai/chat/stream', async (req, res) => {\n sseHeaders(res);\n\n try {\n const message = safeUserText(req.body?.message);\n if (!message) {\n writeSse(res, 'error', { message: 'message is required' });\n res.end();\n return;\n }\n\n const user = await resolveUser(req, req.body || {});\n const promptEstimate = estimateTokens(message);\n const estimated = promptEstimate + 600;\n const budget = enforceTokenBudget(user, estimated);\n const maxCompletionTokens = completionBudgetFor(budget, promptEstimate);\n const createdAt = nowIso();\n const conversationId = safeUserText(req.body?.conversationId) || crypto.randomUUID();\n const requestedModel = normalizeDeepSeekModelName(req.body?.model);\n const clientContext = clientContextMessage(req.body?.client);\n let conversation = statements.getConversation.get(conversationId, user.userId);\n const isNewConversation = !conversation;\n\n if (isNewConversation) {\n conversation = {\n id: conversationId,\n user_id: user.userId,\n title: titleFromMessage(message),\n };\n statements.insertConversation.run({\n id: conversationId,\n userId: user.userId,\n title: conversation.title,\n provider: 'deepseek',\n model: requestedModel,\n createdAt,\n updatedAt: createdAt,\n });\n }\n\n statements.insertMessage.run({\n id: crypto.randomUUID(),\n conversationId,\n role: 'user',\n content: message,\n promptTokens: 0,\n completionTokens: 0,\n totalTokens: 0,\n createdAt,\n });\n\n const codexRuntime = createCodexDeepSeekRuntime(user);\n const libreChatRuntime = createLibreChatAgentRuntime();\n const initialOpenClawRuntime = createOpenClawRuntime();\n let provider = initialOpenClawRuntime.enabled\n ? initialOpenClawRuntime.provider\n : libreChatRuntime.enabled\n ? libreChatRuntime.provider\n : codexRuntime.enabled\n ? codexRuntime.provider\n : 'deepseek';\n let responseModel = initialOpenClawRuntime.enabled\n ? initialOpenClawRuntime.model\n : libreChatRuntime.enabled\n ? libreChatRuntime.model\n : requestedModel;\n writeSse(res, 'meta', {\n conversationId,\n provider,\n model: responseModel,\n });\n const skillResult = await runResourceFinderSkill({\n message,\n user,\n onStep: (step) => writeSse(res, 'step', step),\n });\n const skillContext = resourceContextMessage(skillResult);\n const historyRows = statements.listMessages.all(conversationId).slice(-14);\n const modelMessages = [\n { role: 'system', content: systemPrompt },\n ...(clientContext ? [{ role: 'system', content: clientContext }] : []),\n ...(skillContext ? [{ role: 'system', content: skillContext }] : []),\n ...normalizeMessages(historyRows),\n ];\n\n let aiResult;\n const openClawRuntime = initialOpenClawRuntime;\n try {\n if (openClawRuntime.enabled) {\n aiResult = await callOpenClawAgent({\n messages: modelMessages,\n user,\n conversationId,\n mode: normalizeAgentMode(req.body?.mode),\n });\n provider = aiResult.provider || provider;\n responseModel = aiResult.model || responseModel;\n writeSse(res, 'delta', { text: aiResult.message });\n } else if (libreChatRuntime.enabled) {\n aiResult = await callLibreChatAgentStream(modelMessages, {\n onToken: (token) => writeSse(res, 'delta', { text: token }),\n });\n } else if (codexRuntime.enabled) {\n aiResult = await callCodexSdkDeepSeek(modelMessages, {\n user,\n maxCompletionTokens,\n onToken: (token) => writeSse(res, 'delta', { text: token }),\n onStep: (step) => writeSse(res, 'step', step),\n });\n } else {\n aiResult = await callDeepSeekStream(modelMessages, {\n model: requestedModel,\n maxCompletionTokens,\n onToken: (token) => writeSse(res, 'delta', { text: token }),\n });\n }\n } catch (error) {\n if (libreChatRuntime.enabled) {\n logger.warn({ error: String(error) }, 'LibreChat Agent streaming chat failed, falling back to Codex SDK or DeepSeek direct');\n provider = codexRuntime.enabled ? codexRuntime.provider : 'deepseek';\n responseModel = requestedModel;\n writeSse(res, 'meta', {\n conversationId,\n provider,\n model: responseModel,\n });\n if (codexRuntime.enabled) {\n try {\n aiResult = await callCodexSdkDeepSeek(modelMessages, {\n user,\n maxCompletionTokens,\n onToken: (token) => writeSse(res, 'delta', { text: token }),\n onStep: (step) => writeSse(res, 'step', step),\n });\n } catch (codexError) {\n if (isDeepSeekQuotaError(codexError)) throw codexError;\n logger.warn({ error: String(codexError) }, 'Codex SDK streaming chat failed, falling back to DeepSeek direct');\n provider = 'deepseek';\n responseModel = requestedModel;\n writeSse(res, 'meta', {\n conversationId,\n provider,\n model: responseModel,\n });\n aiResult = await callDeepSeekStream(modelMessages, {\n model: requestedModel,\n maxCompletionTokens,\n onToken: (token) => writeSse(res, 'delta', { text: token }),\n });\n }\n } else {\n aiResult = await callDeepSeekStream(modelMessages, {\n model: requestedModel,\n maxCompletionTokens,\n onToken: (token) => writeSse(res, 'delta', { text: token }),\n });\n }\n } else {\n if (!codexRuntime.enabled || isDeepSeekQuotaError(error)) throw error;\n logger.warn({ error: String(error) }, 'Codex SDK streaming chat failed, falling back to DeepSeek direct');\n provider = 'deepseek';\n responseModel = requestedModel;\n writeSse(res, 'meta', {\n conversationId,\n provider,\n model: responseModel,\n });\n aiResult = await callDeepSeekStream(modelMessages, {\n model: requestedModel,\n maxCompletionTokens,\n onToken: (token) => writeSse(res, 'delta', { text: token }),\n });\n }\n }\n\n const usage = {\n promptTokens: aiResult.usage.promptTokens || estimateTokens(JSON.stringify(modelMessages)),\n completionTokens: aiResult.usage.completionTokens || estimateTokens(aiResult.message),\n totalTokens:\n aiResult.usage.totalTokens ||\n estimateTokens(JSON.stringify(modelMessages)) + estimateTokens(aiResult.message),\n };\n if (!aiResult.usageAlreadyRecorded) recordUsage(user.userId, usage.totalTokens);\n\n const answeredAt = nowIso();\n statements.insertMessage.run({\n id: crypto.randomUUID(),\n conversationId,\n role: 'assistant',\n content: aiResult.message,\n promptTokens: usage.promptTokens,\n completionTokens: usage.completionTokens,\n totalTokens: usage.totalTokens,\n createdAt: answeredAt,\n });\n statements.updateConversation.run({\n id: conversationId,\n userId: user.userId,\n title: isNewConversation ? titleFromMessage(message) : conversation.title,\n updatedAt: answeredAt,\n });\n\n const latestUsage = usageFor(user.userId);\n const monthlyLimit = budget.limit;\n const action = determineClientAction(skillResult, message);\n writeSse(res, 'done', {\n conversationId,\n provider,\n model: responseModel,\n message: aiResult.message,\n usage: {\n ...usage,\n monthlyLimit,\n remainingTokens: Math.max(0, monthlyLimit - latestUsage.used),\n },\n ...(action ? { clientAction: action } : {}),\n });\n res.end();\n } catch (error) {\n logger.error({ error: error.stack || String(error) }, 'streaming chat failed');\n writeSse(res, 'error', {\n message: publicAiErrorMessage(error),\n details: error.details,\n });\n res.end();\n }\n});\n\napp.get(\n '/api/ai/conversations',\n asyncHandler(async (req, res) => {\n const user = await resolveUser(req, { username: req.query.username });\n const items = statements.listConversations.all(user.userId).map((item) => ({\n id: item.id,\n title: item.title,\n updatedAt: item.updatedAt,\n }));\n jsonResponse(res, 200, { success: true, items });\n }),\n);\n\napp.get(\n '/api/ai/conversations/:id',\n asyncHandler(async (req, res) => {\n const user = await resolveUser(req);\n const conversation = statements.getConversation.get(req.params.id, user.userId);\n if (!conversation) {\n return jsonResponse(res, 404, { success: false, message: 'conversation not found' });\n }\n const messages = statements.listMessages.all(req.params.id).map((item) => ({\n role: item.role,\n content: item.content,\n createdAt: item.createdAt,\n }));\n jsonResponse(res, 200, {\n success: true,\n conversation: {\n id: conversation.id,\n title: conversation.title,\n updatedAt: conversation.updated_at,\n },\n messages,\n });\n }),\n);\n\nfunction buildUrl(base, endpoint, params = {}) {\n const url = new URL(endpoint, `${base}/`);\n for (const [key, value] of Object.entries(params)) {\n if (Array.isArray(value)) {\n for (const item of value) {\n if (item !== undefined && item !== null && item !== '') {\n url.searchParams.append(key, String(item));\n }\n }\n } else if (value !== undefined && value !== null && value !== '') {\n url.searchParams.set(key, String(value));\n }\n }\n return url;\n}\n\nasync function fetchJson(url) {\n const response = await fetch(url, {\n headers: { Accept: 'application/json' },\n signal: AbortSignal.timeout(15_000),\n });\n if (!response.ok) {\n throw new Error(`Request failed: ${response.status}`);\n }\n return response.json();\n}\n\nfunction stripHtml(value) {\n return String(value || '')\n .replace(/<script[\\s\\S]*?<\\/script>/gi, ' ')\n .replace(/<style[\\s\\S]*?<\\/style>/gi, ' ')\n .replace(/<[^>]+>/g, ' ')\n .replace(/ /g, ' ')\n .replace(/</g, '<')\n .replace(/>/g, '>')\n .replace(/&/g, '&')\n .replace(/&#(\\d+);/g, (_match, code) => String.fromCharCode(Number(code)))\n .replace(/\\s+/g, ' ')\n .trim();\n}\n\nfunction cleanExtractedText(value) {\n let text = String(value || '')\n .replace(/\\b[A-Z]{1,3}\\d{2}n\\d{4,5}[a-z]?_p?\\d{4}[a-z]\\d{2}\\b/g, ' ')\n .replace(/\\uFFFD/g, '')\n .replace(/\\s+([,。;:!?、」』)》】])/g, '$1')\n .replace(/([「『《(【])\\s+/g, '$1');\n for (let i = 0; i < 3; i += 1) {\n text = text.replace(/([\\u3400-\\u9fff])\\s+([\\u3400-\\u9fff])/g, '$1$2');\n }\n return text.replace(/[ \\t]{2,}/g, ' ').trim();\n}\n\nfunction compactText(value, limit = 140) {\n const text = stripHtml(value);\n return text.length > limit ? `${text.slice(0, limit)}...` : text;\n}\n\nfunction collectSearchResults(data) {\n if (!data || typeof data !== 'object') return [];\n if (Array.isArray(data.results)) return data.results;\n if (Array.isArray(data?.results?.docs)) return data.results.docs;\n if (Array.isArray(data?.response?.docs)) return data.response.docs;\n if (Array.isArray(data.docs)) return data.docs;\n return [];\n}\n\nfunction firstText(item, keys) {\n for (const key of keys) {\n const value = item?.[key];\n if (Array.isArray(value)) {\n const first = value.map((entry) => safeUserText(String(entry))).find(Boolean);\n if (first) return first;\n }\n if (typeof value === 'string' && value.trim()) return value.trim();\n if (typeof value === 'number') return String(value);\n }\n return '';\n}\n\nfunction firstNumber(item, keys, fallback = undefined) {\n for (const key of keys) {\n const value = item?.[key];\n if (typeof value === 'number') return value;\n if (typeof value === 'string' && value.trim() && Number.isFinite(Number(value))) {\n return Number(value);\n }\n }\n return fallback;\n}\n\nasync function searchCbetaTitle(query, limit) {\n const url = buildUrl(cbetaApiRoot, 'search/title', { q: query, start: 0, rows: limit });\n const data = await fetchJson(url);\n return collectSearchResults(data).flatMap((item) => {\n const work = firstText(item, ['work', 'work_id', 'workId']);\n const title = firstText(item, ['content', 'title', 'work_title', 'workTitle']);\n if (!work || !title) return [];\n const juan = firstNumber(item, ['juan', 'juan_num', 'juanNum'], 1);\n return [\n {\n id: `cbeta-title-${work}-${juan}`,\n title,\n sourceName: 'CBETA 佛典',\n url: `cbeta:${work}:${juan}`,\n snippet: firstText(item, ['byline', 'creators', 'time_dynasty']) || '可下载经文正文并用于全球法布施。',\n resourceType: 'scripture',\n work,\n juan,\n },\n ];\n });\n}\n\nasync function searchCbetaContent(query, limit) {\n const endpoints = ['search/fulltext', 'search/content', 'search/all_in_one'];\n const settled = await Promise.allSettled(\n endpoints.map((endpoint) =>\n fetchJson(buildUrl(cbetaApiRoot, endpoint, { q: query, start: 0, rows: limit, field: 'content' })),\n ),\n );\n const results = [];\n for (const response of settled) {\n if (response.status !== 'fulfilled') continue;\n for (const item of collectSearchResults(response.value)) {\n const work = firstText(item, ['work', 'work_id', 'workId', 'sutra', 'id']);\n const snippet = compactText(firstText(item, ['kwic', 'highlight', 'snippet', 'content', 'text', 'body', 'p']));\n if (!work || !snippet) continue;\n const juan = firstNumber(item, ['juan', 'juan_num', 'juanNum', '卷'], 1);\n const title = firstText(item, ['title', 'work_title', 'workTitle', 'sutra_name', 'book', 'name']) || work;\n results.push({\n id: `cbeta-content-${work}-${juan}-${sha256(snippet).slice(0, 8)}`,\n title,\n sourceName: 'CBETA 佛典',\n url: `cbeta:${work}:${juan}`,\n snippet,\n resourceType: 'scripture',\n work,\n juan,\n });\n if (results.length >= limit) return results;\n }\n }\n return results;\n}\n\nfunction directUrlResource(query) {\n const url = safeUserText(query);\n let parsed;\n try {\n parsed = new URL(url);\n } catch {\n return null;\n }\n if (!['http:', 'https:'].includes(parsed.protocol)) return null;\n return {\n id: `url-${sha256(url).slice(0, 12)}`,\n title: parsed.hostname,\n sourceName: '网页资源',\n url,\n snippet: '直接下载并提取这个链接的可读正文。',\n resourceType: 'web',\n };\n}\n\nfunction looksLikeAudioQuery(query) {\n return /音乐|音樂|音频|音訊|mp3|audio|念诵|念誦|诵经|誦經|唱诵|唱誦|读诵|讀誦/.test(\n safeUserText(query).toLowerCase(),\n );\n}\n\nfunction archiveSearchTerm(query) {\n return (\n safeUserText(query)\n .replace(/下载|查找|寻找|找一?个|资源|音频|音訊|音乐|音樂|mp3|audio|念诵|念誦|诵经|誦經|唱诵|唱誦|读诵|讀誦/gi, ' ')\n .replace(/\\s+/g, ' ')\n .trim() || safeUserText(query)\n );\n}\n\nfunction solrPhrase(value) {\n return String(value).replace(/\\\\/g, '\\\\\\\\').replace(/\"/g, '\\\\\"');\n}\n\nasync function searchInternetArchiveAudio(query, limit) {\n if (!looksLikeAudioQuery(query)) return [];\n const term = archiveSearchTerm(query);\n if (!term) return [];\n const variants = Array.from(\n new Set([\n term,\n term.replaceAll('金刚经', '金剛經'),\n term.replaceAll('金剛經', '金刚经'),\n ].filter(Boolean)),\n );\n const phraseQuery = variants\n .flatMap((item) => [`title:\"${solrPhrase(item)}\"`, `description:\"${solrPhrase(item)}\"`])\n .join(' OR ');\n const url = buildUrl('https://archive.org', 'advancedsearch.php', {\n q: `(${phraseQuery}) AND mediatype:audio`,\n rows: Math.min(Math.max(limit, 1), 10),\n output: 'json',\n 'fl[]': ['identifier', 'title', 'description'],\n });\n const data = await fetchJson(url);\n const docs = Array.isArray(data?.response?.docs) ? data.response.docs : [];\n return docs\n .map((item) => {\n const identifier = firstText(item, ['identifier']);\n const title = firstText(item, ['title']) || identifier;\n if (!identifier || !title) return null;\n return {\n id: `ia-audio-${identifier}`,\n title,\n sourceName: 'Internet Archive 音频',\n url: `ia:${identifier}`,\n snippet: compactText(firstText(item, ['description'])) || '公开可访问的音频资源,将下载第一个可用 MP3 文件。',\n resourceType: 'audio',\n identifier,\n };\n })\n .filter(Boolean);\n}\n\nconst curatedCbetaWorks = [\n {\n work: 'T0235',\n juan: 1,\n title: '金剛般若波羅蜜經',\n keywords: ['金剛般若波羅蜜經', '金刚般若波罗蜜经', '金剛經', '金刚经'],\n snippet: '姚秦 鳩摩羅什譯。常用于读诵、共修与法布施的般若经典。',\n },\n {\n work: 'T0251',\n juan: 1,\n title: '般若波羅蜜多心經',\n keywords: ['般若波羅蜜多心經', '般若波罗蜜多心经', '心經', '心经'],\n snippet: '唐 玄奘譯。篇幅短,适合加入禅室功课本与日常持诵。',\n },\n {\n work: 'T0262',\n juan: 1,\n title: '妙法蓮華經',\n keywords: ['妙法蓮華經', '妙法莲华经', '法華經', '法华经'],\n snippet: '姚秦 鳩摩羅什譯。可按卷下载正文,用于长期功课与分享。',\n },\n {\n work: 'T0279',\n juan: 1,\n title: '大方廣佛華嚴經',\n keywords: ['大方廣佛華嚴經', '大方广佛华严经', '華嚴經', '华严经'],\n snippet: '唐 實叉難陀譯。可按卷下载正文,用于系统研读与法布施。',\n },\n {\n work: 'T0360',\n juan: 1,\n title: '佛說無量壽經',\n keywords: ['佛說無量壽經', '佛说无量寿经', '無量壽經', '无量寿经'],\n snippet: '净土经典。可下载正文,用于净土功课本与全球法布施。',\n },\n {\n work: 'T0365',\n juan: 1,\n title: '佛說觀無量壽佛經',\n keywords: ['佛說觀無量壽佛經', '佛说观无量寿佛经', '觀無量壽經', '观无量寿经'],\n snippet: '净土三经之一。适合查找下载后加入净土功课。',\n },\n {\n work: 'T0366',\n juan: 1,\n title: '佛說阿彌陀經',\n keywords: ['佛說阿彌陀經', '佛说阿弥陀经', '阿彌陀經', '阿弥陀经'],\n snippet: '姚秦 鳩摩羅什譯。篇幅适中,适合共修、持诵与分享。',\n },\n {\n work: 'T0475',\n juan: 1,\n title: '維摩詰所說經',\n keywords: ['維摩詰所說經', '维摩诘所说经', '維摩詰經', '维摩诘经'],\n snippet: '姚秦 鳩摩羅什譯。大乘经典,可按卷下载用于研读。',\n },\n {\n work: 'T0676',\n juan: 1,\n title: '解深密經',\n keywords: ['解深密經', '解深密经'],\n snippet: '唯识相关大乘经典。适合研读、整理笔记与法布施。',\n },\n {\n work: 'T1666',\n juan: 1,\n title: '大乘起信論',\n keywords: ['大乘起信論', '大乘起信论', '起信論', '起信论'],\n snippet: '大乘论典。适合下载后作为研习功课资料。',\n },\n];\n\nfunction localCbetaResults(query, limit) {\n const normalizedQuery = safeUserText(query).toLowerCase();\n if (!normalizedQuery) return [];\n return curatedCbetaWorks\n .filter((item) =>\n item.keywords.some((keyword) => {\n const normalizedKeyword = keyword.toLowerCase();\n return normalizedKeyword.includes(normalizedQuery) || normalizedQuery.includes(normalizedKeyword);\n }),\n )\n .slice(0, limit)\n .map((item) => ({\n id: `cbeta-curated-${item.work}-${item.juan}`,\n title: item.title,\n sourceName: 'CBETA 佛典',\n url: `cbeta:${item.work}:${item.juan}`,\n snippet: item.snippet,\n resourceType: 'scripture',\n work: item.work,\n juan: item.juan,\n }));\n}\n\nfunction uniqueResources(items, limit) {\n const seen = new Set();\n const results = [];\n for (const item of items) {\n const key = `${item.url}:${item.title}`;\n if (seen.has(key)) continue;\n seen.add(key);\n results.push(item);\n if (results.length >= limit) break;\n }\n return results;\n}\n\nasync function findResourceCandidates(query, limit) {\n const direct = directUrlResource(query);\n const results = [];\n if (direct) results.push(direct);\n try {\n results.push(...(await searchInternetArchiveAudio(query, limit)));\n } catch (error) {\n logger.warn({ error: String(error), query }, 'Internet Archive audio search skipped');\n }\n results.push(...localCbetaResults(query, limit));\n\n const [titleResults, contentResults] = await Promise.allSettled([\n searchCbetaTitle(query, limit),\n searchCbetaContent(query, limit),\n ]);\n if (titleResults.status === 'fulfilled') results.push(...titleResults.value);\n if (contentResults.status === 'fulfilled') results.push(...contentResults.value);\n\n return uniqueResources(results, limit);\n}\n\nfunction looksLikeResourceTask(message) {\n const text = safeUserText(message).toLowerCase();\n if (!text) return false;\n return /下载|查找|寻找|找一?个|资源|經文|经文|佛经|佛經|仪轨|儀軌|音乐|音樂|mp3|audio|念诵|念誦|诵经|誦經/.test(text);\n}\n\nfunction extractResourceQuery(message) {\n const text = safeUserText(message)\n .replace(/请|請|帮我|幫我|麻烦|麻煩|可以|能不能|需要|我要|想要/g, ' ')\n .replace(/下载|下載|查找|寻找|尋找|找一个|找一個|找|资源|資源|并|並|然后|然後|用于|用於|全球法布施|加入功课本|加入功課本/g, ' ')\n .replace(/\\s+/g, ' ')\n .trim();\n return text || safeUserText(message);\n}\n\nfunction resourceContextMessage(skillResult) {\n if (!skillResult) return '';\n const lines = [\n '本轮资源检索/下载已经由大乘 App 后端完成。',\n '不要再次调用 search_dharma_resources、download_dharma_resource 或其他资源 MCP 工具;请直接基于以下结果回答用户。',\n `后端已调用 skill: ${skillResult.skillName}`,\n `执行目标: ${skillResult.query}`,\n ];\n if (skillResult.selected) {\n lines.push(`候选资源: ${skillResult.selected.title}`);\n lines.push(`来源: ${skillResult.selected.sourceName}`);\n lines.push(`资源地址: ${skillResult.selected.url}`);\n }\n if (skillResult.downloaded) {\n lines.push(`下载标题: ${skillResult.downloaded.title}`);\n lines.push(`下载文件: ${skillResult.downloaded.fileName}`);\n const text = safeUserText(skillResult.downloaded.contentText);\n if (text) {\n lines.push('下载正文摘录:');\n lines.push(text.slice(0, 8000));\n }\n }\n return lines.join('\\n');\n}\n\nfunction determineClientAction(skillResult, message) {\n if (skillResult && skillResult.downloaded) {\n const text = safeUserText(message);\n if (/法布施|全球法布施|分享/.test(text)) {\n return {\n type: 'prepare_dharma_share_text',\n title: skillResult.downloaded.title,\n text: skillResult.downloaded.contentText || '',\n };\n }\n }\n return null;\n}\n\nasync function runResourceFinderSkill({ message, user, onStep }) {\n if (!looksLikeResourceTask(message)) return null;\n\n const skillName = 'resource-finder-downloader';\n const query = extractResourceQuery(message);\n const audioTask = looksLikeAudioQuery(message) || looksLikeAudioQuery(query);\n onStep?.({\n type: 'skill.step',\n skillName,\n stage: 'start',\n title: '调用资源查找下载 skill',\n message: `识别到资源任务: ${query}`,\n });\n\n onStep?.({\n type: 'skill.step',\n skillName,\n stage: 'search',\n title: '搜索可分享资源',\n message: audioTask\n ? '优先检索公开可访问音频资源,失败时再回落到佛典正文资源。'\n : '优先检索 CBETA 佛典正文接口,搜索索引不可用时使用内置经典映射。',\n });\n const candidates = await findResourceCandidates(query, 8);\n if (candidates.length === 0) {\n onStep?.({\n type: 'skill.step',\n skillName,\n stage: 'empty',\n title: '没有找到可下载资源',\n message: '后端没有找到明确可下载且可分享的资源。',\n });\n return {\n skillName,\n query,\n candidates: [],\n selected: null,\n downloaded: null,\n };\n }\n\n const selected = candidates[0];\n onStep?.({\n type: 'skill.step',\n skillName,\n stage: 'select',\n title: '选择可信候选资源',\n message: `${selected.title} / ${selected.sourceName}`,\n });\n\n onStep?.({\n type: 'skill.step',\n skillName,\n stage: 'download',\n title: '下载并提取资源',\n message: selected.url,\n });\n const cbeta = parseCbetaResource(selected);\n const internetArchive = parseInternetArchiveResource(selected);\n const downloaded = cbeta\n ? await downloadCbetaResource({ ...selected, ...cbeta })\n : internetArchive\n ? await downloadInternetArchiveAudio({ ...selected, ...internetArchive })\n : await downloadWebResource(selected);\n const persisted = persistDownloadedResource(user, downloaded);\n\n onStep?.({\n type: 'skill.step',\n skillName,\n stage: 'done',\n title: '资源已准备完成',\n message: downloaded.fileName,\n });\n\n return {\n skillName,\n query,\n candidates,\n selected,\n downloaded: {\n ...downloaded,\n downloadId: persisted.id,\n },\n };\n}\n\napp.post(\n '/api/resources/search',\n asyncHandler(async (req, res) => {\n const query = safeUserText(req.body?.query);\n const limit = Math.min(Math.max(Number(req.body?.limit || 12), 1), 20);\n if (query.length < 2) {\n return jsonResponse(res, 400, { success: false, message: 'query is required' });\n }\n\n const items = await findResourceCandidates(query, limit);\n\n jsonResponse(res, 200, {\n success: true,\n source: 'resource-index',\n items,\n });\n }),\n);\n\nfunction parseCbetaResource(input) {\n const work = safeUserText(input.work);\n const juan = Number(input.juan || 1);\n if (work) return { work, juan: Number.isFinite(juan) ? juan : 1 };\n\n const url = safeUserText(input.url);\n const match = /^cbeta:([^:]+):(\\d+)$/i.exec(url);\n if (!match) return null;\n return {\n work: match[1].toUpperCase(),\n juan: Number(match[2] || 1),\n };\n}\n\nfunction parseInternetArchiveResource(input) {\n const url = safeUserText(input.url);\n const match = /^ia:([^:]+)$/i.exec(url);\n const identifier = match?.[1] || safeUserText(input.identifier);\n if (!identifier) return null;\n return { identifier };\n}\n\nfunction safeFileName(value) {\n const normalized = String(value || 'dharma-resource')\n .replace(/[\\\\/:*?\"<>|]+/g, '-')\n .replace(/\\s+/g, '-')\n .replace(/-+/g, '-')\n .replace(/^-|-$/g, '')\n .slice(0, 80);\n return normalized || 'dharma-resource';\n}\n\nfunction extractCbetaHtml(data) {\n const first = Array.isArray(data?.results) ? data.results[0] : null;\n if (typeof first === 'string') return first;\n if (first && typeof first === 'object' && typeof first.html === 'string') return first.html;\n if (typeof data?.html === 'string') return data.html;\n return '';\n}\n\nasync function downloadCbetaResource(resource) {\n const url = buildUrl(cbetaApiRoot, 'juans', {\n work: resource.work,\n juan: resource.juan,\n work_info: 1,\n toc: 1,\n });\n const data = await fetchJson(url);\n const workInfo = data.work_info || {};\n const title = safeUserText(workInfo.title) || safeUserText(resource.title) || resource.work;\n const html = extractCbetaHtml(data);\n const text = cleanExtractedText(stripHtml(html));\n if (!text) throw new Error('CBETA resource returned empty content');\n return {\n title,\n sourceName: 'CBETA 佛典',\n url: `cbeta:${resource.work}:${resource.juan}`,\n contentText: text.slice(0, maxResourceTextChars),\n fileName: `${resource.work}_${resource.juan}_${safeFileName(title)}.txt`,\n };\n}\n\nasync function downloadInternetArchiveAudio(resource) {\n const identifier = safeUserText(resource.identifier);\n if (!identifier) throw new Error('Internet Archive identifier is required');\n const metadata = await fetchJson(`https://archive.org/metadata/${encodeURIComponent(identifier)}`);\n const title = safeUserText(metadata?.metadata?.title) || safeUserText(resource.title) || identifier;\n const files = Array.isArray(metadata?.files) ? metadata.files : [];\n const audioFile = files.find((file) => {\n const name = safeUserText(file?.name);\n const format = safeUserText(file?.format).toLowerCase();\n return /\\.(mp3|m4a|ogg)$/i.test(name) || format.includes('mp3');\n });\n if (!audioFile?.name) {\n throw new Error('Internet Archive audio item has no downloadable audio file');\n }\n const sourceFileName = audioFile.name;\n const fileUrl = `https://archive.org/download/${encodeURIComponent(identifier)}/${sourceFileName\n .split('/')\n .map((part) => encodeURIComponent(part))\n .join('/')}`;\n const response = await fetch(fileUrl, {\n headers: {\n Accept: 'audio/mpeg, audio/*;q=0.9, */*;q=0.5',\n 'User-Agent': 'DachengResourceDownloader/1.0 (+https://ombhrum.com)',\n },\n signal: AbortSignal.timeout(60_000),\n });\n if (!response.ok) {\n throw new Error(`Internet Archive audio download failed: ${response.status}`);\n }\n const binaryContent = Buffer.from(await response.arrayBuffer());\n if (!binaryContent.length) throw new Error('Internet Archive audio returned empty content');\n const sizeMb = (binaryContent.length / 1024 / 1024).toFixed(1);\n return {\n title,\n sourceName: 'Internet Archive 音频',\n url: fileUrl,\n contentText: `已下载音频资源: ${title}\\n文件名: ${sourceFileName}\\n大小: ${sizeMb} MB\\n来源: ${fileUrl}`,\n binaryContent,\n fileName: safeFileName(sourceFileName),\n };\n}\n\nasync function downloadWebResource(input) {\n const url = safeUserText(input.url);\n const parsed = new URL(url);\n if (!['http:', 'https:'].includes(parsed.protocol)) {\n throw new Error('Only http/https resources can be downloaded');\n }\n\n const response = await fetch(parsed, {\n headers: {\n Accept: 'text/html, text/plain, application/xhtml+xml, application/xml;q=0.8, */*;q=0.5',\n 'User-Agent': 'DachengResourceDownloader/1.0 (+https://ombhrum.com)',\n },\n signal: AbortSignal.timeout(25_000),\n });\n if (!response.ok) throw new Error(`Resource download failed: ${response.status}`);\n const contentType = response.headers.get('content-type') || '';\n const body = await response.text();\n const title = safeUserText(input.title) || parsed.hostname;\n const contentText = contentType.includes('html') ? cleanExtractedText(stripHtml(body)) : body.trim();\n if (!contentText) throw new Error('Resource returned empty content');\n return {\n title,\n sourceName: safeUserText(input.sourceName) || parsed.hostname,\n url,\n contentText: contentText.slice(0, maxResourceTextChars),\n fileName: `${safeFileName(title)}.txt`,\n };\n}\n\nfunction persistDownloadedResource(user, content) {\n const id = crypto.randomUUID();\n const fileName = `${id}-${safeFileName(content.fileName)}`;\n const filePath = path.join(resourcesDir, fileName);\n if (content.binaryContent) {\n fs.writeFileSync(filePath, content.binaryContent);\n } else {\n fs.writeFileSync(filePath, content.contentText, 'utf8');\n }\n statements.insertResourceDownload.run({\n id,\n userId: user.userId,\n title: content.title,\n sourceName: content.sourceName,\n sourceUrl: content.url,\n fileName: content.fileName,\n filePath,\n createdAt: nowIso(),\n });\n return { id, fileName, filePath };\n}\n\nfunction createLibreChatMcpServer() {\n const server = new McpServer({\n name: 'dacheng-ai-tools',\n version: '0.1.0',\n });\n\n server.registerTool(\n 'search_dharma_resources',\n {\n title: 'Search dharma resources',\n description:\n 'Search for legally shareable Buddhist scripture, text, web, or audio resources that can be prepared for Fabushi sharing.',\n inputSchema: {\n query: z.string().min(2).max(200),\n limit: z.number().int().min(1).max(12).optional(),\n },\n outputSchema: {\n query: z.string(),\n items: z.array(\n z.object({\n id: z.string(),\n title: z.string(),\n sourceName: z.string(),\n url: z.string(),\n snippet: z.string(),\n resourceType: z.string(),\n work: z.string().optional(),\n juan: z.number().optional(),\n }),\n ),\n },\n annotations: {\n readOnlyHint: true,\n destructiveHint: false,\n openWorldHint: true,\n },\n },\n async ({ query, limit }) => {\n const items = await findResourceCandidates(query, limit ?? 8);\n return textToolResult(`Found ${items.length} candidate resources for \"${query}\".`, {\n query,\n items,\n });\n },\n );\n\n server.registerTool(\n 'download_dharma_resource',\n {\n title: 'Download dharma resource',\n description:\n 'Download and extract a selected resource. Use the url/work/juan returned by search_dharma_resources whenever possible.',\n inputSchema: {\n url: z.string().min(1).max(1000),\n title: z.string().max(300).optional(),\n sourceName: z.string().max(120).optional(),\n work: z.string().max(40).optional(),\n juan: z.number().int().min(1).optional(),\n identifier: z.string().max(300).optional(),\n },\n outputSchema: {\n title: z.string(),\n sourceName: z.string(),\n url: z.string(),\n fileName: z.string(),\n downloadId: z.string(),\n contentText: z.string(),\n },\n annotations: {\n readOnlyHint: false,\n destructiveHint: false,\n openWorldHint: true,\n },\n },\n async (input) => {\n const user = { userId: 'librechat:mcp' };\n const cbeta = parseCbetaResource(input);\n const internetArchive = parseInternetArchiveResource(input);\n const content = cbeta\n ? await downloadCbetaResource({ ...input, ...cbeta })\n : internetArchive\n ? await downloadInternetArchiveAudio({ ...input, ...internetArchive })\n : await downloadWebResource(input);\n const persisted = persistDownloadedResource(user, content);\n const { binaryContent: _binaryContent, ...publicContent } = content;\n return textToolResult(`Downloaded ${publicContent.title} as ${publicContent.fileName}.`, {\n ...publicContent,\n downloadId: persisted.id,\n });\n },\n );\n\n server.registerTool(\n 'prepare_dharma_share_text',\n {\n title: 'Prepare dharma sharing text',\n description:\n 'Prepare a text payload for the Fabushi app to confirm and add to global dharma sharing. This returns a clientAction; it does not start sending.',\n inputSchema: {\n title: z.string().min(1).max(120),\n text: z.string().min(1).max(20000),\n },\n outputSchema: {\n clientAction: z.object({\n type: z.literal('prepare_dharma_share_text'),\n title: z.string(),\n text: z.string(),\n }),\n },\n annotations: {\n readOnlyHint: false,\n destructiveHint: false,\n openWorldHint: false,\n },\n },\n async ({ title, text }) =>\n textToolResult(`Prepared \"${title}\" for user confirmation in the Fabushi app.`, {\n clientAction: {\n type: 'prepare_dharma_share_text',\n title: safeUserText(title).slice(0, 120),\n text: String(text || '').trim().slice(0, 20000),\n },\n }),\n );\n\n server.registerTool(\n 'prepare_practice_book_item',\n {\n title: 'Prepare practice book item',\n description:\n 'Prepare a scripture, chant, or practice item for the Fabushi app practice book. This returns a clientAction; it does not mutate the app by itself.',\n inputSchema: {\n title: z.string().min(1).max(120),\n sourceName: z.string().max(120).optional(),\n text: z.string().min(1).max(20000),\n },\n outputSchema: {\n clientAction: z.object({\n type: z.literal('prepare_practice_book_item'),\n title: z.string(),\n sourceName: z.string(),\n text: z.string(),\n }),\n },\n annotations: {\n readOnlyHint: false,\n destructiveHint: false,\n openWorldHint: false,\n },\n },\n async ({ title, sourceName, text }) =>\n textToolResult(`Prepared \"${title}\" for the Fabushi practice book confirmation flow.`, {\n clientAction: {\n type: 'prepare_practice_book_item',\n title: safeUserText(title).slice(0, 120),\n sourceName: safeUserText(sourceName || '大乘 AI').slice(0, 120),\n text: String(text || '').trim().slice(0, 20000),\n },\n }),\n );\n\n return server;\n}\n\nasync function handleMcpRequest(req, res) {\n const server = createLibreChatMcpServer();\n const transport = new StreamableHTTPServerTransport({\n sessionIdGenerator: undefined,\n enableJsonResponse: true,\n });\n\n res.on('close', () => {\n transport.close();\n server.close();\n });\n\n await server.connect(transport);\n await transport.handleRequest(req, res, req.body);\n}\n\napp.all('/mcp', async (req, res) => {\n try {\n await handleMcpRequest(req, res);\n } catch (error) {\n logger.error({ error: error.stack || String(error) }, 'MCP request failed');\n if (!res.headersSent) {\n res.status(500).json({\n error: 'MCP request failed',\n message: publicAiErrorMessage(error),\n });\n }\n }\n});\n\napp.post(\n '/api/resources/download',\n asyncHandler(async (req, res) => {\n const user = await resolveUser(req, req.body || {});\n const cbeta = parseCbetaResource(req.body || {});\n const internetArchive = parseInternetArchiveResource(req.body || {});\n const content = cbeta\n ? await downloadCbetaResource({ ...req.body, ...cbeta })\n : internetArchive\n ? await downloadInternetArchiveAudio({ ...req.body, ...internetArchive })\n : await downloadWebResource(req.body || {});\n const persisted = persistDownloadedResource(user, content);\n const { binaryContent: _binaryContent, ...publicContent } = content;\n jsonResponse(res, 200, {\n success: true,\n downloadId: persisted.id,\n ...publicContent,\n });\n }),\n);\n\napp.post(\n '/api/codex/resource-task',\n asyncHandler(async (req, res) => {\n if (process.env.ENABLE_CODEX_SDK !== 'true') {\n return jsonResponse(res, 403, {\n success: false,\n message: 'Codex SDK task runner is disabled. Set ENABLE_CODEX_SDK=true on the server to enable it.',\n });\n }\n const prompt = safeUserText(req.body?.prompt);\n if (!prompt) {\n return jsonResponse(res, 400, { success: false, message: 'prompt is required' });\n }\n const user = await resolveUser(req, req.body || {});\n const { Codex } = await import('@openai/codex-sdk');\n const codexRuntime = createCodexDeepSeekRuntime(user);\n const codex = new Codex(codexRuntime.enabled ? codexRuntime.options : {});\n const thread = codex.startThread(\n codexRuntime.enabled\n ? codexRuntime.threadOptions\n : { skipGitRepoCheck: true, networkAccessEnabled: true },\n );\n const result = await thread.run(prompt);\n const usage = {\n promptTokens: Number(result.usage?.input_tokens || 0),\n completionTokens: Number(result.usage?.output_tokens || 0),\n totalTokens: Number((result.usage?.input_tokens || 0) + (result.usage?.output_tokens || 0)),\n };\n if (!codexRuntime.enabled) {\n recordUsage(user.userId, usage.totalTokens || estimateTokens(JSON.stringify(result)));\n }\n jsonResponse(res, 200, {\n success: true,\n threadId: thread.id,\n provider: codexRuntime.provider,\n model: deepseekModel,\n usage,\n result,\n });\n }),\n);\n\nregisterPlatformApi({ app, db, resolveUser, asyncHandler });\n\napp.use((error, _req, res, _next) => {\n const status = Number(error.statusCode || error.status || 500);\n const safeStatus = status >= 400 && status < 600 ? status : 500;\n logger.error({ error: error.stack || String(error) }, 'request failed');\n jsonResponse(res, safeStatus, {\n success: false,\n message: error.message || 'Internal Server Error',\n details: error.details,\n });\n});\n\nconst port = Number(process.env.PORT || 8788);\napp.listen(port, '0.0.0.0', () => {\n logger.info(\n {\n port,\n dbPath,\n dataDir,\n deepseekModel,\n cbetaApiRoot,\n },\n 'Dacheng AI backend started',\n );\n});\n"}}}} |