fix: update production app configuration

This commit is contained in:
qzl
2026-04-30 11:07:57 +08:00
parent 79d5d0638a
commit 98f4a8d07a
21 changed files with 84 additions and 80 deletions
+2 -12
View File
@@ -91,12 +91,6 @@ ERYAO_LLM__PROVIDER_KEYS__DEEPSEEK=
ERYAO_POINTS_POLICY__REGISTER_BONUS_POINTS=60 ERYAO_POINTS_POLICY__REGISTER_BONUS_POINTS=60
ERYAO_POINTS_POLICY__REGISTER_BONUS_HMAC_KEY=replace-with-strong-random-key ERYAO_POINTS_POLICY__REGISTER_BONUS_HMAC_KEY=replace-with-strong-random-key
############
# 敏感词配置
############
ERYAO_SENSITIVE_WORD__USE_ALIYUN=true
ERYAO_SENSITIVE_WORD__FALLBACK_TO_LOCAL=true
############ ############
# CORS 配置 # CORS 配置
############ ############
@@ -112,13 +106,9 @@ ERYAO_TEST__CODE=123456
# Apple IAP 配置 # Apple IAP 配置
############ ############
ERYAO_APPLE_IAP__BUNDLE_ID=com.meeyao.qianwen ERYAO_APPLE_IAP__BUNDLE_ID=com.meeyao.qianwen
# Apple IAP 环境识别。auto 表示以后端验签后的 Apple transaction environment 为准。
ERYAO_APPLE_IAP__ENVIRONMENT=auto
# Server API 密钥(可选,用于主动查询交易状态) # Server API 密钥(可选,用于主动查询交易状态)
ERYAO_APPLE_IAP__SERVER_API_KEY_ID= ERYAO_APPLE_IAP__SERVER_API_KEY_ID=
ERYAO_APPLE_IAP__SERVER_API_PRIVATE_KEY= ERYAO_APPLE_IAP__SERVER_API_PRIVATE_KEY=
ERYAO_APPLE_IAP__SERVER_API_ISSUER_ID= ERYAO_APPLE_IAP__SERVER_API_ISSUER_ID=
# 沙盒测试账号(仅用于手动测试,不用于后端验证)
ERYAO_APPLE_IAP__SANDBOX_TESTER_EMAIL=
ERYAO_APPLE_IAP__SANDBOX_TESTER_PASSWORD=
# Server Notifications V2 URL(在 App Store Connect 中配置)
# 格式: https://<your-domain>/api/v1/payments/apple/notifications
ERYAO_APPLE_IAP__SERVER_NOTIFICATIONS_URL=
+9 -6
View File
@@ -2,9 +2,15 @@
Flutter client for `觅爻签问`. Flutter client for `觅爻签问`.
## Debug startup with backend injection ## Backend URL
This app supports injecting backend URL at startup (same pattern as social-app): Default backend URL:
```text
https://api.meeyao.com
```
This app also supports injecting backend URL at startup (same pattern as social-app):
- Dart read path: `lib/core/config/env.dart` - Dart read path: `lib/core/config/env.dart`
- Injection key: `BACKEND_URL` - Injection key: `BACKEND_URL`
@@ -21,7 +27,4 @@ flutter run --dart-define=BACKEND_URL=http://192.168.1.100:5775
./tool/run-dev.sh --backend-url http://192.168.1.100:5775 ./tool/run-dev.sh --backend-url http://192.168.1.100:5775
``` ```
If `BACKEND_URL` is not provided, fallback is: If `BACKEND_URL` is not provided, the app uses the production backend URL above.
- Android emulator: `http://10.0.2.2:5775`
- Others: `http://localhost:5775`
+1 -1
View File
@@ -12,7 +12,7 @@ MeeYao Divination is designed based on traditional oriental culture. Our core go
**Developer:** Ann Lee **Developer:** Ann Lee
**Contact Email:** ann@xumee.com **Contact Email:** ann@xunmee.com
--- ---
+2 -2
View File
@@ -116,7 +116,7 @@ In accordance with CCPA/CPRA and U.S. local privacy laws, you enjoy the followin
You can submit data requests through the only dedicated contact method: You can submit data requests through the only dedicated contact method:
- **Contact Email**: ann@xumee.com - **Contact Email**: ann@xunmee.com
I will respond to your legitimate request within 45 days, and properly verify your identity to ensure data security before processing. I will respond to your legitimate request within 45 days, and properly verify your identity to ensure data security before processing.
@@ -152,7 +152,7 @@ This Privacy Policy may be updated irregularly to adapt to platform rules and le
If you have any questions, suggestions or privacy-related complaints about this Privacy Policy, please contact me: If you have any questions, suggestions or privacy-related complaints about this Privacy Policy, please contact me:
**Developer Email**: ann@xumee.com **Developer Email**: ann@xunmee.com
If you are a California resident and dissatisfied with the processing result, you can consult the local privacy regulatory authority. If you are a California resident and dissatisfied with the processing result, you can consult the local privacy regulatory authority.
+1 -1
View File
@@ -118,4 +118,4 @@ I reserve the right to revise and update these Terms of Service at any time. Mat
If you have questions, feedback or legal inquiries about these Terms, please contact: If you have questions, feedback or legal inquiries about these Terms, please contact:
- **Developer**: Individual Independent Developer - **Developer**: Individual Independent Developer
- **Contact Email**: ann@xumee.com - **Contact Email**: ann@xunmee.com
+1 -1
View File
@@ -12,7 +12,7 @@
**开发者**Ann Lee **开发者**Ann Lee
**联系邮箱**ann@xumee.com **联系邮箱**ann@xunmee.com
--- ---
+2 -2
View File
@@ -116,7 +116,7 @@
您可以通过唯一指定联系方式提交数据请求: 您可以通过唯一指定联系方式提交数据请求:
- **联系邮箱**ann@xumee.com - **联系邮箱**ann@xunmee.com
我将在 45 天内回复您的合法请求,并在处理前妥善验证您的身份以确保数据安全。 我将在 45 天内回复您的合法请求,并在处理前妥善验证您的身份以确保数据安全。
@@ -152,7 +152,7 @@
如果您对本隐私政策有任何疑问、建议或隐私相关投诉,请联系我: 如果您对本隐私政策有任何疑问、建议或隐私相关投诉,请联系我:
**开发者邮箱**ann@xumee.com **开发者邮箱**ann@xunmee.com
如果您是加州居民且对处理结果不满意,可咨询当地隐私监管机构。 如果您是加州居民且对处理结果不满意,可咨询当地隐私监管机构。
+1 -1
View File
@@ -118,4 +118,4 @@
如果您对本条款有疑问、反馈或法律咨询,请联系: 如果您对本条款有疑问、反馈或法律咨询,请联系:
- **开发者**:独立个人开发者 - **开发者**:独立个人开发者
- **联系邮箱**ann@xumee.com - **联系邮箱**ann@xunmee.com
+1 -1
View File
@@ -12,7 +12,7 @@
**開發者**Ann Lee **開發者**Ann Lee
**聯繫郵箱**ann@xumee.com **聯繫郵箱**ann@xunmee.com
--- ---
+2 -2
View File
@@ -116,7 +116,7 @@
您可以通過唯一指定聯繫方式提交數據請求: 您可以通過唯一指定聯繫方式提交數據請求:
- **聯繫郵箱**ann@xumee.com - **聯繫郵箱**ann@xunmee.com
我將在 45 天內回覆您的合法請求,並在處理前妥善驗證您的身份以確保數據安全。 我將在 45 天內回覆您的合法請求,並在處理前妥善驗證您的身份以確保數據安全。
@@ -152,7 +152,7 @@
如果您對本隱私政策有任何疑問、建議或隱私相關投訴,請聯繫我: 如果您對本隱私政策有任何疑問、建議或隱私相關投訴,請聯繫我:
**開發者郵箱**ann@xumee.com **開發者郵箱**ann@xunmee.com
如果您是加州居民且對處理結果不滿意,可諮詢當地隱私監管機構。 如果您是加州居民且對處理結果不滿意,可諮詢當地隱私監管機構。
@@ -118,4 +118,4 @@
如果您對本條款有疑問、反饋或法律諮詢,請聯繫: 如果您對本條款有疑問、反饋或法律諮詢,請聯繫:
- **開發者**:獨立個人開發者 - **開發者**:獨立個人開發者
- **聯繫郵箱**ann@xumee.com - **聯繫郵箱**ann@xunmee.com
+3 -9
View File
@@ -1,19 +1,13 @@
import 'dart:io';
class Env { class Env {
static const _productionBackendUrl = 'https://api.meeyao.com';
static String get backendUrl { static String get backendUrl {
final injected = const String.fromEnvironment('BACKEND_URL'); final injected = const String.fromEnvironment('BACKEND_URL');
if (injected.isNotEmpty && injected != 'false') { if (injected.isNotEmpty && injected != 'false') {
return injected; return injected;
} }
if (Platform.isAndroid) { return _productionBackendUrl;
return 'http://10.0.2.2:5775';
}
if (Platform.isIOS) {
return 'http://192.168.1.63:5775';
}
return 'http://localhost:5775';
} }
static Future<void> init() async {} static Future<void> init() async {}
+20 -12
View File
@@ -32,6 +32,22 @@ _MAX_CONTEXT_ATTACHMENTS = 3
_RUNTIME_AGENT_OUTPUT_ADAPTER = TypeAdapter(RuntimeAgentOutput) _RUNTIME_AGENT_OUTPUT_ADAPTER = TypeAdapter(RuntimeAgentOutput)
def create_context_messages_cache() -> Any:
from core.agentscope.caches.context_messages_cache import (
create_context_messages_cache as factory,
)
return factory()
def create_attachment_content_cache() -> Any:
from core.agentscope.caches.attachment_content_cache import (
create_attachment_content_cache as factory,
)
return factory()
def _serialize_tool_agent_output( def _serialize_tool_agent_output(
*, *,
metadata: AgentChatMessageMetadata | None, metadata: AgentChatMessageMetadata | None,
@@ -203,12 +219,6 @@ async def _build_recent_context_messages(
context_config: "MessageContextConfig", context_config: "MessageContextConfig",
) -> list[Msg]: ) -> list[Msg]:
from agentscope.message import Msg from agentscope.message import Msg
from core.agentscope.caches.attachment_content_cache import (
create_attachment_content_cache,
)
from core.agentscope.caches.context_messages_cache import (
create_context_messages_cache,
)
from core.agentscope.services.context_service import AgentContextService from core.agentscope.services.context_service import AgentContextService
from services.base.supabase import supabase_service from services.base.supabase import supabase_service
from v1.agent.repository import AgentRepository from v1.agent.repository import AgentRepository
@@ -348,12 +358,10 @@ async def _build_recent_context_messages(
async def run_agentscope_task(command: dict[str, Any]) -> dict[str, object]: async def run_agentscope_task(command: dict[str, Any]) -> dict[str, object]:
from core.agentscope.events import ( from core.agentscope.events.agui_codec import AgentScopeAgUiCodec
AgentScopeAgUiCodec, from core.agentscope.events.pipeline import AgentScopeEventPipeline
AgentScopeEventPipeline, from core.agentscope.events.redis_bus import RedisStreamBus
RedisStreamBus, from core.agentscope.events.store import SqlAlchemyEventStore
SqlAlchemyEventStore,
)
from core.config.settings import config from core.config.settings import config
from core.db.session import AsyncSessionLocal from core.db.session import AsyncSessionLocal
from services.base.redis import get_or_init_redis_client from services.base.redis import get_or_init_redis_client
+1 -9
View File
@@ -183,11 +183,6 @@ class DatabaseSettings(BaseModel):
) )
class SensitiveWordSettings(BaseModel):
use_aliyun: bool = True
fallback_to_local: bool = True
class TestSettings(BaseModel): class TestSettings(BaseModel):
email: str = "" email: str = ""
code: str = "" code: str = ""
@@ -232,12 +227,10 @@ class AppleIapSettings(BaseModel):
bundle_id: str = Field(default="com.meeyao.qianwen", min_length=1) bundle_id: str = Field(default="com.meeyao.qianwen", min_length=1)
root_cert_url: str = "https://www.apple.com/certificateauthority/AppleIncRootCertificate.cer" root_cert_url: str = "https://www.apple.com/certificateauthority/AppleIncRootCertificate.cer"
jws_x5c_cert_url: str = "https://api.storekit.itunes.apple.com/v1/verificationKeys" jws_x5c_cert_url: str = "https://api.storekit.itunes.apple.com/v1/verificationKeys"
environment: Literal["auto", "sandbox", "production"] = "auto"
server_api_issuer_id: str | None = None server_api_issuer_id: str | None = None
server_api_key_id: str | None = None server_api_key_id: str | None = None
server_api_private_key: SecretStr | None = None server_api_private_key: SecretStr | None = None
sandbox_tester_email: str | None = None
sandbox_tester_password: SecretStr | None = None
server_notifications_url: str | None = None
def _resolve_env_files() -> list[str]: def _resolve_env_files() -> list[str]:
@@ -283,7 +276,6 @@ class Settings(BaseSettings):
storage: StorageSettings = StorageSettings() storage: StorageSettings = StorageSettings()
llm: LlmSettings = LlmSettings() llm: LlmSettings = LlmSettings()
database: DatabaseSettings = DatabaseSettings() database: DatabaseSettings = DatabaseSettings()
sensitive_word: SensitiveWordSettings = Field(default_factory=SensitiveWordSettings)
test: TestSettings = Field(default_factory=TestSettings) test: TestSettings = Field(default_factory=TestSettings)
taskiq: TaskiqSettings = Field(default_factory=TaskiqSettings) taskiq: TaskiqSettings = Field(default_factory=TaskiqSettings)
agent_runtime: AgentRuntimeSettings = Field(default_factory=AgentRuntimeSettings) agent_runtime: AgentRuntimeSettings = Field(default_factory=AgentRuntimeSettings)
+1 -1
View File
@@ -9,7 +9,7 @@ from fastapi import Depends
from redis.asyncio import Redis from redis.asyncio import Redis
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from core.agentscope.events import RedisStreamBus from core.agentscope.events.redis_bus import RedisStreamBus
from core.agentscope.tools.tool_result_storage import ( from core.agentscope.tools.tool_result_storage import (
create_tool_result_storage, create_tool_result_storage,
) )
+1 -1
View File
@@ -9,7 +9,7 @@ from typing import Annotated
from ag_ui.core import RunAgentInput from ag_ui.core import RunAgentInput
from core.http.errors import ApiProblemError, problem_payload from core.http.errors import ApiProblemError, problem_payload
from core.agentscope.events import to_sse_event from core.agentscope.events.sse import to_sse_event
from core.agentscope.schemas.agui_input import ( from core.agentscope.schemas.agui_input import (
parse_run_input, parse_run_input,
validate_run_request_messages_contract, validate_run_request_messages_contract,
-12
View File
@@ -47,7 +47,6 @@ class AppleJwsVerifier:
*, *,
expected_bundle_id: str, expected_bundle_id: str,
expected_product_id: str, expected_product_id: str,
expected_environment: str,
) -> VerifiedTransaction | VerificationError: ) -> VerifiedTransaction | VerificationError:
try: try:
unverified_header = jwt.get_unverified_header(signed_transaction_info) unverified_header = jwt.get_unverified_header(signed_transaction_info)
@@ -148,17 +147,6 @@ class AppleJwsVerifier:
detail=f"Invalid environment: {environment}", detail=f"Invalid environment: {environment}",
) )
if environment != expected_environment:
logger.error(
"Environment mismatch: expected=%s got=%s",
expected_environment,
environment,
)
return VerificationError(
code="PAYMENT_ENVIRONMENT_MISMATCH",
detail=f"Environment mismatch: expected={expected_environment} got={environment}",
)
revocation_date_raw = payload.get("revocationDate") revocation_date_raw = payload.get("revocationDate")
revocation_date: int | None = ( revocation_date: int | None = (
int(revocation_date_raw) if revocation_date_raw is not None else None int(revocation_date_raw) if revocation_date_raw is not None else None
-2
View File
@@ -114,12 +114,10 @@ class PaymentService:
) )
expected_bundle_id = config.apple_iap.bundle_id expected_bundle_id = config.apple_iap.bundle_id
expected_environment = "Sandbox" if config.runtime.environment != "prod" else "Production"
result = self._verifier.verify_signed_transaction( result = self._verifier.verify_signed_transaction(
request.signed_transaction_info, request.signed_transaction_info,
expected_bundle_id=expected_bundle_id, expected_bundle_id=expected_bundle_id,
expected_product_id=product_mapping.app_store_product_id, expected_product_id=product_mapping.app_store_product_id,
expected_environment=expected_environment,
) )
if isinstance(result, VerificationError): if isinstance(result, VerificationError):
@@ -82,10 +82,8 @@ class _FakeVerifier:
*, *,
expected_bundle_id: str, expected_bundle_id: str,
expected_product_id: str, expected_product_id: str,
expected_environment: str,
) -> VerifiedTransaction | VerificationError: ) -> VerifiedTransaction | VerificationError:
del signed_transaction_info, expected_bundle_id, expected_product_id del signed_transaction_info, expected_bundle_id, expected_product_id
del expected_environment
return self._result return self._result
@@ -290,6 +288,32 @@ class TestPaymentServiceSuccessfulGrant:
assert len(points_repo.appended_ledger) == 1 assert len(points_repo.appended_ledger) == 1
assert len(payment_repo.inserted_transactions) == 1 assert len(payment_repo.inserted_transactions) == 1
@pytest.mark.asyncio
async def test_grants_production_transaction_from_verified_payload(self) -> None:
payment_repo = _FakePaymentRepository()
service = PaymentService(
payment_repo=payment_repo,
points_repo=_FakePointsRepository(),
verifier=_FakeVerifier(
result=_make_verified_transaction(environment="Production")
),
)
request = VerifyTransactionRequest(
productCode="starter_pack",
appStoreProductId="com.meeyao.qianwen.starter_pack",
transactionId="2000000123456789",
signedTransactionInfo="fake_jws",
)
result = await service.verify_and_grant(
user_id=uuid4(),
user_email="test@example.com",
request=request,
)
assert result.status == "granted"
assert payment_repo.inserted_transactions[0].environment == "Production"
class TestPaymentServiceStarterPackIneligible: class TestPaymentServiceStarterPackIneligible:
@pytest.mark.asyncio @pytest.mark.asyncio
@@ -108,7 +108,6 @@ This document is the source of truth for backend RFC7807 `code` values consumed
|---|---:|---|---| |---|---:|---|---|
| `PAYMENT_PRODUCT_NOT_FOUND` | 404 | `productCode` does not exist or is not enabled | Refresh packages and show product-unavailable message | | `PAYMENT_PRODUCT_NOT_FOUND` | 404 | `productCode` does not exist or is not enabled | Refresh packages and show product-unavailable message |
| `PAYMENT_PRODUCT_MISMATCH` | 422 | Client product ID does not match backend/Apple verification result | Block grant and prompt retry | | `PAYMENT_PRODUCT_MISMATCH` | 422 | Client product ID does not match backend/Apple verification result | Block grant and prompt retry |
| `PAYMENT_ENVIRONMENT_MISMATCH` | 422 | Transaction environment (Sandbox/Production) does not match server environment | Show purchase-verification-failed message |
| `PAYMENT_TRANSACTION_INVALID` | 422 | Apple signed transaction invalid, signature verification failed, or payload malformed | Show purchase-verification-failed message | | `PAYMENT_TRANSACTION_INVALID` | 422 | Apple signed transaction invalid, signature verification failed, or payload malformed | Show purchase-verification-failed message |
| `PAYMENT_TRANSACTION_REVOKED` | 409 | Transaction has been revoked or refunded, grant not allowed | Show purchase-unavailable message | | `PAYMENT_TRANSACTION_REVOKED` | 409 | Transaction has been revoked or refunded, grant not allowed | Show purchase-unavailable message |
| `PAYMENT_TRANSACTION_CONFLICT` | 409 | Transaction already processed by another user or in conflicting state | Prompt to contact support or refresh balance | | `PAYMENT_TRANSACTION_CONFLICT` | 409 | Transaction already processed by another user or in conflicting state | Prompt to contact support or refresh balance |
@@ -14,6 +14,7 @@ Protocol verification status:
- Current strategy: additive evolution (`backward-compatible`). - Current strategy: additive evolution (`backward-compatible`).
- Breaking change requires explicit migration + rollback notes (`requires-migration`). - Breaking change requires explicit migration + rollback notes (`requires-migration`).
- Apple IAP environment is auto-detected from verified Apple transaction payloads; the verify API is not split by Sandbox/Production.
## Route overview ## Route overview
@@ -70,7 +71,6 @@ Verify and grant credits for an Apple IAP transaction.
|---|---:|---| |---|---:|---|
| `PAYMENT_PRODUCT_NOT_FOUND` | 404 | `productCode` does not exist or is not enabled | | `PAYMENT_PRODUCT_NOT_FOUND` | 404 | `productCode` does not exist or is not enabled |
| `PAYMENT_PRODUCT_MISMATCH` | 422 | Client product ID does not match backend/Apple verification result | | `PAYMENT_PRODUCT_MISMATCH` | 422 | Client product ID does not match backend/Apple verification result |
| `PAYMENT_ENVIRONMENT_MISMATCH` | 422 | Transaction environment (Sandbox/Production) does not match server environment |
| `PAYMENT_TRANSACTION_INVALID` | 422 | Apple signed transaction invalid, signature verification failed, or payload malformed | | `PAYMENT_TRANSACTION_INVALID` | 422 | Apple signed transaction invalid, signature verification failed, or payload malformed |
| `PAYMENT_TRANSACTION_REVOKED` | 409 | Transaction has been revoked or refunded, grant not allowed | | `PAYMENT_TRANSACTION_REVOKED` | 409 | Transaction has been revoked or refunded, grant not allowed |
| `PAYMENT_TRANSACTION_CONFLICT` | 409 | Transaction already processed by another user or in conflicting state | | `PAYMENT_TRANSACTION_CONFLICT` | 409 | Transaction already processed by another user or in conflicting state |
@@ -119,6 +119,14 @@ product_mappings:
- Backend tracks purchase via `register_bonus_claims.has_purchased_starter_pack` - Backend tracks purchase via `register_bonus_claims.has_purchased_starter_pack`
- If already purchased, returns `PAYMENT_STARTER_PACK_INELIGIBLE` (409) - If already purchased, returns `PAYMENT_STARTER_PACK_INELIGIBLE` (409)
## Environment handling
- `signedTransactionInfo` is verified server-side and its Apple-provided `environment` field is the source of truth.
- Valid environments are `Sandbox` and `Production`.
- TestFlight and App Review Sandbox transactions are accepted when Apple signs them as `Sandbox`.
- App Store Production transactions are accepted when Apple signs them as `Production`.
- Backend configuration defaults to `apple_iap.environment=auto`; it must not force all verifications into one Apple environment.
## Ledger integration ## Ledger integration
- Successful purchases create a ledger entry with: - Successful purchases create a ledger entry with: