Deploy production CI/CD automation #3
+2
-12
@@ -91,12 +91,6 @@ ERYAO_LLM__PROVIDER_KEYS__DEEPSEEK=
|
||||
ERYAO_POINTS_POLICY__REGISTER_BONUS_POINTS=60
|
||||
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 配置
|
||||
############
|
||||
@@ -112,13 +106,9 @@ ERYAO_TEST__CODE=123456
|
||||
# Apple IAP 配置
|
||||
############
|
||||
ERYAO_APPLE_IAP__BUNDLE_ID=com.meeyao.qianwen
|
||||
# Apple IAP 环境识别。auto 表示以后端验签后的 Apple transaction environment 为准。
|
||||
ERYAO_APPLE_IAP__ENVIRONMENT=auto
|
||||
# Server API 密钥(可选,用于主动查询交易状态)
|
||||
ERYAO_APPLE_IAP__SERVER_API_KEY_ID=
|
||||
ERYAO_APPLE_IAP__SERVER_API_PRIVATE_KEY=
|
||||
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=
|
||||
|
||||
@@ -24,6 +24,7 @@ jobs:
|
||||
test -n "${{ secrets.AWS_REGION }}"
|
||||
test -n "${{ secrets.AWS_ACCOUNT_ID }}"
|
||||
test -n "${{ secrets.ECR_REPOSITORY }}"
|
||||
test -n "${{ secrets.DEPLOY_SSH_KEY }}"
|
||||
|
||||
- name: Build backend production image
|
||||
run: |
|
||||
@@ -33,7 +34,6 @@ jobs:
|
||||
--load \
|
||||
--file backend/Dockerfile \
|
||||
--tag ${IMAGE_NAME}:prod-${GITHUB_SHA} \
|
||||
--tag ${IMAGE_NAME}:prod-latest \
|
||||
.
|
||||
|
||||
- name: Check image size budget
|
||||
@@ -88,7 +88,81 @@ jobs:
|
||||
aws ecr get-login-password --region "${AWS_REGION}" \
|
||||
| docker login --username AWS --password-stdin "${ecr_registry}"
|
||||
|
||||
docker tag "${IMAGE_NAME}:prod-${GITHUB_SHA}" "${ecr_image}:${GITHUB_SHA}"
|
||||
docker tag "${IMAGE_NAME}:prod-${GITHUB_SHA}" "${ecr_image}:latest"
|
||||
docker push "${ecr_image}:${GITHUB_SHA}"
|
||||
|
||||
image_ids="$(aws ecr list-images \
|
||||
--region "${AWS_REGION}" \
|
||||
--repository-name "${ECR_REPOSITORY}" \
|
||||
--query 'imageIds[*]' \
|
||||
--output json)"
|
||||
if [ "${image_ids}" != "[]" ]; then
|
||||
aws ecr batch-delete-image \
|
||||
--region "${AWS_REGION}" \
|
||||
--repository-name "${ECR_REPOSITORY}" \
|
||||
--image-ids "${image_ids}" >/dev/null
|
||||
fi
|
||||
|
||||
docker push "${ecr_image}:latest"
|
||||
|
||||
deploy-production:
|
||||
needs: build-backend-image
|
||||
runs-on: wsl2-docker-host
|
||||
steps:
|
||||
- name: Validate deploy configuration
|
||||
run: |
|
||||
set -euo pipefail
|
||||
test -n "${{ secrets.DEPLOY_SSH_KEY }}"
|
||||
test -n "${{ secrets.DEPLOY_HOST }}"
|
||||
test -n "${{ secrets.DEPLOY_USER }}"
|
||||
test -n "${{ secrets.AWS_ACCESS_KEY_ID }}"
|
||||
test -n "${{ secrets.AWS_SECRET_ACCESS_KEY }}"
|
||||
test -n "${{ secrets.AWS_REGION }}"
|
||||
|
||||
- name: Deploy production server
|
||||
env:
|
||||
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
|
||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
|
||||
AWS_DEFAULT_REGION: ${{ secrets.AWS_REGION }}
|
||||
AWS_REGION: ${{ secrets.AWS_REGION }}
|
||||
DEPLOY_HOST: ${{ secrets.DEPLOY_HOST }}
|
||||
DEPLOY_USER: ${{ secrets.DEPLOY_USER }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
install -m 700 -d ~/.ssh
|
||||
printf '%s\n' '${{ secrets.DEPLOY_SSH_KEY }}' > ~/.ssh/eryao_deploy_key
|
||||
chmod 600 ~/.ssh/eryao_deploy_key
|
||||
ssh-keyscan -H "${DEPLOY_HOST}" >> ~/.ssh/known_hosts
|
||||
|
||||
ssh -i ~/.ssh/eryao_deploy_key \
|
||||
-o IdentitiesOnly=yes \
|
||||
"${DEPLOY_USER}@${DEPLOY_HOST}" \
|
||||
"AWS_ACCESS_KEY_ID='${AWS_ACCESS_KEY_ID}' AWS_SECRET_ACCESS_KEY='${AWS_SECRET_ACCESS_KEY}' AWS_DEFAULT_REGION='${AWS_REGION}' AWS_REGION='${AWS_REGION}' bash -se" <<'REMOTE'
|
||||
set -euo pipefail
|
||||
|
||||
cd ~/deploy
|
||||
set -a
|
||||
. ./.env
|
||||
set +a
|
||||
|
||||
ecr_registry="${AWS_ACCOUNT_ID}.dkr.ecr.${AWS_REGION}.amazonaws.com"
|
||||
aws ecr get-login-password --region "${AWS_REGION}" \
|
||||
| sudo docker login --username AWS --password-stdin "${ecr_registry}"
|
||||
|
||||
sudo docker compose --env-file ./.env -f docker-compose.prod.yml --profile workers pull
|
||||
sudo docker compose --env-file ./.env -f docker-compose.prod.yml --profile workers up -d --remove-orphans
|
||||
|
||||
for attempt in $(seq 1 12); do
|
||||
if curl -fsS "http://127.0.0.1:${ERYAO_WEB__PORT:-5775}/health"; then
|
||||
break
|
||||
fi
|
||||
if [ "${attempt}" -eq 12 ]; then
|
||||
sudo docker compose --env-file ./.env -f docker-compose.prod.yml --profile workers ps
|
||||
sudo docker logs --tail 200 eryao-prod-backend || true
|
||||
exit 1
|
||||
fi
|
||||
sleep 5
|
||||
done
|
||||
|
||||
sudo docker image prune -af --filter "until=168h"
|
||||
REMOTE
|
||||
|
||||
@@ -137,6 +137,11 @@ ENV/
|
||||
env.bak/
|
||||
venv.bak/
|
||||
|
||||
# Local deployment secrets
|
||||
*.pem
|
||||
deploy_eryao_ci
|
||||
deploy_eryao_ci.pub
|
||||
|
||||
# Spyder project settings
|
||||
.spyderproject
|
||||
.spyproject
|
||||
|
||||
+9
-6
@@ -2,9 +2,15 @@
|
||||
|
||||
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`
|
||||
- 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
|
||||
```
|
||||
|
||||
If `BACKEND_URL` is not provided, fallback is:
|
||||
|
||||
- Android emulator: `http://10.0.2.2:5775`
|
||||
- Others: `http://localhost:5775`
|
||||
If `BACKEND_URL` is not provided, the app uses the production backend URL above.
|
||||
|
||||
@@ -12,7 +12,7 @@ MeeYao Divination is designed based on traditional oriental culture. Our core go
|
||||
|
||||
**Developer:** Ann Lee
|
||||
|
||||
**Contact Email:** ann@xumee.com
|
||||
**Contact Email:** ann@xunmee.com
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
- **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.
|
||||
|
||||
@@ -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:
|
||||
|
||||
**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.
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
- **Developer**: Individual Independent Developer
|
||||
- **Contact Email**: ann@xumee.com
|
||||
- **Contact Email**: ann@xunmee.com
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
|
||||
**开发者**:Ann Lee
|
||||
|
||||
**联系邮箱**:ann@xumee.com
|
||||
**联系邮箱**:ann@xunmee.com
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
## 引言
|
||||
|
||||
尊敬的用户,欢迎使用 米爻 MeeYao(以下简称"本应用"),本应用由**个人开发者**("我")独立开发和运营。我致力于保护您的个人隐私,并遵守适用的美国联邦和州隐私法律,包括《加州消费者隐私法》(CCPA/CPRA)、《儿童在线隐私保护法》(COPPA)、CalOPPA 以及其他美国州隐私法规。
|
||||
尊敬的用户,欢迎使用 觅爻 MeeYao(以下简称"本应用"),本应用由**个人开发者**("我")独立开发和运营。我致力于保护您的个人隐私,并遵守适用的美国联邦和州隐私法律,包括《加州消费者隐私法》(CCPA/CPRA)、《儿童在线隐私保护法》(COPPA)、CalOPPA 以及其他美国州隐私法规。
|
||||
|
||||
本隐私政策清晰说明:
|
||||
|
||||
@@ -116,7 +116,7 @@
|
||||
|
||||
您可以通过唯一指定联系方式提交数据请求:
|
||||
|
||||
- **联系邮箱**:ann@xumee.com
|
||||
- **联系邮箱**:ann@xunmee.com
|
||||
|
||||
我将在 45 天内回复您的合法请求,并在处理前妥善验证您的身份以确保数据安全。
|
||||
|
||||
@@ -152,7 +152,7 @@
|
||||
|
||||
如果您对本隐私政策有任何疑问、建议或隐私相关投诉,请联系我:
|
||||
|
||||
**开发者邮箱**:ann@xumee.com
|
||||
**开发者邮箱**:ann@xunmee.com
|
||||
|
||||
如果您是加州居民且对处理结果不满意,可咨询当地隐私监管机构。
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
## 1. 条款接受
|
||||
|
||||
米爻 MeeYao(以下简称"本应用")由**个人开发者**("我")独立开发、拥有和运营。
|
||||
觅爻 MeeYao(以下简称"本应用")由**个人开发者**("我")独立开发、拥有和运营。
|
||||
|
||||
下载、安装、注册、访问或使用本应用,即表示您("您"或"用户")确认已阅读、理解并无条件同意受本服务条款("条款")及我的隐私政策约束。如果您不同意本条款,请勿使用本应用。
|
||||
|
||||
@@ -118,4 +118,4 @@
|
||||
如果您对本条款有疑问、反馈或法律咨询,请联系:
|
||||
|
||||
- **开发者**:独立个人开发者
|
||||
- **联系邮箱**:ann@xumee.com
|
||||
- **联系邮箱**:ann@xunmee.com
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
# 關於我們
|
||||
|
||||
歡迎使用 觅爻 MeeYao,一款依託 AI 技術、以傳統六爻文化與易經智慧為核心的傳統文化參考工具。
|
||||
歡迎使用 覓爻 MeeYao,一款依託 AI 技術、以傳統六爻文化與易經智慧為核心的傳統文化參考工具。
|
||||
|
||||
六爻文化源自博大精深的易經哲學體系,承載著古人對於心念、時序與天地變化相生相融的傳統認知。結合卦象文化、五行理論及干支傳統人文理念,幫助用戶探索東方傳統文化內涵,獲得多元的生活參考視角。
|
||||
|
||||
觅爻 MeeYao 根植於東方傳統文脈,核心初衷是幫助用戶跳出固有思維局限,以更開闊的視角看待日常抉擇與生活狀態,保持理性平和的心態。我們希望借助現代 AI 技術,讓大眾更輕鬆地了解、感受與體驗中華傳統經典文化。
|
||||
覓爻 MeeYao 根植於東方傳統文脈,核心初衷是幫助用戶跳出固有思維局限,以更開闊的視角看待日常抉擇與生活狀態,保持理性平和的心態。我們希望借助現代 AI 技術,讓大眾更輕鬆地了解、感受與體驗中華傳統經典文化。
|
||||
|
||||
---
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
|
||||
**開發者**:Ann Lee
|
||||
|
||||
**聯繫郵箱**:ann@xumee.com
|
||||
**聯繫郵箱**:ann@xunmee.com
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
## 引言
|
||||
|
||||
尊敬的用戶,歡迎使用 米爻 MeeYao(以下簡稱「本應用」),本應用由**個人開發者**(「我」)獨立開發和運營。我致力於保護您的個人隱私,並遵守適用的美國聯邦和州隱私法律,包括《加州消費者隱私法》(CCPA/CPRA)、《兒童在線隱私保護法》(COPPA)、CalOPPA 以及其他美國州隱私法規。
|
||||
尊敬的用戶,歡迎使用 覓爻 MeeYao(以下簡稱「本應用」),本應用由**個人開發者**(「我」)獨立開發和運營。我致力於保護您的個人隱私,並遵守適用的美國聯邦和州隱私法律,包括《加州消費者隱私法》(CCPA/CPRA)、《兒童在線隱私保護法》(COPPA)、CalOPPA 以及其他美國州隱私法規。
|
||||
|
||||
本隱私政策清晰說明:
|
||||
|
||||
@@ -116,7 +116,7 @@
|
||||
|
||||
您可以通過唯一指定聯繫方式提交數據請求:
|
||||
|
||||
- **聯繫郵箱**:ann@xumee.com
|
||||
- **聯繫郵箱**:ann@xunmee.com
|
||||
|
||||
我將在 45 天內回覆您的合法請求,並在處理前妥善驗證您的身份以確保數據安全。
|
||||
|
||||
@@ -152,7 +152,7 @@
|
||||
|
||||
如果您對本隱私政策有任何疑問、建議或隱私相關投訴,請聯繫我:
|
||||
|
||||
**開發者郵箱**:ann@xumee.com
|
||||
**開發者郵箱**:ann@xunmee.com
|
||||
|
||||
如果您是加州居民且對處理結果不滿意,可諮詢當地隱私監管機構。
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
## 1. 條款接受
|
||||
|
||||
米爻 MeeYao(以下簡稱「本應用」)由**個人開發者**(「我」)獨立開發、擁有和運營。
|
||||
覓爻 MeeYao(以下簡稱「本應用」)由**個人開發者**(「我」)獨立開發、擁有和運營。
|
||||
|
||||
下載、安裝、註冊、訪問或使用本應用,即表示您(「您」或「用戶」)確認已閱讀、理解並無條件同意受本服務條款(「條款」)及我的隱私政策約束。如果您不同意本條款,請勿使用本應用。
|
||||
|
||||
@@ -118,4 +118,4 @@
|
||||
如果您對本條款有疑問、反饋或法律諮詢,請聯繫:
|
||||
|
||||
- **開發者**:獨立個人開發者
|
||||
- **聯繫郵箱**:ann@xumee.com
|
||||
- **聯繫郵箱**:ann@xunmee.com
|
||||
|
||||
@@ -1,19 +1,13 @@
|
||||
import 'dart:io';
|
||||
|
||||
class Env {
|
||||
static const _productionBackendUrl = 'https://api.meeyao.com';
|
||||
|
||||
static String get backendUrl {
|
||||
final injected = const String.fromEnvironment('BACKEND_URL');
|
||||
if (injected.isNotEmpty && injected != 'false') {
|
||||
return injected;
|
||||
}
|
||||
|
||||
if (Platform.isAndroid) {
|
||||
return 'http://10.0.2.2:5775';
|
||||
}
|
||||
if (Platform.isIOS) {
|
||||
return 'http://192.168.1.63:5775';
|
||||
}
|
||||
return 'http://localhost:5775';
|
||||
return _productionBackendUrl;
|
||||
}
|
||||
|
||||
static Future<void> init() async {}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""手动触发 worker-general 定时任务:生成反馈报告
|
||||
"""手动触发 worker-agent 定时任务:生成反馈报告
|
||||
|
||||
用法:
|
||||
cd /home/qzl/Code/eryao/.worktrees/feat-user-feedback
|
||||
@@ -12,13 +12,13 @@ from pathlib import Path
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent.parent / "src"))
|
||||
|
||||
from core.taskiq.app import worker_general_broker
|
||||
from core.taskiq.app import worker_agent_broker
|
||||
from v1.feedback.tasks import generate_daily_feedback_report
|
||||
|
||||
|
||||
def main():
|
||||
task = generate_daily_feedback_report.kiq()
|
||||
result = worker_general_broker.wait_result(task, timeout=120)
|
||||
result = worker_agent_broker.wait_result(task, timeout=120)
|
||||
print(f"Task result: {result.return_value}")
|
||||
|
||||
|
||||
|
||||
@@ -1,9 +1,3 @@
|
||||
from core.agentscope.events.agui_codec import AgentScopeAgUiCodec, to_agui_wire_event
|
||||
from core.agentscope.events.pipeline import AgentScopeEventPipeline
|
||||
from core.agentscope.events.redis_bus import RedisStreamBus
|
||||
from core.agentscope.events.sse import to_sse_event
|
||||
from core.agentscope.events.store import NullEventStore, SqlAlchemyEventStore
|
||||
|
||||
__all__ = [
|
||||
"AgentScopeAgUiCodec",
|
||||
"AgentScopeEventPipeline",
|
||||
@@ -13,3 +7,36 @@ __all__ = [
|
||||
"to_agui_wire_event",
|
||||
"to_sse_event",
|
||||
]
|
||||
|
||||
|
||||
def __getattr__(name: str):
|
||||
if name in {"AgentScopeAgUiCodec", "to_agui_wire_event"}:
|
||||
from core.agentscope.events.agui_codec import (
|
||||
AgentScopeAgUiCodec,
|
||||
to_agui_wire_event,
|
||||
)
|
||||
|
||||
return {
|
||||
"AgentScopeAgUiCodec": AgentScopeAgUiCodec,
|
||||
"to_agui_wire_event": to_agui_wire_event,
|
||||
}[name]
|
||||
if name == "AgentScopeEventPipeline":
|
||||
from core.agentscope.events.pipeline import AgentScopeEventPipeline
|
||||
|
||||
return AgentScopeEventPipeline
|
||||
if name == "RedisStreamBus":
|
||||
from core.agentscope.events.redis_bus import RedisStreamBus
|
||||
|
||||
return RedisStreamBus
|
||||
if name == "to_sse_event":
|
||||
from core.agentscope.events.sse import to_sse_event
|
||||
|
||||
return to_sse_event
|
||||
if name in {"NullEventStore", "SqlAlchemyEventStore"}:
|
||||
from core.agentscope.events.store import NullEventStore, SqlAlchemyEventStore
|
||||
|
||||
return {
|
||||
"NullEventStore": NullEventStore,
|
||||
"SqlAlchemyEventStore": SqlAlchemyEventStore,
|
||||
}[name]
|
||||
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
||||
|
||||
@@ -6,7 +6,7 @@ from agentscope.agent import ReActAgent
|
||||
from agentscope.message import Msg
|
||||
from pydantic import BaseModel
|
||||
|
||||
from core.agentscope.utils import finalize_json_response
|
||||
from core.agentscope.utils.json_finalize import finalize_json_response
|
||||
|
||||
|
||||
class JsonReActAgent(ReActAgent):
|
||||
|
||||
@@ -1,17 +1,18 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from typing import Any, Awaitable, Callable, Protocol
|
||||
from typing import TYPE_CHECKING, Any, Awaitable, Callable, Protocol
|
||||
|
||||
from ag_ui.core.types import RunAgentInput
|
||||
from agentscope.message import Msg
|
||||
from openai import APIConnectionError
|
||||
from core.agentscope.runtime.runner import AgentScopeRunner
|
||||
from core.agentscope.runtime.protocols import PipelineLike
|
||||
from core.logging import get_logger
|
||||
from schemas.agent.runtime_config import RuntimeConfig
|
||||
from schemas.shared.user import UserContext
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from agentscope.message import Msg
|
||||
|
||||
logger = get_logger("core.agentscope.runtime.orchestrator")
|
||||
|
||||
|
||||
@@ -38,8 +39,12 @@ class AgentScopeRuntimeOrchestrator:
|
||||
pipeline: PipelineLike,
|
||||
runner: RunnerLike | None = None,
|
||||
) -> None:
|
||||
if runner is None:
|
||||
from core.agentscope.runtime.runner import AgentScopeRunner
|
||||
|
||||
runner = AgentScopeRunner()
|
||||
self._pipeline = pipeline
|
||||
self._runner = runner or AgentScopeRunner()
|
||||
self._runner = runner
|
||||
|
||||
async def run(
|
||||
self,
|
||||
|
||||
@@ -22,7 +22,7 @@ from core.divination import derive_divination
|
||||
from core.agentscope.runtime.json_react_agent import JsonReActAgent
|
||||
from core.agentscope.runtime.model_tracking import TrackingChatModel
|
||||
from core.agentscope.runtime.stage_emitter import PipelineStageEmitter
|
||||
from core.agentscope.utils import patch_agentscope_json_repair_compat
|
||||
from core.agentscope.utils.compat import patch_agentscope_json_repair_compat
|
||||
from core.agentscope.utils.json_finalize import finalize_json_response
|
||||
from core.config.settings import config
|
||||
from core.db.session import AsyncSessionLocal
|
||||
|
||||
@@ -5,7 +5,7 @@ from uuid import uuid4
|
||||
|
||||
from agentscope.message import Msg
|
||||
|
||||
from core.agentscope.utils import parse_tool_agent_output
|
||||
from core.agentscope.utils.parsing import parse_tool_agent_output
|
||||
|
||||
|
||||
class PipelineLike(Protocol):
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from core.taskiq.app import worker_agent_broker
|
||||
|
||||
|
||||
@worker_agent_broker.task(task_name="tasks.agentscope.run_command.agent")
|
||||
async def run_command_task_agent(command: dict[str, object]) -> dict[str, object]:
|
||||
del command
|
||||
raise RuntimeError("task handle is only for enqueueing")
|
||||
|
||||
|
||||
@worker_agent_broker.task(task_name="tasks.agentscope.run_command.general")
|
||||
async def run_command_task_general(command: dict[str, object]) -> dict[str, object]:
|
||||
del command
|
||||
raise RuntimeError("task handle is only for enqueueing")
|
||||
@@ -3,31 +3,13 @@ from __future__ import annotations
|
||||
import asyncio
|
||||
import base64
|
||||
import json
|
||||
from typing import Any, cast
|
||||
from typing import TYPE_CHECKING, Any, cast
|
||||
from uuid import UUID
|
||||
|
||||
from agentscope.message import Msg
|
||||
from pydantic import TypeAdapter
|
||||
from core.agentscope.caches import create_user_context_cache
|
||||
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.events import (
|
||||
AgentScopeAgUiCodec,
|
||||
AgentScopeEventPipeline,
|
||||
RedisStreamBus,
|
||||
SqlAlchemyEventStore,
|
||||
)
|
||||
from core.agentscope.runtime.orchestrator import AgentScopeRuntimeOrchestrator
|
||||
from core.agentscope.schemas.agui_input import parse_run_input
|
||||
from core.agentscope.services.context_service import AgentContextService
|
||||
from core.config.settings import config
|
||||
from core.db.session import AsyncSessionLocal
|
||||
from core.logging import get_logger
|
||||
from core.taskiq.app import worker_agent_broker, worker_general_broker
|
||||
from core.taskiq.app import worker_agent_broker
|
||||
from schemas.agent.forwarded_props import (
|
||||
RuntimeMode,
|
||||
parse_forwarded_props_runtime_mode,
|
||||
@@ -40,18 +22,32 @@ from schemas.domain.chat_message import (
|
||||
)
|
||||
from schemas.shared.user import UserContext
|
||||
from schemas.shared.user import parse_profile_settings
|
||||
from services.base.redis import get_or_init_redis_client
|
||||
from services.base.supabase import supabase_service
|
||||
from v1.agent.repository import AgentRepository
|
||||
from v1.points.repository import PointsRepository
|
||||
from v1.users.repository import SQLAlchemyUserRepository
|
||||
from v1.points.service import PointsService
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from agentscope.message import Msg
|
||||
|
||||
logger = get_logger("core.agentscope.runtime.tasks")
|
||||
_MAX_CONTEXT_ATTACHMENTS = 3
|
||||
_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(
|
||||
*,
|
||||
metadata: AgentChatMessageMetadata | None,
|
||||
@@ -176,6 +172,8 @@ def _serialize_assistant_context_from_metadata(
|
||||
|
||||
|
||||
def _load_runtime() -> type[Any]:
|
||||
from core.agentscope.runtime.orchestrator import AgentScopeRuntimeOrchestrator
|
||||
|
||||
return AgentScopeRuntimeOrchestrator
|
||||
|
||||
|
||||
@@ -186,6 +184,8 @@ async def _build_user_context(
|
||||
session: Any,
|
||||
session_id: str,
|
||||
) -> UserContext:
|
||||
from core.agentscope.caches import create_user_context_cache
|
||||
|
||||
cache = create_user_context_cache()
|
||||
cached = await cache.get(session_id=UUID(session_id))
|
||||
if cached:
|
||||
@@ -218,6 +218,11 @@ async def _build_recent_context_messages(
|
||||
runtime_mode: RuntimeMode = RuntimeMode.CHAT,
|
||||
context_config: "MessageContextConfig",
|
||||
) -> list[Msg]:
|
||||
from agentscope.message import Msg
|
||||
from core.agentscope.services.context_service import AgentContextService
|
||||
from services.base.supabase import supabase_service
|
||||
from v1.agent.repository import AgentRepository
|
||||
|
||||
context_cache = create_context_messages_cache()
|
||||
attachment_cache = create_attachment_content_cache()
|
||||
raw_messages = await context_cache.get(
|
||||
@@ -353,6 +358,16 @@ async def _build_recent_context_messages(
|
||||
|
||||
|
||||
async def run_agentscope_task(command: dict[str, Any]) -> dict[str, object]:
|
||||
from core.agentscope.events.agui_codec import AgentScopeAgUiCodec
|
||||
from core.agentscope.events.pipeline import AgentScopeEventPipeline
|
||||
from core.agentscope.events.redis_bus import RedisStreamBus
|
||||
from core.agentscope.events.store import SqlAlchemyEventStore
|
||||
from core.config.settings import config
|
||||
from core.db.session import AsyncSessionLocal
|
||||
from services.base.redis import get_or_init_redis_client
|
||||
from v1.points.repository import PointsRepository
|
||||
from v1.points.service import PointsService
|
||||
|
||||
command_type = str(command.get("command", "run")).strip().lower()
|
||||
raw_owner_id = command.get("owner_id")
|
||||
raw_owner_email = command.get("owner_email")
|
||||
@@ -485,6 +500,6 @@ async def run_command_task_agent(command: dict[str, object]) -> dict[str, object
|
||||
return await run_agentscope_task(command)
|
||||
|
||||
|
||||
@worker_general_broker.task(task_name="tasks.agentscope.run_command.general")
|
||||
@worker_agent_broker.task(task_name="tasks.agentscope.run_command.general")
|
||||
async def run_command_task_general(command: dict[str, object]) -> dict[str, object]:
|
||||
return await run_agentscope_task(command)
|
||||
|
||||
@@ -183,11 +183,6 @@ class DatabaseSettings(BaseModel):
|
||||
)
|
||||
|
||||
|
||||
class SensitiveWordSettings(BaseModel):
|
||||
use_aliyun: bool = True
|
||||
fallback_to_local: bool = True
|
||||
|
||||
|
||||
class TestSettings(BaseModel):
|
||||
email: str = ""
|
||||
code: str = ""
|
||||
@@ -232,12 +227,10 @@ class AppleIapSettings(BaseModel):
|
||||
bundle_id: str = Field(default="com.meeyao.qianwen", min_length=1)
|
||||
root_cert_url: str = "https://www.apple.com/certificateauthority/AppleIncRootCertificate.cer"
|
||||
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_key_id: str | 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]:
|
||||
@@ -283,7 +276,6 @@ class Settings(BaseSettings):
|
||||
storage: StorageSettings = StorageSettings()
|
||||
llm: LlmSettings = LlmSettings()
|
||||
database: DatabaseSettings = DatabaseSettings()
|
||||
sensitive_word: SensitiveWordSettings = Field(default_factory=SensitiveWordSettings)
|
||||
test: TestSettings = Field(default_factory=TestSettings)
|
||||
taskiq: TaskiqSettings = Field(default_factory=TaskiqSettings)
|
||||
agent_runtime: AgentRuntimeSettings = Field(default_factory=AgentRuntimeSettings)
|
||||
|
||||
@@ -23,7 +23,7 @@ def _build_broker(queue_name: str) -> ListQueueBroker:
|
||||
|
||||
|
||||
worker_agent_broker = _build_broker("agent")
|
||||
worker_general_broker = _build_broker("general")
|
||||
worker_general_broker = worker_agent_broker
|
||||
|
||||
broker = worker_agent_broker
|
||||
|
||||
|
||||
@@ -3,9 +3,6 @@ from __future__ import annotations
|
||||
import asyncio
|
||||
from typing import Any
|
||||
|
||||
import dashscope
|
||||
from dashscope.audio.asr import Recognition, RecognitionCallback
|
||||
|
||||
from core.config.settings import config
|
||||
from core.logging import get_logger
|
||||
|
||||
@@ -28,6 +25,9 @@ class AsrService:
|
||||
|
||||
async def transcribe_file(self, file_path: str, filename: str) -> str:
|
||||
try:
|
||||
import dashscope
|
||||
from dashscope.audio.asr import Recognition, RecognitionCallback
|
||||
|
||||
dashscope.api_key = self._get_api_key()
|
||||
|
||||
loop = asyncio.get_event_loop()
|
||||
|
||||
@@ -9,7 +9,7 @@ from fastapi import Depends
|
||||
from redis.asyncio import Redis
|
||||
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 (
|
||||
create_tool_result_storage,
|
||||
)
|
||||
@@ -48,7 +48,7 @@ class TaskiqQueueClient:
|
||||
|
||||
@staticmethod
|
||||
def _select_queue_task(command: dict[str, object]) -> Any:
|
||||
from core.agentscope.runtime.tasks import (
|
||||
from core.agentscope.runtime.task_handles import (
|
||||
run_command_task_agent,
|
||||
run_command_task_general,
|
||||
)
|
||||
|
||||
@@ -9,7 +9,7 @@ from typing import Annotated
|
||||
|
||||
from ag_ui.core import RunAgentInput
|
||||
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 (
|
||||
parse_run_input,
|
||||
validate_run_request_messages_contract,
|
||||
|
||||
@@ -5,15 +5,11 @@ from pathlib import Path
|
||||
|
||||
from sqlalchemy import select
|
||||
from structlog import get_logger
|
||||
from taskiq_redis import RedisScheduleSource
|
||||
|
||||
from core.config.settings import config
|
||||
from core.db.session import AsyncSessionLocal
|
||||
from core.email.sender import EmailAttachment, EmailMessage, EmailSender
|
||||
from core.email.template_loader import load_template
|
||||
from core.taskiq.app import worker_general_broker
|
||||
from core.taskiq.app import worker_agent_broker
|
||||
from models.user_feedback import UserFeedback
|
||||
from v1.feedback.report import generate_feedback_report
|
||||
|
||||
logger = get_logger("v1.feedback.tasks")
|
||||
|
||||
@@ -52,6 +48,8 @@ def _build_report_email_html(
|
||||
end_time: datetime,
|
||||
push_hour: int,
|
||||
) -> str:
|
||||
from core.email.template_loader import load_template
|
||||
|
||||
template = load_template("feedback", "daily_report.html")
|
||||
return template.substitute(
|
||||
start_date=start_time.strftime("%Y-%m-%d"),
|
||||
@@ -70,6 +68,8 @@ def _build_no_feedback_email_html(
|
||||
end_time: datetime,
|
||||
push_hour: int,
|
||||
) -> str:
|
||||
from core.email.template_loader import load_template
|
||||
|
||||
template = load_template("feedback", "no_feedback.html")
|
||||
return template.substitute(
|
||||
start_date=start_time.strftime("%Y-%m-%d"),
|
||||
@@ -87,6 +87,8 @@ async def _send_feedback_email(
|
||||
push_hour: int,
|
||||
report_path: Path | None = None,
|
||||
) -> bool:
|
||||
from core.email.sender import EmailAttachment, EmailMessage, EmailSender
|
||||
|
||||
sender = EmailSender()
|
||||
|
||||
if feedbacks:
|
||||
@@ -123,12 +125,14 @@ async def _send_feedback_email(
|
||||
|
||||
|
||||
# type: ignore reportArgumentType for taskiq decorator
|
||||
@worker_general_broker.on_event("startup") # pyright: ignore[reportArgumentType]
|
||||
@worker_agent_broker.on_event("startup") # pyright: ignore[reportArgumentType]
|
||||
async def _register_feedback_report_schedule() -> None:
|
||||
if not config.feedback_report.enabled:
|
||||
logger.info("Feedback report scheduling disabled")
|
||||
return
|
||||
|
||||
from taskiq_redis import RedisScheduleSource
|
||||
|
||||
schedule_source = RedisScheduleSource(
|
||||
url=config.taskiq_broker_url,
|
||||
prefix="schedule:feedback",
|
||||
@@ -146,12 +150,14 @@ async def _register_feedback_report_schedule() -> None:
|
||||
)
|
||||
|
||||
|
||||
@worker_general_broker.task(task_name="tasks.feedback.generate_daily_report")
|
||||
@worker_agent_broker.task(task_name="tasks.feedback.generate_daily_report")
|
||||
async def generate_daily_feedback_report() -> str | None:
|
||||
if not config.feedback_report.enabled:
|
||||
logger.info("Feedback report is disabled, skipping")
|
||||
return None
|
||||
|
||||
from v1.feedback.report import generate_feedback_report
|
||||
|
||||
now = datetime.now(timezone.utc)
|
||||
push_hour = 10
|
||||
end_time = now.replace(hour=push_hour, minute=0, second=0, microsecond=0)
|
||||
@@ -192,6 +198,8 @@ async def generate_daily_feedback_report() -> str | None:
|
||||
|
||||
|
||||
async def generate_all_feedback_report() -> Path:
|
||||
from v1.feedback.report import generate_feedback_report
|
||||
|
||||
async with AsyncSessionLocal() as session:
|
||||
stmt = select(UserFeedback).order_by(UserFeedback.created_at.desc())
|
||||
result = await session.execute(stmt)
|
||||
|
||||
@@ -47,7 +47,6 @@ class AppleJwsVerifier:
|
||||
*,
|
||||
expected_bundle_id: str,
|
||||
expected_product_id: str,
|
||||
expected_environment: str,
|
||||
) -> VerifiedTransaction | VerificationError:
|
||||
try:
|
||||
unverified_header = jwt.get_unverified_header(signed_transaction_info)
|
||||
@@ -148,17 +147,6 @@ class AppleJwsVerifier:
|
||||
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: int | None = (
|
||||
int(revocation_date_raw) if revocation_date_raw is not None else None
|
||||
|
||||
@@ -114,12 +114,10 @@ class PaymentService:
|
||||
)
|
||||
|
||||
expected_bundle_id = config.apple_iap.bundle_id
|
||||
expected_environment = "Sandbox" if config.runtime.environment != "prod" else "Production"
|
||||
result = self._verifier.verify_signed_transaction(
|
||||
request.signed_transaction_info,
|
||||
expected_bundle_id=expected_bundle_id,
|
||||
expected_product_id=product_mapping.app_store_product_id,
|
||||
expected_environment=expected_environment,
|
||||
)
|
||||
|
||||
if isinstance(result, VerificationError):
|
||||
|
||||
@@ -82,10 +82,8 @@ class _FakeVerifier:
|
||||
*,
|
||||
expected_bundle_id: str,
|
||||
expected_product_id: str,
|
||||
expected_environment: str,
|
||||
) -> VerifiedTransaction | VerificationError:
|
||||
del signed_transaction_info, expected_bundle_id, expected_product_id
|
||||
del expected_environment
|
||||
return self._result
|
||||
|
||||
|
||||
@@ -290,6 +288,32 @@ class TestPaymentServiceSuccessfulGrant:
|
||||
assert len(points_repo.appended_ledger) == 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:
|
||||
@pytest.mark.asyncio
|
||||
|
||||
+42
-2
@@ -71,6 +71,24 @@ ERYAO_DEPLOY_BIND_HOST=127.0.0.1
|
||||
ERYAO_DEPLOY_BIND_HOST=0.0.0.0
|
||||
```
|
||||
|
||||
### 进程配置建议
|
||||
|
||||
生产 Compose 只启动一个 `worker-agent` 容器。Agent 任务、低频通用任务和反馈日报任务共用 `agent` 队列,不再单独常驻 `worker-general` 进程。
|
||||
|
||||
2 核 2G 机器建议使用:
|
||||
|
||||
```text
|
||||
ERYAO_WEB__WORKERS=1
|
||||
ERYAO_WORKER__GROUPS__AGENT__CONCURRENCY=2
|
||||
```
|
||||
|
||||
4G 以上机器可按流量提高 Web 或 Agent worker 数量:
|
||||
|
||||
```text
|
||||
ERYAO_WEB__WORKERS=2
|
||||
ERYAO_WORKER__GROUPS__AGENT__CONCURRENCY=2
|
||||
```
|
||||
|
||||
## 登录 ECR
|
||||
|
||||
进入部署目录,并把 `.env` 加载到当前 shell:
|
||||
@@ -128,13 +146,35 @@ cd deploy
|
||||
docker compose --env-file ./.env -f docker-compose.prod.yml --profile workers ps
|
||||
docker logs -f eryao-prod-backend
|
||||
docker logs -f eryao-prod-worker-agent
|
||||
docker logs -f eryao-prod-worker-general
|
||||
docker logs -f eryao-prod-redis
|
||||
```
|
||||
|
||||
## 更新版本
|
||||
|
||||
CI 推送新镜像到 ECR 后,在生产机器执行:
|
||||
当前 CI/CD 会在 `main` 分支构建后自动部署到生产机器:
|
||||
|
||||
- 推送前清空 ECR 仓库旧镜像,只保留新推送的 `latest`。
|
||||
- 通过 `DEPLOY_SSH_KEY` 登录生产机器。
|
||||
- 在生产机器执行 ECR 登录、`docker compose pull`、`docker compose up -d`。
|
||||
- 健康检查通过后清理 7 天前未使用的本地 Docker 镜像。
|
||||
|
||||
Gitea Secrets 需要包含:
|
||||
|
||||
```text
|
||||
AWS_ACCESS_KEY_ID
|
||||
AWS_SECRET_ACCESS_KEY
|
||||
AWS_REGION
|
||||
AWS_ACCOUNT_ID
|
||||
ECR_REPOSITORY
|
||||
DEPLOY_SSH_KEY
|
||||
DEPLOY_HOST
|
||||
DEPLOY_USER
|
||||
```
|
||||
|
||||
`DEPLOY_SSH_KEY` 是已加入生产机器 `ubuntu` 用户 `~/.ssh/authorized_keys` 的部署专用私钥。
|
||||
当前生产机器对应:`DEPLOY_HOST=18.218.38.213`,`DEPLOY_USER=ubuntu`。
|
||||
|
||||
如需手动更新,在生产机器执行:
|
||||
|
||||
```bash
|
||||
cd deploy
|
||||
|
||||
@@ -34,21 +34,7 @@ services:
|
||||
command:
|
||||
- sh
|
||||
- -c
|
||||
- exec taskiq worker core.taskiq.app:worker_agent_broker core.agentscope.runtime.tasks --workers ${ERYAO_WORKER__GROUPS__AGENT__CONCURRENCY:-2}
|
||||
|
||||
worker-general:
|
||||
<<: *backend-common
|
||||
container_name: eryao-prod-worker-general
|
||||
profiles: ["workers"]
|
||||
environment:
|
||||
ERYAO_RUNTIME__ENVIRONMENT: prod
|
||||
ERYAO_RUNTIME__SERVICE_NAME: worker-general
|
||||
ERYAO_REDIS__HOST: redis
|
||||
ERYAO_REDIS__PORT: 6379
|
||||
command:
|
||||
- sh
|
||||
- -c
|
||||
- exec taskiq worker core.taskiq.app:worker_general_broker core.agentscope.runtime.tasks v1.feedback.tasks --workers ${ERYAO_WORKER__GROUPS__GENERAL__CONCURRENCY:-1}
|
||||
- exec taskiq worker core.taskiq.app:worker_agent_broker core.agentscope.runtime.tasks v1.feedback.tasks --workers ${ERYAO_WORKER__GROUPS__AGENT__CONCURRENCY:-2}
|
||||
|
||||
redis:
|
||||
image: redis:7.4.2-alpine
|
||||
|
||||
@@ -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_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_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 |
|
||||
|
||||
@@ -14,6 +14,7 @@ Protocol verification status:
|
||||
|
||||
- Current strategy: additive evolution (`backward-compatible`).
|
||||
- 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
|
||||
|
||||
@@ -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_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_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 |
|
||||
@@ -119,6 +119,14 @@ product_mappings:
|
||||
- Backend tracks purchase via `register_bonus_claims.has_purchased_starter_pack`
|
||||
- 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
|
||||
|
||||
- Successful purchases create a ledger entry with:
|
||||
|
||||
@@ -153,14 +153,12 @@ start() {
|
||||
|
||||
WEB_CMD="cd '$ROOT_DIR' && PYTHONPATH=backend/src ERYAO_RUNTIME__SERVICE_NAME=web uv run uvicorn app:app --host ${ERYAO_WEB__HOST:-0.0.0.0} --port ${WEB_PORT} --workers ${ERYAO_WEB__WORKERS:-2} --log-level ${UVICORN_LOG_LEVEL}"
|
||||
|
||||
WORKER_AGENT_CMD="cd '$ROOT_DIR' && PYTHONPATH=backend/src ERYAO_RUNTIME__SERVICE_NAME=worker-agent uv run taskiq worker core.taskiq.app:worker_agent_broker core.agentscope.runtime.tasks --workers ${ERYAO_WORKER__GROUPS__AGENT__CONCURRENCY:-2}"
|
||||
WORKER_GENERAL_CMD="cd '$ROOT_DIR' && PYTHONPATH=backend/src ERYAO_RUNTIME__SERVICE_NAME=worker-general uv run taskiq worker core.taskiq.app:worker_general_broker core.agentscope.runtime.tasks v1.feedback.tasks --workers ${ERYAO_WORKER__GROUPS__GENERAL__CONCURRENCY:-1}"
|
||||
WORKER_AGENT_CMD="cd '$ROOT_DIR' && PYTHONPATH=backend/src ERYAO_RUNTIME__SERVICE_NAME=worker-agent uv run taskiq worker core.taskiq.app:worker_agent_broker core.agentscope.runtime.tasks v1.feedback.tasks --workers ${ERYAO_WORKER__GROUPS__AGENT__CONCURRENCY:-2}"
|
||||
|
||||
echo "Starting tmux web process in session '$SESSION_NAME'..."
|
||||
|
||||
tmux new-session -d -s "$SESSION_NAME" -n web "bash -lc \"$WEB_CMD; echo '[web] exited'; exec bash\""
|
||||
tmux new-window -t "$SESSION_NAME" -n worker-agent "bash -lc \"$WORKER_AGENT_CMD; echo '[worker-agent] exited'; exec bash\""
|
||||
tmux new-window -t "$SESSION_NAME" -n worker-general "bash -lc \"$WORKER_GENERAL_CMD; echo '[worker-general] exited'; exec bash\""
|
||||
|
||||
echo ""
|
||||
echo "=== App Started ==="
|
||||
@@ -170,7 +168,6 @@ start() {
|
||||
echo "Log files will be created in logs/ directory:"
|
||||
echo " - web.log, web.error.log"
|
||||
echo " - worker-agent.log, worker-agent.error.log"
|
||||
echo " - worker-general.log, worker-general.error.log"
|
||||
echo ""
|
||||
echo "tmux attach -t $SESSION_NAME"
|
||||
echo "tmux list-windows -t $SESSION_NAME"
|
||||
|
||||
Reference in New Issue
Block a user