chore: 迁移到 social-app 架构,集成 Supabase 和 taskiq worker
@@ -26,55 +26,44 @@ ERYAO_REDIS__PORT=6379
|
|||||||
ERYAO_REDIS__DB=0
|
ERYAO_REDIS__DB=0
|
||||||
|
|
||||||
############
|
############
|
||||||
# MySQL 数据库配置
|
# Worker 队列分组配置
|
||||||
|
############
|
||||||
|
# agent: 常规异步任务
|
||||||
|
ERYAO_WORKER__GROUPS__AGENT__CONCURRENCY=2
|
||||||
|
|
||||||
|
############
|
||||||
|
# Supabase 配置
|
||||||
|
############
|
||||||
|
ERYAO_SUPABASE__PUBLIC_URL=https://your-project.supabase.co
|
||||||
|
ERYAO_SUPABASE__ANON_KEY=
|
||||||
|
ERYAO_SUPABASE__SERVICE_ROLE_KEY=
|
||||||
|
ERYAO_SUPABASE__JWT_SECRET=
|
||||||
|
ERYAO_SUPABASE__JWT_ALGORITHM=HS256
|
||||||
|
|
||||||
|
############
|
||||||
|
# PostgreSQL 数据库配置(Supabase 本地开发)
|
||||||
############
|
############
|
||||||
ERYAO_DATABASE__HOST=localhost
|
ERYAO_DATABASE__HOST=localhost
|
||||||
ERYAO_DATABASE__PORT=3306
|
ERYAO_DATABASE__PORT=5432
|
||||||
ERYAO_DATABASE__NAME=eryao
|
ERYAO_DATABASE__NAME=eryao
|
||||||
ERYAO_DATABASE__USER=root
|
ERYAO_DATABASE__USER=postgres
|
||||||
ERYAO_DATABASE__PASSWORD=your_mysql_password_here
|
ERYAO_DATABASE__PASSWORD=change-me-strong-password
|
||||||
|
|
||||||
############
|
############
|
||||||
# 阿里云短信配置
|
# Storage 配置
|
||||||
############
|
############
|
||||||
ERYAO_ALIYUN_SMS__ACCESS_KEY_ID=your_aliyun_access_key_id
|
ERYAO_STORAGE__ATTACHMENT__BUCKET=agent-attachments
|
||||||
ERYAO_ALIYUN_SMS__ACCESS_KEY_SECRET=your_aliyun_access_key_secret
|
ERYAO_STORAGE__AVATAR__BUCKET=avatars
|
||||||
ERYAO_ALIYUN_SMS__SIGN_NAME=your_sign_name
|
ERYAO_STORAGE__SIGNED_URL_TTL_SECONDS=600
|
||||||
ERYAO_ALIYUN_SMS__TEMPLATE_CODE=your_template_code
|
ERYAO_STORAGE__ATTACHMENT__MAX_SIZE_MB=20
|
||||||
|
ERYAO_STORAGE__AVATAR__MAX_SIZE_MB=2
|
||||||
|
ERYAO_STORAGE__RETENTION_DAYS=30
|
||||||
|
|
||||||
############
|
############
|
||||||
# 阿里云内容安全配置
|
# LLM API KEY
|
||||||
############
|
############
|
||||||
ERYAO_ALIYUN_CONTENT_SECURITY__ACCESS_KEY_ID=your_aliyun_access_key_id
|
ERYAO_LLM__PROVIDER_KEYS__DASHSCOPE=
|
||||||
ERYAO_ALIYUN_CONTENT_SECURITY__ACCESS_KEY_SECRET=your_aliyun_access_key_secret
|
ERYAO_LLM__PROVIDER_KEYS__DEEPSEEK=
|
||||||
|
|
||||||
############
|
|
||||||
# 支付宝配置
|
|
||||||
############
|
|
||||||
ERYAO_ALIPAY__APP_ID=your_app_id
|
|
||||||
ERYAO_ALIPAY__MERCHANT_ID=your_merchant_id
|
|
||||||
ERYAO_ALIPAY__PUBLIC_KEY=your_alipay_public_key
|
|
||||||
ERYAO_ALIPAY__PRIVATE_KEY=your_alipay_private_key
|
|
||||||
ERYAO_ALIPAY__NOTIFY_URL=https://your-domain.com/api/payment/notify
|
|
||||||
ERYAO_ALIPAY__SANDBOX=false
|
|
||||||
|
|
||||||
############
|
|
||||||
# DeepSeek API 配置
|
|
||||||
############
|
|
||||||
ERYAO_DEEPSEEK__API_KEY=your_deepseek_api_key
|
|
||||||
|
|
||||||
############
|
|
||||||
# 认证配置
|
|
||||||
############
|
|
||||||
ERYAO_AUTH__TOKEN_EXPIRATION_DAYS=7
|
|
||||||
ERYAO_AUTH__TOKEN_REFRESH_THRESHOLD_HOURS=2
|
|
||||||
|
|
||||||
############
|
|
||||||
# 验证码配置
|
|
||||||
############
|
|
||||||
ERYAO_VERIFICATION__CODE_LENGTH=6
|
|
||||||
ERYAO_VERIFICATION__EXPIRATION_MINUTES=5
|
|
||||||
ERYAO_VERIFICATION__TEST_MODE=false
|
|
||||||
|
|
||||||
############
|
############
|
||||||
# 敏感词配置
|
# 敏感词配置
|
||||||
@@ -82,14 +71,13 @@ ERYAO_VERIFICATION__TEST_MODE=false
|
|||||||
ERYAO_SENSITIVE_WORD__USE_ALIYUN=true
|
ERYAO_SENSITIVE_WORD__USE_ALIYUN=true
|
||||||
ERYAO_SENSITIVE_WORD__FALLBACK_TO_LOCAL=true
|
ERYAO_SENSITIVE_WORD__FALLBACK_TO_LOCAL=true
|
||||||
|
|
||||||
############
|
|
||||||
# App 版本更新配置
|
|
||||||
############
|
|
||||||
ERYAO_APP_VERSION__MANIFEST_PATH=deploy/static/releases/manifest.json
|
|
||||||
ERYAO_APP_VERSION__RELEASE_PATH_PREFIX=releases
|
|
||||||
ERYAO_APP_VERSION__DOWNLOAD_BASE_URL=
|
|
||||||
|
|
||||||
############
|
############
|
||||||
# CORS 配置
|
# CORS 配置
|
||||||
############
|
############
|
||||||
ERYAO_CORS__ALLOW_ORIGINS=["http://localhost", "http://localhost:3000"]
|
ERYAO_CORS__ALLOW_ORIGINS=["http://localhost", "http://localhost:3000"]
|
||||||
|
|
||||||
|
############
|
||||||
|
# Test相关
|
||||||
|
############
|
||||||
|
ERYAO_TEST__PHONE=8613812345678
|
||||||
|
ERYAO_TEST__PASSWORD=Test@123456
|
||||||
|
|||||||
@@ -1,71 +1,198 @@
|
|||||||
# ============================================
|
# Byte-compiled / optimized / DLL files
|
||||||
# Environment & Secrets
|
|
||||||
# ============================================
|
|
||||||
.env
|
|
||||||
.env.local
|
|
||||||
.env.*.local
|
|
||||||
!.env.example
|
|
||||||
|
|
||||||
# ============================================
|
|
||||||
# Python
|
|
||||||
# ============================================
|
|
||||||
__pycache__/
|
__pycache__/
|
||||||
*.py[codz]
|
*.py[codz]
|
||||||
*$py.class
|
*$py.class
|
||||||
|
|
||||||
|
# C extensions
|
||||||
*.so
|
*.so
|
||||||
|
|
||||||
|
# Distribution / packaging
|
||||||
.Python
|
.Python
|
||||||
build/
|
build/
|
||||||
|
develop-eggs/
|
||||||
dist/
|
dist/
|
||||||
|
downloads/
|
||||||
|
eggs/
|
||||||
|
.eggs/
|
||||||
|
/lib/
|
||||||
|
/lib64/
|
||||||
|
!apps/lib/
|
||||||
|
parts/
|
||||||
|
sdist/
|
||||||
|
var/
|
||||||
|
wheels/
|
||||||
|
share/python-wheels/
|
||||||
*.egg-info/
|
*.egg-info/
|
||||||
|
.installed.cfg
|
||||||
*.egg
|
*.egg
|
||||||
MANIFEST
|
MANIFEST
|
||||||
|
|
||||||
|
# PyInstaller
|
||||||
|
*.manifest
|
||||||
|
*.spec
|
||||||
|
|
||||||
|
# Installer logs
|
||||||
|
pip-log.txt
|
||||||
|
pip-delete-this-directory.txt
|
||||||
|
|
||||||
|
# Unit test / coverage reports
|
||||||
|
htmlcov/
|
||||||
.tox/
|
.tox/
|
||||||
.nox/
|
.nox/
|
||||||
.coverage
|
.coverage
|
||||||
.coverage.*
|
.coverage.*
|
||||||
|
.cache
|
||||||
|
nosetests.xml
|
||||||
|
coverage.xml
|
||||||
|
*.cover
|
||||||
|
*.py.cover
|
||||||
|
.hypothesis/
|
||||||
.pytest_cache/
|
.pytest_cache/
|
||||||
htmlcov/
|
cover/
|
||||||
|
|
||||||
|
# Translations
|
||||||
|
*.mo
|
||||||
|
*.pot
|
||||||
|
|
||||||
|
# Django stuff:
|
||||||
|
*.log
|
||||||
|
local_settings.py
|
||||||
|
db.sqlite3
|
||||||
|
db.sqlite3-journal
|
||||||
|
|
||||||
|
# Flask stuff:
|
||||||
|
instance/
|
||||||
|
.webassets-cache
|
||||||
|
|
||||||
|
# Scrapy stuff:
|
||||||
|
.scrapy
|
||||||
|
|
||||||
|
# Sphinx documentation
|
||||||
|
docs/_build/
|
||||||
|
|
||||||
|
# PyBuilder
|
||||||
|
.pybuilder/
|
||||||
|
target/
|
||||||
|
|
||||||
|
# Jupyter Notebook
|
||||||
|
.ipynb_checkpoints
|
||||||
|
|
||||||
|
# IPython
|
||||||
|
profile_default/
|
||||||
|
ipython_config.py
|
||||||
|
|
||||||
|
# pyenv
|
||||||
|
.python-version
|
||||||
|
|
||||||
|
# pipenv
|
||||||
|
# Pipfile.lock
|
||||||
|
|
||||||
|
# UV
|
||||||
|
# uv.lock
|
||||||
|
|
||||||
|
# poetry
|
||||||
|
# poetry.lock
|
||||||
|
# poetry.toml
|
||||||
|
|
||||||
|
# pdm
|
||||||
|
# pdm.lock
|
||||||
|
# pdm.toml
|
||||||
|
.pdm-python
|
||||||
|
.pdm-build/
|
||||||
|
|
||||||
|
# pixi
|
||||||
|
# pixi.lock
|
||||||
|
.pixi
|
||||||
|
|
||||||
|
# PEP 582
|
||||||
|
__pypackages__/
|
||||||
|
|
||||||
|
# Celery stuff
|
||||||
|
celerybeat-schedule
|
||||||
|
celerybeat.pid
|
||||||
|
|
||||||
|
# Redis
|
||||||
|
*.rdb
|
||||||
|
*.aof
|
||||||
|
*.pid
|
||||||
|
|
||||||
|
# RabbitMQ
|
||||||
|
mnesia/
|
||||||
|
rabbitmq/
|
||||||
|
rabbitmq-data/
|
||||||
|
|
||||||
|
# ActiveMQ
|
||||||
|
activemq-data/
|
||||||
|
|
||||||
|
# SageMath parsed files
|
||||||
|
*.sage.py
|
||||||
|
|
||||||
|
# Environments
|
||||||
.env
|
.env
|
||||||
.envrc
|
.envrc
|
||||||
.venv
|
.venv
|
||||||
|
env/
|
||||||
venv/
|
venv/
|
||||||
ENV/
|
ENV/
|
||||||
env.bak/
|
env.bak/
|
||||||
venv.bak/
|
venv.bak/
|
||||||
|
|
||||||
|
# Spyder project settings
|
||||||
|
.spyderproject
|
||||||
|
.spyproject
|
||||||
|
|
||||||
|
# Rope project settings
|
||||||
|
.ropeproject
|
||||||
|
|
||||||
|
# mkdocs documentation
|
||||||
|
/site
|
||||||
|
|
||||||
|
# mypy
|
||||||
.mypy_cache/
|
.mypy_cache/
|
||||||
.dmypy.json
|
.dmypy.json
|
||||||
dmypy.json
|
dmypy.json
|
||||||
.pyre/
|
|
||||||
.pytype/
|
|
||||||
*.log
|
|
||||||
db.sqlite3
|
|
||||||
|
|
||||||
# ============================================
|
# Pyre type checker
|
||||||
# Flutter
|
.pyre/
|
||||||
# ============================================
|
|
||||||
/bin/cache/
|
# pytype static type analyzer
|
||||||
/bin/internal/
|
.pytype/
|
||||||
/dev/benchmarks/
|
|
||||||
/dev/bots/
|
# Cython debug symbols
|
||||||
/dev/docs/
|
cython_debug/
|
||||||
/dev/integration_tests/**/xcuserdata
|
|
||||||
/dev/integration_tests/**/Pods
|
# Ruff stuff:
|
||||||
/packages/flutter/coverage/
|
.ruff_cache/
|
||||||
version
|
|
||||||
analysis_benchmark.json
|
# PyPI configuration file
|
||||||
.packages.generated
|
.pypirc
|
||||||
|
|
||||||
|
# Marimo
|
||||||
|
marimo/_static/
|
||||||
|
marimo/_lsp/
|
||||||
|
__marimo__/
|
||||||
|
|
||||||
|
# Streamlit
|
||||||
|
.streamlit/secrets.toml
|
||||||
|
|
||||||
|
# Flutter/Dart/Pub related
|
||||||
**/doc/api/
|
**/doc/api/
|
||||||
.dart_tool/
|
.dart_tool/
|
||||||
.flutter-plugins
|
.dart_tool/flutter_build/
|
||||||
.flutter-plugins-dependencies
|
|
||||||
**/generated_plugin_registrant.dart
|
**/generated_plugin_registrant.dart
|
||||||
.packages
|
.packages
|
||||||
.pub-preload-cache/
|
.pub-preload-cache/
|
||||||
.pub/
|
.pub/
|
||||||
build/
|
build/
|
||||||
flutter_*.png
|
flutter_*.png
|
||||||
|
linked_*.ds
|
||||||
|
unlinked.ds
|
||||||
|
unlinked_spec.ds
|
||||||
|
|
||||||
# Android
|
# IDE
|
||||||
|
.idea/
|
||||||
|
|
||||||
|
# Android related
|
||||||
**/android/**/gradle-wrapper.jar
|
**/android/**/gradle-wrapper.jar
|
||||||
.gradle/
|
.gradle/
|
||||||
**/android/captures/
|
**/android/captures/
|
||||||
@@ -75,8 +202,9 @@ flutter_*.png
|
|||||||
**/android/**/GeneratedPluginRegistrant.java
|
**/android/**/GeneratedPluginRegistrant.java
|
||||||
**/android/key.properties
|
**/android/key.properties
|
||||||
*.jks
|
*.jks
|
||||||
|
**/android/**/*.iml
|
||||||
|
|
||||||
# iOS/XCode
|
# iOS/XCode related
|
||||||
**/ios/**/*.mode1v3
|
**/ios/**/*.mode1v3
|
||||||
**/ios/**/*.mode2v3
|
**/ios/**/*.mode2v3
|
||||||
**/ios/**/*.moved-aside
|
**/ios/**/*.moved-aside
|
||||||
@@ -102,8 +230,12 @@ flutter_*.png
|
|||||||
**/ios/Flutter/app.flx
|
**/ios/Flutter/app.flx
|
||||||
**/ios/Flutter/app.zip
|
**/ios/Flutter/app.zip
|
||||||
**/ios/Flutter/flutter_assets/
|
**/ios/Flutter/flutter_assets/
|
||||||
|
**/ios/Flutter/flutter_export_environment.sh
|
||||||
**/ios/ServiceDefinitions.json
|
**/ios/ServiceDefinitions.json
|
||||||
**/ios/Runner/GeneratedPluginRegistrant.*
|
**/ios/Runner/GeneratedPluginRegistrant.*
|
||||||
|
**/ios/Podfile.lock
|
||||||
|
**/ios/Runner.xcodeproj/
|
||||||
|
**/ios/Runner.xcworkspace/
|
||||||
|
|
||||||
# macOS
|
# macOS
|
||||||
**/Flutter/ephemeral/
|
**/Flutter/ephemeral/
|
||||||
@@ -112,78 +244,92 @@ flutter_*.png
|
|||||||
**/macos/Flutter/ephemeral
|
**/macos/Flutter/ephemeral
|
||||||
**/xcuserdata/
|
**/xcuserdata/
|
||||||
|
|
||||||
|
# Linux
|
||||||
|
**/linux/flutter/generated_plugin_registrant.cc
|
||||||
|
**/linux/flutter/generated_plugin_registrant.h
|
||||||
|
**/linux/flutter/generated_plugins.cmake
|
||||||
|
|
||||||
|
# Windows
|
||||||
|
**/windows/flutter/generated_plugin_registrant.cc
|
||||||
|
**/windows/flutter/generated_plugin_registrant.h
|
||||||
|
**/windows/flutter/generated_plugins.cmake
|
||||||
|
**/windows/runner/
|
||||||
|
|
||||||
|
# Linux
|
||||||
|
**/linux/flutter/generated_plugin_registrant.cc
|
||||||
|
**/linux/flutter/generated_plugin_registrant.h
|
||||||
|
**/linux/flutter/generated_plugins.cmake
|
||||||
|
|
||||||
# Coverage
|
# Coverage
|
||||||
coverage/
|
coverage/
|
||||||
|
|
||||||
# ============================================
|
# Symbols
|
||||||
# Kotlin / Gradle / Android
|
app.*.symbols
|
||||||
# ============================================
|
|
||||||
|
# Exceptions to above rules.
|
||||||
|
!**/ios/**/default.mode1v3
|
||||||
|
!**/ios/**/default.mode2v3
|
||||||
|
!**/ios/**/default.pbxuser
|
||||||
|
!**/ios/**/default.perspectivev3
|
||||||
|
!/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages
|
||||||
|
!/dev/ci/**/Gemfile.lock
|
||||||
|
|
||||||
|
# Local environment files
|
||||||
|
infra/local/env/*.env
|
||||||
|
configs/env/*.env
|
||||||
|
infra/cloud/volcano/env/*.env
|
||||||
|
!infra/local/env/*.env.example
|
||||||
|
!configs/env/*.env.example
|
||||||
|
!infra/cloud/volcano/env/*.env.example
|
||||||
|
.env.local
|
||||||
|
.env.*.local
|
||||||
|
.env.cloud
|
||||||
|
.env.*.cloud
|
||||||
|
deploy/.env.prod
|
||||||
|
|
||||||
|
# Misc
|
||||||
*.class
|
*.class
|
||||||
*.log
|
|
||||||
*.lock
|
*.lock
|
||||||
|
*.swp
|
||||||
.buildlog/
|
.buildlog/
|
||||||
.history
|
.history
|
||||||
build/
|
/logs/
|
||||||
app/build/
|
backend/logs/
|
||||||
login-service/build/
|
backend/data/analytics/
|
||||||
.gradle/
|
*.tar.gz
|
||||||
.idea/
|
*.tar
|
||||||
!.idea/codeStyles/
|
# Docker volumes (local data)
|
||||||
*.iml
|
docker/supabase/volumes/db/data/
|
||||||
out/
|
infra/docker/volumes/db/data/
|
||||||
*.apk
|
infra/docker/supabase/volumes/db/data/
|
||||||
*.aab
|
infra/docker/supabase/volumes/storage/
|
||||||
*.dex
|
|
||||||
|
|
||||||
# ============================================
|
# OpenCode local config
|
||||||
# Node.js
|
# .opencode/ is now tracked - see .opencode/.gitignore for exclusions
|
||||||
# ============================================
|
|
||||||
node_modules/
|
|
||||||
npm-debug.log*
|
|
||||||
yarn-debug.log*
|
|
||||||
yarn-error.log*
|
|
||||||
package-lock.json
|
|
||||||
yarn.lock
|
|
||||||
|
|
||||||
# ============================================
|
# Agents and skills
|
||||||
# Java / Spring Boot
|
.agents/
|
||||||
# ============================================
|
|
||||||
target/
|
|
||||||
*.class
|
|
||||||
*.jar
|
|
||||||
*.war
|
|
||||||
*.ear
|
|
||||||
hs_err_pid*
|
|
||||||
spring-boot-*.jar
|
|
||||||
|
|
||||||
# ============================================
|
# Local git worktrees
|
||||||
# IDE
|
.worktrees/
|
||||||
# ============================================
|
worktrees/
|
||||||
.vscode/
|
|
||||||
*.swp
|
# Runtime temp files
|
||||||
*.swo
|
.tmp/
|
||||||
*~
|
|
||||||
|
# macOS system files
|
||||||
.DS_Store
|
.DS_Store
|
||||||
Thumbs.db
|
**/.DS_Store
|
||||||
*.sublime-*
|
|
||||||
.idea/
|
|
||||||
*.iml
|
|
||||||
atlassian-ide-plugin.xml
|
|
||||||
.project
|
|
||||||
.classpath
|
|
||||||
.settings/
|
|
||||||
|
|
||||||
# ============================================
|
# Deploy releases (APK files only, keep manifest.json)
|
||||||
# Misc
|
deploy/static/releases/*.apk
|
||||||
# ============================================
|
deploy/static/releases/*.ipa
|
||||||
*.pid
|
|
||||||
*.seed
|
|
||||||
*.pid.lock
|
|
||||||
*.rdb
|
|
||||||
*.aof
|
|
||||||
*.pid
|
|
||||||
|
|
||||||
# ============================================
|
# Superset
|
||||||
# Local folders
|
.superset/
|
||||||
# ============================================
|
|
||||||
|
# Local agents and skills
|
||||||
|
.agents/
|
||||||
|
|
||||||
|
# Old legacy code
|
||||||
old/
|
old/
|
||||||
|
|||||||
@@ -0,0 +1,77 @@
|
|||||||
|
---
|
||||||
|
description: 审查并更新 docs/protocols,确保与当前代码实现一致
|
||||||
|
---
|
||||||
|
|
||||||
|
你现在要执行一次“协议文档一致性审查与更新”,目标是让 `docs/protocols/` 成为项目协议与数据格式的最新事实来源,并与当前代码实现保持一致。
|
||||||
|
|
||||||
|
## 执行目标
|
||||||
|
|
||||||
|
1. 审查 `docs/protocols/` 下所有协议文档是否过期、缺失或与实现不一致。
|
||||||
|
2. 在发现差异时,优先更新协议文档(而不是先改代码),明确兼容策略。
|
||||||
|
3. 输出结构化审查结果:发现的问题、已更新内容、仍待确认项。
|
||||||
|
|
||||||
|
## 约束
|
||||||
|
|
||||||
|
- 审查范围优先限定在:`docs/protocols/**` 与本次协议相关代码(`backend/**`、`apps/**`)。
|
||||||
|
- 不做无关重构,不改动与协议无关模块。
|
||||||
|
- 禁止“吞错式”描述:若不确定,明确标记为待确认,不要假设正确。
|
||||||
|
- 若涉及破坏性变更,必须在文档中写明迁移与回滚策略。
|
||||||
|
|
||||||
|
## 步骤
|
||||||
|
|
||||||
|
1. **建立协议清单**
|
||||||
|
- 列出 `docs/protocols/` 中所有文档。
|
||||||
|
- 为每份文档提取:涉及的接口、事件、字段、枚举、状态码、错误结构。
|
||||||
|
|
||||||
|
2. **建立实现映射**
|
||||||
|
- 在 `backend/**`、`apps/**` 中定位对应实现与调用点。
|
||||||
|
- 对每个协议项建立“文档 -> 实现”映射(文件路径 + 关键符号/接口名)。
|
||||||
|
|
||||||
|
3. **逐项比对并分级**
|
||||||
|
- 比对以下维度:
|
||||||
|
- 请求/响应结构与字段可选性
|
||||||
|
- 字段命名、类型、默认值、约束
|
||||||
|
- 错误码/错误体格式
|
||||||
|
- 版本号、兼容说明、废弃说明
|
||||||
|
- 给每个差异打级别:
|
||||||
|
- CRITICAL:会导致客户端/服务端不兼容
|
||||||
|
- HIGH:行为偏差明显,容易引发线上错误
|
||||||
|
- MEDIUM:文档缺失或描述不完整
|
||||||
|
- LOW:措辞、示例、格式问题
|
||||||
|
|
||||||
|
4. **更新文档(优先)**
|
||||||
|
- 先更新 `docs/protocols/**`,使其反映当前真实实现。
|
||||||
|
- 每处更新都要补充“兼容策略”:
|
||||||
|
- `backward-compatible`(向后兼容)或
|
||||||
|
- `requires-migration`(需要迁移)
|
||||||
|
- 如为 `requires-migration`,补充迁移步骤与回滚注意事项。
|
||||||
|
|
||||||
|
5. **一致性复核**
|
||||||
|
- 复查所有变更是否与实现一致。
|
||||||
|
- 如仓库有协议相关测试/校验脚本,执行最小必要验证。
|
||||||
|
|
||||||
|
## 输出格式(必须)
|
||||||
|
|
||||||
|
1. **Protocol Audit Scope**
|
||||||
|
- 本次审查的文档列表
|
||||||
|
- 对应实现文件映射
|
||||||
|
|
||||||
|
2. **Findings**
|
||||||
|
- 按 CRITICAL/HIGH/MEDIUM/LOW 分组列出差异
|
||||||
|
- 每条包含:文档位置、实现位置、差异说明、影响范围
|
||||||
|
|
||||||
|
3. **Doc Updates Applied**
|
||||||
|
- 列出已更新的文档与关键修改点
|
||||||
|
- 标注每项兼容策略(`backward-compatible` / `requires-migration`)
|
||||||
|
|
||||||
|
4. **Open Questions**
|
||||||
|
- 仍需产品/后端/前端确认的点
|
||||||
|
|
||||||
|
5. **Verification**
|
||||||
|
- 列出执行的验证命令与结果(通过/失败)
|
||||||
|
|
||||||
|
## 完成标准
|
||||||
|
|
||||||
|
- `docs/protocols/` 覆盖当前实现的真实协议。
|
||||||
|
- 所有发现的关键差异(CRITICAL/HIGH)要么已修正文档,要么被明确记录为阻塞项。
|
||||||
|
- 输出报告完整,后续协作者可据此继续推进。
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://opencode.ai/config.json",
|
||||||
|
"mcp": {
|
||||||
|
"supabase": {
|
||||||
|
"type": "local",
|
||||||
|
"enabled": true,
|
||||||
|
"command": [
|
||||||
|
"npx",
|
||||||
|
"-y",
|
||||||
|
"@aliyun-rds/supabase-mcp-server",
|
||||||
|
"--supabase-url",
|
||||||
|
"http://47.112.66.83",
|
||||||
|
"--supabase-anon-key",
|
||||||
|
"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJvbGUiOiJhbm9uIiwiaWF0IjoxNzczMDI3NDE5LCJleHAiOjEzMjgzNjY3NDE5fQ.NVXDla5_nYPdcJk_81fc3k1UrnNTrNne_trMqt6Hg4g",
|
||||||
|
"--supabase-service-role-key",
|
||||||
|
"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJvbGUiOiJzZXJ2aWNlX3JvbGUiLCJpYXQiOjE3NzMwMjc0MTksImV4cCI6MTMyODM2Njc0MTl9.RzQBia-3QcjupsHnqaxgDWB7wnY9R7Ms9R8pMokyvLY"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -37,3 +37,7 @@ Do not place backend/frontend implementation details here.
|
|||||||
- Update protocol docs before changing data/API/UI contracts.
|
- Update protocol docs before changing data/API/UI contracts.
|
||||||
- Document compatibility strategy (backward-compatible vs migration).
|
- Document compatibility strategy (backward-compatible vs migration).
|
||||||
- Keep frontend/backend implementations aligned with documented protocol.
|
- Keep frontend/backend implementations aligned with documented protocol.
|
||||||
|
|
||||||
|
## Database Access
|
||||||
|
|
||||||
|
When viewing data in the database, use `supabase mcp` tools (`supabase_execute_sql`, `supabase_list_tables`, etc.) instead of direct queries or other methods.
|
||||||
|
|||||||
@@ -0,0 +1,45 @@
|
|||||||
|
# Miscellaneous
|
||||||
|
*.class
|
||||||
|
*.log
|
||||||
|
*.pyc
|
||||||
|
*.swp
|
||||||
|
.DS_Store
|
||||||
|
.atom/
|
||||||
|
.build/
|
||||||
|
.buildlog/
|
||||||
|
.history
|
||||||
|
.svn/
|
||||||
|
.swiftpm/
|
||||||
|
migrate_working_dir/
|
||||||
|
|
||||||
|
# IntelliJ related
|
||||||
|
*.iml
|
||||||
|
*.ipr
|
||||||
|
*.iws
|
||||||
|
.idea/
|
||||||
|
|
||||||
|
# The .vscode folder contains launch configuration and tasks you configure in
|
||||||
|
# VS Code which you may wish to be included in version control, so this line
|
||||||
|
# is commented out by default.
|
||||||
|
#.vscode/
|
||||||
|
|
||||||
|
# Flutter/Dart/Pub related
|
||||||
|
**/doc/api/
|
||||||
|
**/ios/Flutter/.last_build_id
|
||||||
|
.dart_tool/
|
||||||
|
.flutter-plugins-dependencies
|
||||||
|
.pub-cache/
|
||||||
|
.pub/
|
||||||
|
/build/
|
||||||
|
/coverage/
|
||||||
|
|
||||||
|
# Symbolication related
|
||||||
|
app.*.symbols
|
||||||
|
|
||||||
|
# Obfuscation related
|
||||||
|
app.*.map.json
|
||||||
|
|
||||||
|
# Android Studio will place build artifacts here
|
||||||
|
/android/app/debug
|
||||||
|
/android/app/profile
|
||||||
|
/android/app/release
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
# This file tracks properties of this Flutter project.
|
||||||
|
# Used by Flutter tool to assess capabilities and perform upgrades etc.
|
||||||
|
#
|
||||||
|
# This file should be version controlled and should not be manually edited.
|
||||||
|
|
||||||
|
version:
|
||||||
|
revision: "3b62efc2a3da49882f43c372e0bc53daef7295a6"
|
||||||
|
channel: "stable"
|
||||||
|
|
||||||
|
project_type: app
|
||||||
|
|
||||||
|
# Tracks metadata for the flutter migrate command
|
||||||
|
migration:
|
||||||
|
platforms:
|
||||||
|
- platform: root
|
||||||
|
create_revision: 3b62efc2a3da49882f43c372e0bc53daef7295a6
|
||||||
|
base_revision: 3b62efc2a3da49882f43c372e0bc53daef7295a6
|
||||||
|
- platform: android
|
||||||
|
create_revision: 3b62efc2a3da49882f43c372e0bc53daef7295a6
|
||||||
|
base_revision: 3b62efc2a3da49882f43c372e0bc53daef7295a6
|
||||||
|
- platform: ios
|
||||||
|
create_revision: 3b62efc2a3da49882f43c372e0bc53daef7295a6
|
||||||
|
base_revision: 3b62efc2a3da49882f43c372e0bc53daef7295a6
|
||||||
|
|
||||||
|
# User provided section
|
||||||
|
|
||||||
|
# List of Local paths (relative to this file) that should be
|
||||||
|
# ignored by the migrate tool.
|
||||||
|
#
|
||||||
|
# Files that are not part of the templates will be ignored by default.
|
||||||
|
unmanaged_files:
|
||||||
|
- 'lib/main.dart'
|
||||||
|
- 'ios/Runner.xcodeproj/project.pbxproj'
|
||||||
@@ -0,0 +1,201 @@
|
|||||||
|
# Apps Domain Rules
|
||||||
|
|
||||||
|
This file governs `apps/**` (Flutter). Keep rules strict, short, and reusable.
|
||||||
|
|
||||||
|
## Scope & Precedence
|
||||||
|
|
||||||
|
- Inherits root `AGENTS.md` and workspace runtime rules.
|
||||||
|
- If rules conflict, apply the stricter one.
|
||||||
|
- Visual language source of truth: `apps/rules/visual_design_language.md`.
|
||||||
|
|
||||||
|
## Flutter Directory Contract (Must)
|
||||||
|
|
||||||
|
- `apps/lib` only allows these second-level directories: `app/`, `core/`, `data/`, `features/`, `shared/`, `l10n/`.
|
||||||
|
- `apps/lib/main.dart` is the only allowed root entry file.
|
||||||
|
- Do not add new second-level directories under `apps/lib` without explicit approval.
|
||||||
|
|
||||||
|
## Module Responsibilities (Must)
|
||||||
|
|
||||||
|
- `app/`: app bootstrap, DI wiring, global lifecycle orchestration, router composition.
|
||||||
|
- `core/`: cross-feature business primitives/protocols/orchestrators (no feature-specific page logic).
|
||||||
|
- `data/`: shared infrastructure only (cache/network/storage/adapters), not feature business repositories/models.
|
||||||
|
- `features/`: user-facing bounded feature modules with clear product ownership.
|
||||||
|
- `shared/`: reusable UI widgets and presentation helpers without feature business orchestration.
|
||||||
|
- Cross-cutting capabilities (e.g. notification orchestration, UI schema protocol) must live in `core/` + `shared/`, not under `features/`.
|
||||||
|
|
||||||
|
## Placement Rules (Must)
|
||||||
|
|
||||||
|
- Put code in `features/` only when it belongs to one bounded product capability/screen flow.
|
||||||
|
- Put code in `core/` when it is cross-feature protocol, policy, or orchestration that does not belong to one feature.
|
||||||
|
- Put reusable UI renderers in `shared/widgets/`; they must not contain feature-only business orchestration.
|
||||||
|
- In feature data layers, use semantic subfolders: `data/apis/`, `data/repositories/`, `data/services/`, `data/models/`.
|
||||||
|
- Avoid deep redundant nesting like `models/<same_name>/...`; prefer flat by concern.
|
||||||
|
|
||||||
|
## Shared Data Layer Boundary (Must)
|
||||||
|
|
||||||
|
- Do not place feature business repositories/models under `apps/lib/data/`.
|
||||||
|
- Feature business repositories/models must live under each feature's `data/` tree.
|
||||||
|
- `apps/lib/data/` is only for infrastructure abstractions and implementations (cache/network/storage), reusable by features.
|
||||||
|
|
||||||
|
## UI Design System (Must)
|
||||||
|
|
||||||
|
- **Semantic colors**: always use `Theme.of(context).colorScheme.*` (primary, surface, error, etc.). Never hardcode hex or `Colors.*`.
|
||||||
|
- **Brand palette colors** (event presets, avatar colors, Eisenhower matrix quadrants): use `Theme.of(context).extension<AppColorPalette>()!.*`.
|
||||||
|
- **Spacing / Radius**: use `AppSpacing` / `AppRadius` from `design_tokens.dart`. No hardcoded values.
|
||||||
|
- `AppTheme.light` / `AppTheme.dark` provide complete `ColorScheme` (light + dark). `MaterialApp` wires them via `theme:` / `darkTheme:`.
|
||||||
|
- If a semantic slot is missing from `ColorScheme`, add it to `AppTheme` — do not bypass `colorScheme` with hardcoded values.
|
||||||
|
|
||||||
|
## Reuse & Composition (Must)
|
||||||
|
|
||||||
|
- Prefer `apps/lib/shared/widgets/` before adding new components.
|
||||||
|
- Extract repeated page structures/components; do not duplicate sibling-page scaffolds.
|
||||||
|
- Detail page top-right actions must use shared action-menu components.
|
||||||
|
- Destructive confirmations must use project-consistent shared surfaces.
|
||||||
|
|
||||||
|
## Interaction & Feedback (Must)
|
||||||
|
|
||||||
|
- User feedback: `Toast` / `AppBanner` only.
|
||||||
|
- Loading indicators: `AppLoadingIndicator` only.
|
||||||
|
- Form pages should default to keyboard-overlay behavior to avoid full-page layout jumps.
|
||||||
|
|
||||||
|
## Interaction & Feedback (Must)
|
||||||
|
|
||||||
|
## Agent Chat Protocol (Must)
|
||||||
|
|
||||||
|
- Agent chat must follow AG-UI over SSE.
|
||||||
|
- Lifecycle events are mandatory: `RUN_STARTED` and exactly one of `RUN_FINISHED` or `RUN_ERROR`.
|
||||||
|
- Current default text delivery is finalized `TEXT_MESSAGE_END` payloads; do not require token-level `TEXT_MESSAGE_CONTENT` unless backend protocol explicitly enables it.
|
||||||
|
|
||||||
|
## HTTP Error Parse Contract (Must)
|
||||||
|
|
||||||
|
- Frontend must parse backend errors as RFC7807: `type/title/status/detail/instance` + extension `code/params`.
|
||||||
|
- Error code registry single source of truth: `docs/protocols/common/http-error-codes.md`.
|
||||||
|
- Frontend mapping must be based on documented `code` only (`code -> l10n key`), not inferred from `detail` text.
|
||||||
|
- Any new/changed code requires protocol doc update first, then frontend mapping update.
|
||||||
|
- Unknown code fallback order: status-generic localized message -> safe generic localized message.
|
||||||
|
|
||||||
|
## High-Risk Modules (Must)
|
||||||
|
|
||||||
|
### Auth
|
||||||
|
|
||||||
|
- `AuthBloc` is the single source of truth.
|
||||||
|
- 401 invalidation must go through global callback chain; no feature-level token clearing or direct login navigation.
|
||||||
|
|
||||||
|
### Home Message Viewport
|
||||||
|
|
||||||
|
- Home message auto-scroll/anchor restore must be event-driven.
|
||||||
|
- Preserve viewport during history prepend and when user is reading above bottom.
|
||||||
|
|
||||||
|
### Cache / Repository
|
||||||
|
|
||||||
|
- Reads/writes that affect consistency must go through repository layer.
|
||||||
|
- Cache keys and invalidation policy belong to repository, not UI/Bloc.
|
||||||
|
- Shared cache infrastructure must live under `apps/lib/data/cache/`; feature modules must not duplicate low-level cache store logic.
|
||||||
|
- Shared cache infrastructure (`apps/lib/data/cache/`) must remain domain-agnostic: do not import `features/**` or business model DTOs there.
|
||||||
|
- Domain object serialization/deserialization belongs to repository/feature layer via local mappers/codecs; do not centralize feature-specific codecs in shared cache layer.
|
||||||
|
- Shared cache layer may only encode/decode primitives, collections, and cache metadata wrappers.
|
||||||
|
- Cache strategy default is `SWR + TTL + invalidation/reload`.
|
||||||
|
- Local partial cache patching is allowed only for simple single-entity updates with clear rollback paths; complex cross-list/cross-feature states must invalidate and refetch.
|
||||||
|
- Feature TTL policy must be defined in each feature repository; do not add centralized feature TTL registries in shared cache infra.
|
||||||
|
- Runtime cache is hybrid (`memory + local persistent`) managed by DI singletons; do not create per-screen/per-widget cache store instances.
|
||||||
|
- Cross-feature data access must go through app-level facade/usecase boundaries; do not import another feature's data implementation directly from UI/Bloc.
|
||||||
|
- Repository instances should be resolved from DI singletons to reuse cache and avoid per-feature re-creation.
|
||||||
|
|
||||||
|
### Reminder / Notification Rewrite Boundary
|
||||||
|
|
||||||
|
- Reminder/notification data-interaction logic is under rewrite. Do not reintroduce local-notification scheduling/callback execution paths in `apps/lib/data/services/`.
|
||||||
|
- During rewrite, keep protocol/orchestration in `core/notification/**` and reusable rendering in `shared/widgets/notification/**`.
|
||||||
|
|
||||||
|
## Testing Policy
|
||||||
|
|
||||||
|
- Prioritize tests for model parsing, service logic, and high-regression interaction flows.
|
||||||
|
- Simple static UI changes may skip tests.
|
||||||
|
- Auth/Home/Cache changes must include targeted regression tests.
|
||||||
|
|
||||||
|
## Logging Conventions (Must)
|
||||||
|
|
||||||
|
### Logger Setup
|
||||||
|
|
||||||
|
```dart
|
||||||
|
import 'core/logging/logger.dart';
|
||||||
|
|
||||||
|
class SomeBloc extends Cubit<SomeState> {
|
||||||
|
final Logger _logger = getLogger('features.<feature>.<component>');
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Log Level Policy
|
||||||
|
|
||||||
|
| Level | When to Use | Noise Level |
|
||||||
|
|-------|-------------|-------------|
|
||||||
|
| **error** | All exceptions and failures - MUST log every error site | Required, never skip |
|
||||||
|
| **warning** | Degraded behavior, retry, fallback, malformed data | Minimal, only when action taken |
|
||||||
|
| **info** | Key business events (login, logout, send message) | Minimal, only milestone events |
|
||||||
|
| **debug** | Detailed flow tracing (only in debug builds) | High, avoid in release |
|
||||||
|
|
||||||
|
### Error Logging Requirements
|
||||||
|
|
||||||
|
**Every try-catch that handles an exception MUST log it:**
|
||||||
|
```dart
|
||||||
|
try {
|
||||||
|
await _repository.someOperation();
|
||||||
|
} catch (e, stackTrace) {
|
||||||
|
_logger.error(
|
||||||
|
message: 'Operation failed: $operationName',
|
||||||
|
error: e,
|
||||||
|
stackTrace: stackTrace,
|
||||||
|
extra: {'context': 'relevant_data'},
|
||||||
|
);
|
||||||
|
// handle error
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Info Logging Requirements
|
||||||
|
|
||||||
|
**Only log these milestone events:**
|
||||||
|
- User login/logout
|
||||||
|
- Message sent/received
|
||||||
|
- Data sync completed
|
||||||
|
- Important state transitions
|
||||||
|
|
||||||
|
```dart
|
||||||
|
_logger.info(
|
||||||
|
message: 'User logged in',
|
||||||
|
extra: {'user_id': user.id},
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Warning Logging Requirements
|
||||||
|
|
||||||
|
**Only log when taking corrective action:**
|
||||||
|
- Retrying after failure
|
||||||
|
- Using fallback data
|
||||||
|
- Skipping malformed data
|
||||||
|
- Deprecation warnings
|
||||||
|
|
||||||
|
```dart
|
||||||
|
_logger.warning(
|
||||||
|
message: 'Cache miss, loading from remote',
|
||||||
|
extra: {'key': cacheKey},
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Module Naming Convention
|
||||||
|
|
||||||
|
| Feature | Module Path |
|
||||||
|
|---------|------------|
|
||||||
|
| auth | `features.auth` |
|
||||||
|
| calendar | `features.calendar` |
|
||||||
|
| chat | `features.chat` |
|
||||||
|
| contacts | `features.contacts` |
|
||||||
|
| home | `features.home` |
|
||||||
|
| messages | `features.messages` |
|
||||||
|
| settings | `features.settings` |
|
||||||
|
| todo | `features.todo` |
|
||||||
|
|
||||||
|
### Prohibited Practices
|
||||||
|
|
||||||
|
- **Never** log sensitive data: passwords, tokens, PII, message content
|
||||||
|
- **Never** log at debug level in production (release mode)
|
||||||
|
- **Never** skip error logging even if you "handle" the error
|
||||||
|
- **Never** log for every iteration in loops - only on failures
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
# meeyao_qianwen
|
||||||
|
|
||||||
|
A new Flutter project.
|
||||||
|
|
||||||
|
## Getting Started
|
||||||
|
|
||||||
|
This project is a starting point for a Flutter application.
|
||||||
|
|
||||||
|
A few resources to get you started if this is your first Flutter project:
|
||||||
|
|
||||||
|
- [Lab: Write your first Flutter app](https://docs.flutter.dev/get-started/codelab)
|
||||||
|
- [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook)
|
||||||
|
|
||||||
|
For help getting started with Flutter development, view the
|
||||||
|
[online documentation](https://docs.flutter.dev/), which offers tutorials,
|
||||||
|
samples, guidance on mobile development, and a full API reference.
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
# This file configures the analyzer, which statically analyzes Dart code to
|
||||||
|
# check for errors, warnings, and lints.
|
||||||
|
#
|
||||||
|
# The issues identified by the analyzer are surfaced in the UI of Dart-enabled
|
||||||
|
# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be
|
||||||
|
# invoked from the command line by running `flutter analyze`.
|
||||||
|
|
||||||
|
# The following line activates a set of recommended lints for Flutter apps,
|
||||||
|
# packages, and plugins designed to encourage good coding practices.
|
||||||
|
include: package:flutter_lints/flutter.yaml
|
||||||
|
|
||||||
|
linter:
|
||||||
|
# The lint rules applied to this project can be customized in the
|
||||||
|
# section below to disable rules from the `package:flutter_lints/flutter.yaml`
|
||||||
|
# included above or to enable additional rules. A list of all available lints
|
||||||
|
# and their documentation is published at https://dart.dev/lints.
|
||||||
|
#
|
||||||
|
# Instead of disabling a lint rule for the entire project in the
|
||||||
|
# section below, it can also be suppressed for a single line of code
|
||||||
|
# or a specific dart file by using the `// ignore: name_of_lint` and
|
||||||
|
# `// ignore_for_file: name_of_lint` syntax on the line or in the file
|
||||||
|
# producing the lint.
|
||||||
|
rules:
|
||||||
|
# avoid_print: false # Uncomment to disable the `avoid_print` rule
|
||||||
|
# prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
|
||||||
|
|
||||||
|
# Additional information about this file can be found at
|
||||||
|
# https://dart.dev/guides/language/analysis-options
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
gradle-wrapper.jar
|
||||||
|
/.gradle
|
||||||
|
/captures/
|
||||||
|
/gradlew
|
||||||
|
/gradlew.bat
|
||||||
|
/local.properties
|
||||||
|
GeneratedPluginRegistrant.java
|
||||||
|
.cxx/
|
||||||
|
|
||||||
|
# Remember to never publicly share your keystore.
|
||||||
|
# See https://flutter.dev/to/reference-keystore
|
||||||
|
key.properties
|
||||||
|
**/*.keystore
|
||||||
|
**/*.jks
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
plugins {
|
||||||
|
id("com.android.application")
|
||||||
|
id("kotlin-android")
|
||||||
|
// The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins.
|
||||||
|
id("dev.flutter.flutter-gradle-plugin")
|
||||||
|
}
|
||||||
|
|
||||||
|
android {
|
||||||
|
namespace = "com.meeyao.meeyao_qianwen"
|
||||||
|
compileSdk = flutter.compileSdkVersion
|
||||||
|
ndkVersion = flutter.ndkVersion
|
||||||
|
|
||||||
|
compileOptions {
|
||||||
|
sourceCompatibility = JavaVersion.VERSION_17
|
||||||
|
targetCompatibility = JavaVersion.VERSION_17
|
||||||
|
}
|
||||||
|
|
||||||
|
kotlinOptions {
|
||||||
|
jvmTarget = JavaVersion.VERSION_17.toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
defaultConfig {
|
||||||
|
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
|
||||||
|
applicationId = "com.meeyao.meeyao_qianwen"
|
||||||
|
// You can update the following values to match your application needs.
|
||||||
|
// For more information, see: https://flutter.dev/to/review-gradle-config.
|
||||||
|
minSdk = flutter.minSdkVersion
|
||||||
|
targetSdk = flutter.targetSdkVersion
|
||||||
|
versionCode = flutter.versionCode
|
||||||
|
versionName = flutter.versionName
|
||||||
|
}
|
||||||
|
|
||||||
|
buildTypes {
|
||||||
|
release {
|
||||||
|
// TODO: Add your own signing config for the release build.
|
||||||
|
// Signing with the debug keys for now, so `flutter run --release` works.
|
||||||
|
signingConfig = signingConfigs.getByName("debug")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
flutter {
|
||||||
|
source = "../.."
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<!-- The INTERNET permission is required for development. Specifically,
|
||||||
|
the Flutter tool needs it to communicate with the running application
|
||||||
|
to allow setting breakpoints, to provide hot reload, etc.
|
||||||
|
-->
|
||||||
|
<uses-permission android:name="android.permission.INTERNET"/>
|
||||||
|
</manifest>
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<application
|
||||||
|
android:label="meeyao_qianwen"
|
||||||
|
android:name="${applicationName}"
|
||||||
|
android:icon="@mipmap/ic_launcher">
|
||||||
|
<activity
|
||||||
|
android:name=".MainActivity"
|
||||||
|
android:exported="true"
|
||||||
|
android:launchMode="singleTop"
|
||||||
|
android:taskAffinity=""
|
||||||
|
android:theme="@style/LaunchTheme"
|
||||||
|
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
|
||||||
|
android:hardwareAccelerated="true"
|
||||||
|
android:windowSoftInputMode="adjustResize">
|
||||||
|
<!-- Specifies an Android theme to apply to this Activity as soon as
|
||||||
|
the Android process has started. This theme is visible to the user
|
||||||
|
while the Flutter UI initializes. After that, this theme continues
|
||||||
|
to determine the Window background behind the Flutter UI. -->
|
||||||
|
<meta-data
|
||||||
|
android:name="io.flutter.embedding.android.NormalTheme"
|
||||||
|
android:resource="@style/NormalTheme"
|
||||||
|
/>
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.MAIN"/>
|
||||||
|
<category android:name="android.intent.category.LAUNCHER"/>
|
||||||
|
</intent-filter>
|
||||||
|
</activity>
|
||||||
|
<!-- Don't delete the meta-data below.
|
||||||
|
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
|
||||||
|
<meta-data
|
||||||
|
android:name="flutterEmbedding"
|
||||||
|
android:value="2" />
|
||||||
|
</application>
|
||||||
|
<!-- Required to query activities that can process text, see:
|
||||||
|
https://developer.android.com/training/package-visibility and
|
||||||
|
https://developer.android.com/reference/android/content/Intent#ACTION_PROCESS_TEXT.
|
||||||
|
|
||||||
|
In particular, this is used by the Flutter engine in io.flutter.plugin.text.ProcessTextPlugin. -->
|
||||||
|
<queries>
|
||||||
|
<intent>
|
||||||
|
<action android:name="android.intent.action.PROCESS_TEXT"/>
|
||||||
|
<data android:mimeType="text/plain"/>
|
||||||
|
</intent>
|
||||||
|
</queries>
|
||||||
|
</manifest>
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
package com.meeyao.meeyao_qianwen
|
||||||
|
|
||||||
|
import io.flutter.embedding.android.FlutterActivity
|
||||||
|
|
||||||
|
class MainActivity : FlutterActivity()
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- Modify this file to customize your launch splash screen -->
|
||||||
|
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<item android:drawable="?android:colorBackground" />
|
||||||
|
|
||||||
|
<!-- You can insert your own image assets here -->
|
||||||
|
<!-- <item>
|
||||||
|
<bitmap
|
||||||
|
android:gravity="center"
|
||||||
|
android:src="@mipmap/launch_image" />
|
||||||
|
</item> -->
|
||||||
|
</layer-list>
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- Modify this file to customize your launch splash screen -->
|
||||||
|
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<item android:drawable="@android:color/white" />
|
||||||
|
|
||||||
|
<!-- You can insert your own image assets here -->
|
||||||
|
<!-- <item>
|
||||||
|
<bitmap
|
||||||
|
android:gravity="center"
|
||||||
|
android:src="@mipmap/launch_image" />
|
||||||
|
</item> -->
|
||||||
|
</layer-list>
|
||||||
|
After Width: | Height: | Size: 544 B |
|
After Width: | Height: | Size: 442 B |
|
After Width: | Height: | Size: 721 B |
|
After Width: | Height: | Size: 1.0 KiB |
|
After Width: | Height: | Size: 1.4 KiB |
@@ -0,0 +1,18 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is on -->
|
||||||
|
<style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
|
||||||
|
<!-- Show a splash screen on the activity. Automatically removed when
|
||||||
|
the Flutter engine draws its first frame -->
|
||||||
|
<item name="android:windowBackground">@drawable/launch_background</item>
|
||||||
|
</style>
|
||||||
|
<!-- Theme applied to the Android Window as soon as the process has started.
|
||||||
|
This theme determines the color of the Android Window while your
|
||||||
|
Flutter UI initializes, as well as behind your Flutter UI while its
|
||||||
|
running.
|
||||||
|
|
||||||
|
This Theme is only used starting with V2 of Flutter's Android embedding. -->
|
||||||
|
<style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
|
||||||
|
<item name="android:windowBackground">?android:colorBackground</item>
|
||||||
|
</style>
|
||||||
|
</resources>
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is off -->
|
||||||
|
<style name="LaunchTheme" parent="@android:style/Theme.Light.NoTitleBar">
|
||||||
|
<!-- Show a splash screen on the activity. Automatically removed when
|
||||||
|
the Flutter engine draws its first frame -->
|
||||||
|
<item name="android:windowBackground">@drawable/launch_background</item>
|
||||||
|
</style>
|
||||||
|
<!-- Theme applied to the Android Window as soon as the process has started.
|
||||||
|
This theme determines the color of the Android Window while your
|
||||||
|
Flutter UI initializes, as well as behind your Flutter UI while its
|
||||||
|
running.
|
||||||
|
|
||||||
|
This Theme is only used starting with V2 of Flutter's Android embedding. -->
|
||||||
|
<style name="NormalTheme" parent="@android:style/Theme.Light.NoTitleBar">
|
||||||
|
<item name="android:windowBackground">?android:colorBackground</item>
|
||||||
|
</style>
|
||||||
|
</resources>
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<!-- The INTERNET permission is required for development. Specifically,
|
||||||
|
the Flutter tool needs it to communicate with the running application
|
||||||
|
to allow setting breakpoints, to provide hot reload, etc.
|
||||||
|
-->
|
||||||
|
<uses-permission android:name="android.permission.INTERNET"/>
|
||||||
|
</manifest>
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
allprojects {
|
||||||
|
repositories {
|
||||||
|
google()
|
||||||
|
mavenCentral()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val newBuildDir: Directory =
|
||||||
|
rootProject.layout.buildDirectory
|
||||||
|
.dir("../../build")
|
||||||
|
.get()
|
||||||
|
rootProject.layout.buildDirectory.value(newBuildDir)
|
||||||
|
|
||||||
|
subprojects {
|
||||||
|
val newSubprojectBuildDir: Directory = newBuildDir.dir(project.name)
|
||||||
|
project.layout.buildDirectory.value(newSubprojectBuildDir)
|
||||||
|
}
|
||||||
|
subprojects {
|
||||||
|
project.evaluationDependsOn(":app")
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks.register<Delete>("clean") {
|
||||||
|
delete(rootProject.layout.buildDirectory)
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError
|
||||||
|
android.useAndroidX=true
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
distributionBase=GRADLE_USER_HOME
|
||||||
|
distributionPath=wrapper/dists
|
||||||
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
|
zipStorePath=wrapper/dists
|
||||||
|
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14-all.zip
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
pluginManagement {
|
||||||
|
val flutterSdkPath =
|
||||||
|
run {
|
||||||
|
val properties = java.util.Properties()
|
||||||
|
file("local.properties").inputStream().use { properties.load(it) }
|
||||||
|
val flutterSdkPath = properties.getProperty("flutter.sdk")
|
||||||
|
require(flutterSdkPath != null) { "flutter.sdk not set in local.properties" }
|
||||||
|
flutterSdkPath
|
||||||
|
}
|
||||||
|
|
||||||
|
includeBuild("$flutterSdkPath/packages/flutter_tools/gradle")
|
||||||
|
|
||||||
|
repositories {
|
||||||
|
google()
|
||||||
|
mavenCentral()
|
||||||
|
gradlePluginPortal()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
plugins {
|
||||||
|
id("dev.flutter.flutter-plugin-loader") version "1.0.0"
|
||||||
|
id("com.android.application") version "8.11.1" apply false
|
||||||
|
id("org.jetbrains.kotlin.android") version "2.2.20" apply false
|
||||||
|
}
|
||||||
|
|
||||||
|
include(":app")
|
||||||
|
After Width: | Height: | Size: 142 KiB |
@@ -0,0 +1,34 @@
|
|||||||
|
**/dgph
|
||||||
|
*.mode1v3
|
||||||
|
*.mode2v3
|
||||||
|
*.moved-aside
|
||||||
|
*.pbxuser
|
||||||
|
*.perspectivev3
|
||||||
|
**/*sync/
|
||||||
|
.sconsign.dblite
|
||||||
|
.tags*
|
||||||
|
**/.vagrant/
|
||||||
|
**/DerivedData/
|
||||||
|
Icon?
|
||||||
|
**/Pods/
|
||||||
|
**/.symlinks/
|
||||||
|
profile
|
||||||
|
xcuserdata
|
||||||
|
**/.generated/
|
||||||
|
Flutter/App.framework
|
||||||
|
Flutter/Flutter.framework
|
||||||
|
Flutter/Flutter.podspec
|
||||||
|
Flutter/Generated.xcconfig
|
||||||
|
Flutter/ephemeral/
|
||||||
|
Flutter/app.flx
|
||||||
|
Flutter/app.zip
|
||||||
|
Flutter/flutter_assets/
|
||||||
|
Flutter/flutter_export_environment.sh
|
||||||
|
ServiceDefinitions.json
|
||||||
|
Runner/GeneratedPluginRegistrant.*
|
||||||
|
|
||||||
|
# Exceptions to above rules.
|
||||||
|
!default.mode1v3
|
||||||
|
!default.mode2v3
|
||||||
|
!default.pbxuser
|
||||||
|
!default.perspectivev3
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>CFBundleDevelopmentRegion</key>
|
||||||
|
<string>en</string>
|
||||||
|
<key>CFBundleExecutable</key>
|
||||||
|
<string>App</string>
|
||||||
|
<key>CFBundleIdentifier</key>
|
||||||
|
<string>io.flutter.flutter.app</string>
|
||||||
|
<key>CFBundleInfoDictionaryVersion</key>
|
||||||
|
<string>6.0</string>
|
||||||
|
<key>CFBundleName</key>
|
||||||
|
<string>App</string>
|
||||||
|
<key>CFBundlePackageType</key>
|
||||||
|
<string>FMWK</string>
|
||||||
|
<key>CFBundleShortVersionString</key>
|
||||||
|
<string>1.0</string>
|
||||||
|
<key>CFBundleSignature</key>
|
||||||
|
<string>????</string>
|
||||||
|
<key>CFBundleVersion</key>
|
||||||
|
<string>1.0</string>
|
||||||
|
<key>MinimumOSVersion</key>
|
||||||
|
<string>13.0</string>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
#include "Generated.xcconfig"
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
#include "Generated.xcconfig"
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import Flutter
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
@main
|
||||||
|
@objc class AppDelegate: FlutterAppDelegate {
|
||||||
|
override func application(
|
||||||
|
_ application: UIApplication,
|
||||||
|
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
|
||||||
|
) -> Bool {
|
||||||
|
GeneratedPluginRegistrant.register(with: self)
|
||||||
|
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,122 @@
|
|||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"size" : "20x20",
|
||||||
|
"idiom" : "iphone",
|
||||||
|
"filename" : "Icon-App-20x20@2x.png",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"size" : "20x20",
|
||||||
|
"idiom" : "iphone",
|
||||||
|
"filename" : "Icon-App-20x20@3x.png",
|
||||||
|
"scale" : "3x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"size" : "29x29",
|
||||||
|
"idiom" : "iphone",
|
||||||
|
"filename" : "Icon-App-29x29@1x.png",
|
||||||
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"size" : "29x29",
|
||||||
|
"idiom" : "iphone",
|
||||||
|
"filename" : "Icon-App-29x29@2x.png",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"size" : "29x29",
|
||||||
|
"idiom" : "iphone",
|
||||||
|
"filename" : "Icon-App-29x29@3x.png",
|
||||||
|
"scale" : "3x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"size" : "40x40",
|
||||||
|
"idiom" : "iphone",
|
||||||
|
"filename" : "Icon-App-40x40@2x.png",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"size" : "40x40",
|
||||||
|
"idiom" : "iphone",
|
||||||
|
"filename" : "Icon-App-40x40@3x.png",
|
||||||
|
"scale" : "3x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"size" : "60x60",
|
||||||
|
"idiom" : "iphone",
|
||||||
|
"filename" : "Icon-App-60x60@2x.png",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"size" : "60x60",
|
||||||
|
"idiom" : "iphone",
|
||||||
|
"filename" : "Icon-App-60x60@3x.png",
|
||||||
|
"scale" : "3x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"size" : "20x20",
|
||||||
|
"idiom" : "ipad",
|
||||||
|
"filename" : "Icon-App-20x20@1x.png",
|
||||||
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"size" : "20x20",
|
||||||
|
"idiom" : "ipad",
|
||||||
|
"filename" : "Icon-App-20x20@2x.png",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"size" : "29x29",
|
||||||
|
"idiom" : "ipad",
|
||||||
|
"filename" : "Icon-App-29x29@1x.png",
|
||||||
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"size" : "29x29",
|
||||||
|
"idiom" : "ipad",
|
||||||
|
"filename" : "Icon-App-29x29@2x.png",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"size" : "40x40",
|
||||||
|
"idiom" : "ipad",
|
||||||
|
"filename" : "Icon-App-40x40@1x.png",
|
||||||
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"size" : "40x40",
|
||||||
|
"idiom" : "ipad",
|
||||||
|
"filename" : "Icon-App-40x40@2x.png",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"size" : "76x76",
|
||||||
|
"idiom" : "ipad",
|
||||||
|
"filename" : "Icon-App-76x76@1x.png",
|
||||||
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"size" : "76x76",
|
||||||
|
"idiom" : "ipad",
|
||||||
|
"filename" : "Icon-App-76x76@2x.png",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"size" : "83.5x83.5",
|
||||||
|
"idiom" : "ipad",
|
||||||
|
"filename" : "Icon-App-83.5x83.5@2x.png",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"size" : "1024x1024",
|
||||||
|
"idiom" : "ios-marketing",
|
||||||
|
"filename" : "Icon-App-1024x1024@1x.png",
|
||||||
|
"scale" : "1x"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"version" : 1,
|
||||||
|
"author" : "xcode"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 295 B |
|
After Width: | Height: | Size: 406 B |
|
After Width: | Height: | Size: 450 B |
|
After Width: | Height: | Size: 282 B |
|
After Width: | Height: | Size: 462 B |
|
After Width: | Height: | Size: 704 B |
|
After Width: | Height: | Size: 406 B |
|
After Width: | Height: | Size: 586 B |
|
After Width: | Height: | Size: 862 B |
|
After Width: | Height: | Size: 862 B |
|
After Width: | Height: | Size: 1.6 KiB |
|
After Width: | Height: | Size: 762 B |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 1.4 KiB |
@@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"filename" : "LaunchImage.png",
|
||||||
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"filename" : "LaunchImage@2x.png",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"filename" : "LaunchImage@3x.png",
|
||||||
|
"scale" : "3x"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"version" : 1,
|
||||||
|
"author" : "xcode"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
After Width: | Height: | Size: 68 B |
|
After Width: | Height: | Size: 68 B |
|
After Width: | Height: | Size: 68 B |
@@ -0,0 +1,5 @@
|
|||||||
|
# Launch Screen Assets
|
||||||
|
|
||||||
|
You can customize the launch screen with your own desired assets by replacing the image files in this directory.
|
||||||
|
|
||||||
|
You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images.
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="12121" systemVersion="16G29" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" colorMatched="YES" initialViewController="01J-lp-oVM">
|
||||||
|
<dependencies>
|
||||||
|
<deployment identifier="iOS"/>
|
||||||
|
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="12089"/>
|
||||||
|
</dependencies>
|
||||||
|
<scenes>
|
||||||
|
<!--View Controller-->
|
||||||
|
<scene sceneID="EHf-IW-A2E">
|
||||||
|
<objects>
|
||||||
|
<viewController id="01J-lp-oVM" sceneMemberID="viewController">
|
||||||
|
<layoutGuides>
|
||||||
|
<viewControllerLayoutGuide type="top" id="Ydg-fD-yQy"/>
|
||||||
|
<viewControllerLayoutGuide type="bottom" id="xbc-2k-c8Z"/>
|
||||||
|
</layoutGuides>
|
||||||
|
<view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3">
|
||||||
|
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||||
|
<subviews>
|
||||||
|
<imageView opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" image="LaunchImage" translatesAutoresizingMaskIntoConstraints="NO" id="YRO-k0-Ey4">
|
||||||
|
</imageView>
|
||||||
|
</subviews>
|
||||||
|
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||||
|
<constraints>
|
||||||
|
<constraint firstItem="YRO-k0-Ey4" firstAttribute="centerX" secondItem="Ze5-6b-2t3" secondAttribute="centerX" id="1a2-6s-vTC"/>
|
||||||
|
<constraint firstItem="YRO-k0-Ey4" firstAttribute="centerY" secondItem="Ze5-6b-2t3" secondAttribute="centerY" id="4X2-HB-R7a"/>
|
||||||
|
</constraints>
|
||||||
|
</view>
|
||||||
|
</viewController>
|
||||||
|
<placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>
|
||||||
|
</objects>
|
||||||
|
<point key="canvasLocation" x="53" y="375"/>
|
||||||
|
</scene>
|
||||||
|
</scenes>
|
||||||
|
<resources>
|
||||||
|
<image name="LaunchImage" width="168" height="185"/>
|
||||||
|
</resources>
|
||||||
|
</document>
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="10117" systemVersion="15F34" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" initialViewController="BYZ-38-t0r">
|
||||||
|
<dependencies>
|
||||||
|
<deployment identifier="iOS"/>
|
||||||
|
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="10085"/>
|
||||||
|
</dependencies>
|
||||||
|
<scenes>
|
||||||
|
<!--Flutter View Controller-->
|
||||||
|
<scene sceneID="tne-QT-ifu">
|
||||||
|
<objects>
|
||||||
|
<viewController id="BYZ-38-t0r" customClass="FlutterViewController" sceneMemberID="viewController">
|
||||||
|
<layoutGuides>
|
||||||
|
<viewControllerLayoutGuide type="top" id="y3c-jy-aDJ"/>
|
||||||
|
<viewControllerLayoutGuide type="bottom" id="wfy-db-euE"/>
|
||||||
|
</layoutGuides>
|
||||||
|
<view key="view" contentMode="scaleToFill" id="8bC-Xf-vdC">
|
||||||
|
<rect key="frame" x="0.0" y="0.0" width="600" height="600"/>
|
||||||
|
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||||
|
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="calibratedWhite"/>
|
||||||
|
</view>
|
||||||
|
</viewController>
|
||||||
|
<placeholder placeholderIdentifier="IBFirstResponder" id="dkx-z0-nzr" sceneMemberID="firstResponder"/>
|
||||||
|
</objects>
|
||||||
|
</scene>
|
||||||
|
</scenes>
|
||||||
|
</document>
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>CFBundleDevelopmentRegion</key>
|
||||||
|
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||||
|
<key>CFBundleDisplayName</key>
|
||||||
|
<string>Meeyao Qianwen</string>
|
||||||
|
<key>CFBundleExecutable</key>
|
||||||
|
<string>$(EXECUTABLE_NAME)</string>
|
||||||
|
<key>CFBundleIdentifier</key>
|
||||||
|
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||||
|
<key>CFBundleInfoDictionaryVersion</key>
|
||||||
|
<string>6.0</string>
|
||||||
|
<key>CFBundleName</key>
|
||||||
|
<string>meeyao_qianwen</string>
|
||||||
|
<key>CFBundlePackageType</key>
|
||||||
|
<string>APPL</string>
|
||||||
|
<key>CFBundleShortVersionString</key>
|
||||||
|
<string>$(FLUTTER_BUILD_NAME)</string>
|
||||||
|
<key>CFBundleSignature</key>
|
||||||
|
<string>????</string>
|
||||||
|
<key>CFBundleVersion</key>
|
||||||
|
<string>$(FLUTTER_BUILD_NUMBER)</string>
|
||||||
|
<key>LSRequiresIPhoneOS</key>
|
||||||
|
<true/>
|
||||||
|
<key>UILaunchStoryboardName</key>
|
||||||
|
<string>LaunchScreen</string>
|
||||||
|
<key>UIMainStoryboardFile</key>
|
||||||
|
<string>Main</string>
|
||||||
|
<key>UISupportedInterfaceOrientations</key>
|
||||||
|
<array>
|
||||||
|
<string>UIInterfaceOrientationPortrait</string>
|
||||||
|
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||||
|
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||||
|
</array>
|
||||||
|
<key>UISupportedInterfaceOrientations~ipad</key>
|
||||||
|
<array>
|
||||||
|
<string>UIInterfaceOrientationPortrait</string>
|
||||||
|
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
||||||
|
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||||
|
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||||
|
</array>
|
||||||
|
<key>CADisableMinimumFrameDurationOnPhone</key>
|
||||||
|
<true/>
|
||||||
|
<key>UIApplicationSupportsIndirectInputEvents</key>
|
||||||
|
<true/>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
#import "GeneratedPluginRegistrant.h"
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
import Flutter
|
||||||
|
import UIKit
|
||||||
|
import XCTest
|
||||||
|
|
||||||
|
class RunnerTests: XCTestCase {
|
||||||
|
|
||||||
|
func testExample() {
|
||||||
|
// If you add code to the Runner application, consider adding tests here.
|
||||||
|
// See https://developer.apple.com/documentation/xctest for more information about using XCTest.
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
arb-dir: lib/l10n
|
||||||
|
template-arb-file: app_zh.arb
|
||||||
|
output-localization-file: app_localizations.dart
|
||||||
|
output-class: AppLocalizations
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
{
|
||||||
|
"@@locale": "zh",
|
||||||
|
"appTitle": "觅爻签问",
|
||||||
|
"welcomeLogin": "欢迎登录",
|
||||||
|
"loginSubtitle": "请使用手机号登录",
|
||||||
|
"phoneHint": "请输入手机号码",
|
||||||
|
"codeHint": "请输入验证码",
|
||||||
|
"sendCode": "获取验证码",
|
||||||
|
"sending": "发送中...",
|
||||||
|
"retryAfter": "{seconds}秒后重试",
|
||||||
|
"@retryAfter": {
|
||||||
|
"placeholders": {
|
||||||
|
"seconds": {
|
||||||
|
"type": "int"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"login": "登录",
|
||||||
|
"agreementPrefix": "我已阅读并同意",
|
||||||
|
"privacyPolicy": "隐私政策",
|
||||||
|
"termsOfService": "服务条款",
|
||||||
|
"disclaimer": "免责声明",
|
||||||
|
"icp": "粤ICP备2025428416号-1A",
|
||||||
|
"invalidPhone": "请输入正确的手机号码",
|
||||||
|
"invalidCode": "请输入6位验证码",
|
||||||
|
"agreementRequired": "请先勾选协议",
|
||||||
|
"codeSent": "验证码已发送,请注意查收",
|
||||||
|
"mockLoginSuccess": "模拟登录成功",
|
||||||
|
"helloUser": "您好,{name}",
|
||||||
|
"@helloUser": {
|
||||||
|
"placeholders": {
|
||||||
|
"name": {
|
||||||
|
"type": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"startJourney": "开始您的卦象之旅",
|
||||||
|
"journeySubtitle": "借助AI智能,探索未来的可能",
|
||||||
|
"startNow": "立即起卦",
|
||||||
|
"historyTitle": "历史解卦",
|
||||||
|
"more": "更多",
|
||||||
|
"noRecords": "暂无记录",
|
||||||
|
"noRecordsSubtitle": "您并没有保存任何卦象",
|
||||||
|
"homeTab": "首页",
|
||||||
|
"profileTab": "我的",
|
||||||
|
"notify": "消息通知",
|
||||||
|
"featurePending": "该功能暂未接入数据",
|
||||||
|
"welcomeDialogTitle": "欢迎使用觅爻签问",
|
||||||
|
"welcomeParagraph1": "你好,欢迎来到觅爻签问,这是一个借助于AI解读传统六爻卦象的平台,为用户了解中国传统易学文化提供一个窗口。",
|
||||||
|
"welcomeParagraph2": "六爻卦象源于《周易》深邃的哲学体系,是古人探索世界运行规律的一种独特方法。古人认为宇宙万物相互关联,在你起卦时,你的心念与时空信息会凝结成卦象的方式呈现出来。",
|
||||||
|
"welcomeParagraph3": "觅爻签问基于这样的思路,帮助你跳出局限思维,从全局和演变趋势看清矛盾、机会与风险,为判断和行动提供参考。",
|
||||||
|
"warningTitle": "特别提醒",
|
||||||
|
"warningBody": "卦象解读结果均由AI生成,仅供娱乐参考,切不可作为商业、医疗等专业领域的决策依据。理性看待卦象,自由掌握人生。",
|
||||||
|
"scrollHint": "请向下滚动阅读全部内容",
|
||||||
|
"understood": "我已了解",
|
||||||
|
"readAllFirst": "请先阅读完整内容",
|
||||||
|
"categoryCareer": "事业学业",
|
||||||
|
"categoryLove": "情感婚姻",
|
||||||
|
"categoryMoney": "财富投资",
|
||||||
|
"signBest": "上上签",
|
||||||
|
"signGood": "中上签",
|
||||||
|
"signNormal": "中下签",
|
||||||
|
"language": "语言",
|
||||||
|
"english": "英文",
|
||||||
|
"chinese": "中文",
|
||||||
|
"privacyContent": "隐私政策内容展示占位。",
|
||||||
|
"termsContent": "服务条款内容展示占位。",
|
||||||
|
"disclaimerContent": "免责声明内容展示占位。"
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
import 'package:flutter/widgets.dart';
|
||||||
|
|
||||||
|
import 'app/app.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
WidgetsFlutterBinding.ensureInitialized();
|
||||||
|
runApp(const EryaoApp());
|
||||||
|
}
|
||||||
@@ -0,0 +1,97 @@
|
|||||||
|
name: meeyao_qianwen
|
||||||
|
description: "觅爻签问 Flutter app"
|
||||||
|
# The following line prevents the package from being accidentally published to
|
||||||
|
# pub.dev using `flutter pub publish`. This is preferred for private packages.
|
||||||
|
publish_to: 'none' # Remove this line if you wish to publish to pub.dev
|
||||||
|
|
||||||
|
# The following defines the version and build number for your application.
|
||||||
|
# A version number is three numbers separated by dots, like 1.2.43
|
||||||
|
# followed by an optional build number separated by a +.
|
||||||
|
# Both the version and the builder number may be overridden in flutter
|
||||||
|
# build by specifying --build-name and --build-number, respectively.
|
||||||
|
# In Android, build-name is used as versionName while build-number used as versionCode.
|
||||||
|
# Read more about Android versioning at https://developer.android.com/studio/publish/versioning
|
||||||
|
# In iOS, build-name is used as CFBundleShortVersionString while build-number is used as CFBundleVersion.
|
||||||
|
# Read more about iOS versioning at
|
||||||
|
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
||||||
|
# In Windows, build-name is used as the major, minor, and patch parts
|
||||||
|
# of the product and file versions while build-number is used as the build suffix.
|
||||||
|
version: 1.0.0+1
|
||||||
|
|
||||||
|
environment:
|
||||||
|
sdk: ^3.10.7
|
||||||
|
|
||||||
|
# Dependencies specify other packages that your package needs in order to work.
|
||||||
|
# To automatically upgrade your package dependencies to the latest versions
|
||||||
|
# consider running `flutter pub upgrade --major-versions`. Alternatively,
|
||||||
|
# dependencies can be manually updated by changing the version numbers below to
|
||||||
|
# the latest version available on pub.dev. To see which dependencies have newer
|
||||||
|
# versions available, run `flutter pub outdated`.
|
||||||
|
dependencies:
|
||||||
|
flutter:
|
||||||
|
sdk: flutter
|
||||||
|
flutter_localizations:
|
||||||
|
sdk: flutter
|
||||||
|
intl: ^0.20.2
|
||||||
|
shared_preferences: ^2.5.3
|
||||||
|
|
||||||
|
# The following adds the Cupertino Icons font to your application.
|
||||||
|
# Use with the CupertinoIcons class for iOS style icons.
|
||||||
|
cupertino_icons: ^1.0.8
|
||||||
|
|
||||||
|
dev_dependencies:
|
||||||
|
flutter_test:
|
||||||
|
sdk: flutter
|
||||||
|
|
||||||
|
# The "flutter_lints" package below contains a set of recommended lints to
|
||||||
|
# encourage good coding practices. The lint set provided by the package is
|
||||||
|
# activated in the `analysis_options.yaml` file located at the root of your
|
||||||
|
# package. See that file for information about deactivating specific lint
|
||||||
|
# rules and activating additional ones.
|
||||||
|
flutter_lints: ^6.0.0
|
||||||
|
|
||||||
|
# For information on the generic Dart part of this file, see the
|
||||||
|
# following page: https://dart.dev/tools/pub/pubspec
|
||||||
|
|
||||||
|
# The following section is specific to Flutter packages.
|
||||||
|
flutter:
|
||||||
|
generate: true
|
||||||
|
|
||||||
|
# The following line ensures that the Material Icons font is
|
||||||
|
# included with your application, so that you can use the icons in
|
||||||
|
# the material Icons class.
|
||||||
|
uses-material-design: true
|
||||||
|
|
||||||
|
assets:
|
||||||
|
- assets/images/logo.png
|
||||||
|
|
||||||
|
# To add assets to your application, add an assets section, like this:
|
||||||
|
# assets:
|
||||||
|
# - images/a_dot_burr.jpeg
|
||||||
|
# - images/a_dot_ham.jpeg
|
||||||
|
|
||||||
|
# An image asset can refer to one or more resolution-specific "variants", see
|
||||||
|
# https://flutter.dev/to/resolution-aware-images
|
||||||
|
|
||||||
|
# For details regarding adding assets from package dependencies, see
|
||||||
|
# https://flutter.dev/to/asset-from-package
|
||||||
|
|
||||||
|
# To add custom fonts to your application, add a fonts section here,
|
||||||
|
# in this "flutter" section. Each entry in this list should have a
|
||||||
|
# "family" key with the font family name, and a "fonts" key with a
|
||||||
|
# list giving the asset and other descriptors for the font. For
|
||||||
|
# example:
|
||||||
|
# fonts:
|
||||||
|
# - family: Schyler
|
||||||
|
# fonts:
|
||||||
|
# - asset: fonts/Schyler-Regular.ttf
|
||||||
|
# - asset: fonts/Schyler-Italic.ttf
|
||||||
|
# style: italic
|
||||||
|
# - family: Trajan Pro
|
||||||
|
# fonts:
|
||||||
|
# - asset: fonts/TrajanPro.ttf
|
||||||
|
# - asset: fonts/TrajanPro_Bold.ttf
|
||||||
|
# weight: 700
|
||||||
|
#
|
||||||
|
# For details regarding fonts from package dependencies,
|
||||||
|
# see https://flutter.dev/to/font-from-package
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
// This is a basic Flutter widget test.
|
||||||
|
//
|
||||||
|
// To perform an interaction with a widget in your test, use the WidgetTester
|
||||||
|
// utility in the flutter_test package. For example, you can send tap and scroll
|
||||||
|
// gestures. You can also use WidgetTester to find child widgets in the widget
|
||||||
|
// tree, read text, and verify that the values of widget properties are correct.
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
|
||||||
|
import 'package:meeyao_qianwen/main.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
testWidgets('Counter increments smoke test', (WidgetTester tester) async {
|
||||||
|
// Build our app and trigger a frame.
|
||||||
|
await tester.pumpWidget(const MyApp());
|
||||||
|
|
||||||
|
// Verify that our counter starts at 0.
|
||||||
|
expect(find.text('0'), findsOneWidget);
|
||||||
|
expect(find.text('1'), findsNothing);
|
||||||
|
|
||||||
|
// Tap the '+' icon and trigger a frame.
|
||||||
|
await tester.tap(find.byIcon(Icons.add));
|
||||||
|
await tester.pump();
|
||||||
|
|
||||||
|
// Verify that our counter has incremented.
|
||||||
|
expect(find.text('0'), findsNothing);
|
||||||
|
expect(find.text('1'), findsOneWidget);
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -49,6 +49,32 @@ This file governs `backend/**` only. Keep it minimal, enforceable, and non-dupli
|
|||||||
- Strong typing required at boundaries (Pydantic/dataclass); avoid weak untyped payload contracts.
|
- Strong typing required at boundaries (Pydantic/dataclass); avoid weak untyped payload contracts.
|
||||||
- Protocol/data contract changes must stay aligned with `docs/protocols/`.
|
- Protocol/data contract changes must stay aligned with `docs/protocols/`.
|
||||||
|
|
||||||
|
## Database Rules
|
||||||
|
|
||||||
|
- Supabase Auth is identity source; backend enforces business authorization.
|
||||||
|
- Use service-role DB access only in backend.
|
||||||
|
- Soft delete uses `deleted_at`; reads must exclude deleted records by default.
|
||||||
|
- Alembic is the only schema migration source of truth.
|
||||||
|
- Database migrations use `./infra/scripts/dev-migrate.sh`:
|
||||||
|
- `migrate` - run migrations only
|
||||||
|
- `init-data` - seed data only
|
||||||
|
- `bootstrap` - migrate + init-data
|
||||||
|
|
||||||
|
## Agent Runtime & Tools
|
||||||
|
|
||||||
|
- AG-UI protocol is mandatory for agent loop behavior.
|
||||||
|
- `ToolAgentOutput.result` is the canonical tool result field.
|
||||||
|
- Tool results must be machine-oriented and include IDs/outcomes needed for chaining.
|
||||||
|
|
||||||
|
## Tool Schema Rules for Small Models (e.g., qwen3.5-flash)
|
||||||
|
|
||||||
|
- Prefer `operations: list[OperationModel]` over parallel arrays.
|
||||||
|
- Validate tool args with strict Pydantic models (`extra="forbid"`).
|
||||||
|
- Keep payloads JSON-native (objects/lists), shallow, and deterministic.
|
||||||
|
- Make action-specific required fields explicit and fail with structured errors.
|
||||||
|
- Return per-item outcomes (`success/failed`, identifiers, partial status) for self-correction.
|
||||||
|
- Avoid broad entry-point coercion fallbacks; fix schema/prompt alignment first.
|
||||||
|
- Do not pass provider request fields with `None` values (avoid upstream 400 blocking tool calls).
|
||||||
|
|
||||||
## Testing
|
## Testing
|
||||||
|
|
||||||
|
|||||||
@@ -1,14 +0,0 @@
|
|||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from fastapi import FastAPI
|
|
||||||
|
|
||||||
app = FastAPI(
|
|
||||||
title="Eryao API",
|
|
||||||
description="觅爻签问后端服务",
|
|
||||||
version="0.1.0",
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@app.get("/health")
|
|
||||||
async def health_check() -> dict[str, str]:
|
|
||||||
return {"status": "ok"}
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
from .settings import Settings, config
|
|
||||||
|
|
||||||
__all__ = ["Settings", "config"]
|
|
||||||
@@ -8,6 +8,7 @@ from pydantic import (
|
|||||||
AnyHttpUrl,
|
AnyHttpUrl,
|
||||||
BaseModel,
|
BaseModel,
|
||||||
Field,
|
Field,
|
||||||
|
SecretStr,
|
||||||
computed_field,
|
computed_field,
|
||||||
field_validator,
|
field_validator,
|
||||||
model_validator,
|
model_validator,
|
||||||
@@ -118,11 +119,54 @@ class RedisSettings(BaseModel):
|
|||||||
return f"redis://{self.host}:{self.port}/{self.db}"
|
return f"redis://{self.host}:{self.port}/{self.db}"
|
||||||
|
|
||||||
|
|
||||||
|
class SupabaseSettings(BaseModel):
|
||||||
|
public_url: AnyHttpUrl
|
||||||
|
anon_key: str = "CHANGE_ME"
|
||||||
|
service_role_key: str = "CHANGE_ME"
|
||||||
|
jwt_secret: SecretStr | None = Field(default=None, exclude=True)
|
||||||
|
jwt_algorithm: Literal["HS256"] = "HS256"
|
||||||
|
jwt_issuer: str | None = None
|
||||||
|
|
||||||
|
@model_validator(mode="after")
|
||||||
|
def compute_defaults(self) -> "SupabaseSettings":
|
||||||
|
base = str(self.public_url).rstrip("/")
|
||||||
|
if self.jwt_issuer is None:
|
||||||
|
self.jwt_issuer = f"{base}/auth/v1"
|
||||||
|
|
||||||
|
return self
|
||||||
|
|
||||||
|
@computed_field
|
||||||
|
@property
|
||||||
|
def url(self) -> str:
|
||||||
|
return str(self.public_url)
|
||||||
|
|
||||||
|
|
||||||
|
class StorageSettings(BaseModel):
|
||||||
|
provider: Literal["supabase"] = "supabase"
|
||||||
|
signed_url_ttl_seconds: int = Field(default=600, ge=60, le=3600)
|
||||||
|
retention_days: int = Field(default=30, ge=1, le=3650)
|
||||||
|
|
||||||
|
class AttachmentSettings(BaseModel):
|
||||||
|
bucket: str = Field(default="eryao-attachments", min_length=3, max_length=63)
|
||||||
|
max_size_mb: int = Field(default=20, ge=1, le=200)
|
||||||
|
|
||||||
|
class AvatarSettings(BaseModel):
|
||||||
|
bucket: str = Field(default="avatars", min_length=3, max_length=63)
|
||||||
|
max_size_mb: int = Field(default=2, ge=1, le=10)
|
||||||
|
|
||||||
|
attachment: AttachmentSettings = Field(default_factory=AttachmentSettings)
|
||||||
|
avatar: AvatarSettings = Field(default_factory=AvatarSettings)
|
||||||
|
|
||||||
|
|
||||||
|
class LlmSettings(BaseModel):
|
||||||
|
provider_keys: dict[str, str] = Field(default_factory=dict)
|
||||||
|
|
||||||
|
|
||||||
class DatabaseSettings(BaseModel):
|
class DatabaseSettings(BaseModel):
|
||||||
host: str = "localhost"
|
host: str = "localhost"
|
||||||
port: int = 3306
|
port: int = 5432
|
||||||
name: str = "eryao"
|
name: str = "postgres"
|
||||||
user: str = "root"
|
user: str = "postgres"
|
||||||
password: str = "CHANGE_ME"
|
password: str = "CHANGE_ME"
|
||||||
|
|
||||||
@computed_field
|
@computed_field
|
||||||
@@ -130,83 +174,11 @@ class DatabaseSettings(BaseModel):
|
|||||||
def url(self) -> str:
|
def url(self) -> str:
|
||||||
password = quote(self.password, safe="")
|
password = quote(self.password, safe="")
|
||||||
return (
|
return (
|
||||||
f"mysql+aiomysql://{self.user}:{password}"
|
f"postgresql+asyncpg://{self.user}:{password}"
|
||||||
f"@{self.host}:{self.port}/{self.name}"
|
f"@{self.host}:{self.port}/{self.name}"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class AppVersionSettings(BaseModel):
|
|
||||||
manifest_path: str = Field(
|
|
||||||
default="deploy/static/releases/manifest.json",
|
|
||||||
description="发布清单文件路径,相对于项目根目录",
|
|
||||||
)
|
|
||||||
release_path_prefix: str = Field(
|
|
||||||
default="releases",
|
|
||||||
description="下载 URL 中文件目录前缀",
|
|
||||||
)
|
|
||||||
download_base_url: AnyHttpUrl | None = Field(
|
|
||||||
default=None,
|
|
||||||
description="下载链接基础域名,如 https://your-domain.com",
|
|
||||||
)
|
|
||||||
|
|
||||||
@field_validator("download_base_url", mode="before")
|
|
||||||
@classmethod
|
|
||||||
def empty_download_base_url_to_none(cls, value: object) -> object:
|
|
||||||
if value == "":
|
|
||||||
return None
|
|
||||||
return value
|
|
||||||
|
|
||||||
@field_validator("manifest_path")
|
|
||||||
@classmethod
|
|
||||||
def validate_manifest_path(cls, value: str) -> str:
|
|
||||||
normalized = Path(value)
|
|
||||||
if normalized.is_absolute() or ".." in normalized.parts:
|
|
||||||
raise ValueError("manifest_path must be a safe relative path")
|
|
||||||
return value
|
|
||||||
|
|
||||||
|
|
||||||
class AliyunSmsSettings(BaseModel):
|
|
||||||
access_key_id: str = "CHANGE_ME"
|
|
||||||
access_key_secret: str = "CHANGE_ME"
|
|
||||||
sign_name: str = "CHANGE_ME"
|
|
||||||
template_code: str = "CHANGE_ME"
|
|
||||||
region_id: str = "cn-hangzhou"
|
|
||||||
endpoint: str = "dysmsapi.aliyuncs.com"
|
|
||||||
test_mode: bool = False
|
|
||||||
|
|
||||||
|
|
||||||
class AliyunContentSecuritySettings(BaseModel):
|
|
||||||
access_key_id: str = "CHANGE_ME"
|
|
||||||
access_key_secret: str = "CHANGE_ME"
|
|
||||||
endpoint: str = "green-cip.cn-shenzhen.aliyuncs.com"
|
|
||||||
|
|
||||||
|
|
||||||
class AlipaySettings(BaseModel):
|
|
||||||
app_id: str = "CHANGE_ME"
|
|
||||||
merchant_id: str = "CHANGE_ME"
|
|
||||||
public_key: str = "CHANGE_ME"
|
|
||||||
private_key: str = "CHANGE_ME"
|
|
||||||
sign_type: str = "RSA2"
|
|
||||||
notify_url: str = ""
|
|
||||||
timeout_express: str = "30m"
|
|
||||||
sandbox: bool = False
|
|
||||||
|
|
||||||
|
|
||||||
class DeepSeekSettings(BaseModel):
|
|
||||||
api_key: str = "CHANGE_ME"
|
|
||||||
|
|
||||||
|
|
||||||
class AuthSettings(BaseModel):
|
|
||||||
token_expiration_days: int = 7
|
|
||||||
token_refresh_threshold_hours: int = 2
|
|
||||||
|
|
||||||
|
|
||||||
class VerificationSettings(BaseModel):
|
|
||||||
code_length: int = 6
|
|
||||||
expiration_minutes: int = 5
|
|
||||||
test_mode: bool = False
|
|
||||||
|
|
||||||
|
|
||||||
class SensitiveWordSettings(BaseModel):
|
class SensitiveWordSettings(BaseModel):
|
||||||
use_aliyun: bool = True
|
use_aliyun: bool = True
|
||||||
fallback_to_local: bool = True
|
fallback_to_local: bool = True
|
||||||
@@ -217,6 +189,11 @@ class TestSettings(BaseModel):
|
|||||||
password: str = ""
|
password: str = ""
|
||||||
|
|
||||||
|
|
||||||
|
class TaskiqSettings(BaseModel):
|
||||||
|
broker_url: str | None = None
|
||||||
|
result_backend_url: str | None = None
|
||||||
|
|
||||||
|
|
||||||
def _resolve_env_file() -> str:
|
def _resolve_env_file() -> str:
|
||||||
current = Path(__file__).resolve()
|
current = Path(__file__).resolve()
|
||||||
for parent in [current, *current.parents]:
|
for parent in [current, *current.parents]:
|
||||||
@@ -233,24 +210,31 @@ class Settings(BaseSettings):
|
|||||||
runtime: RuntimeSettings = RuntimeSettings()
|
runtime: RuntimeSettings = RuntimeSettings()
|
||||||
cors: CorsSettings = CorsSettings()
|
cors: CorsSettings = CorsSettings()
|
||||||
redis: RedisSettings = RedisSettings()
|
redis: RedisSettings = RedisSettings()
|
||||||
database: DatabaseSettings = DatabaseSettings()
|
supabase: SupabaseSettings = Field(
|
||||||
app_version: AppVersionSettings = AppVersionSettings()
|
default_factory=lambda: SupabaseSettings(public_url="http://localhost:8001")
|
||||||
aliyun_sms: AliyunSmsSettings = AliyunSmsSettings()
|
|
||||||
aliyun_content_security: AliyunContentSecuritySettings = (
|
|
||||||
AliyunContentSecuritySettings()
|
|
||||||
)
|
)
|
||||||
alipay: AlipaySettings = AlipaySettings()
|
storage: StorageSettings = StorageSettings()
|
||||||
deepseek: DeepSeekSettings = DeepSeekSettings()
|
llm: LlmSettings = LlmSettings()
|
||||||
auth: AuthSettings = AuthSettings()
|
database: DatabaseSettings = DatabaseSettings()
|
||||||
verification: VerificationSettings = VerificationSettings()
|
sensitive_word: SensitiveWordSettings = Field(default_factory=SensitiveWordSettings)
|
||||||
sensitive_word: SensitiveWordSettings = SensitiveWordSettings()
|
|
||||||
test: TestSettings = Field(default_factory=TestSettings)
|
test: TestSettings = Field(default_factory=TestSettings)
|
||||||
|
taskiq: TaskiqSettings = Field(default_factory=TaskiqSettings)
|
||||||
|
|
||||||
@computed_field
|
@computed_field
|
||||||
@property
|
@property
|
||||||
def database_url(self) -> str:
|
def database_url(self) -> str:
|
||||||
return self.database.url
|
return self.database.url
|
||||||
|
|
||||||
|
@computed_field
|
||||||
|
@property
|
||||||
|
def taskiq_broker_url(self) -> str:
|
||||||
|
return self.taskiq.broker_url or self.redis.url
|
||||||
|
|
||||||
|
@computed_field
|
||||||
|
@property
|
||||||
|
def taskiq_result_backend_url(self) -> str:
|
||||||
|
return self.taskiq.result_backend_url or self.redis.url
|
||||||
|
|
||||||
model_config: ClassVar[SettingsConfigDict] = SettingsConfigDict(
|
model_config: ClassVar[SettingsConfigDict] = SettingsConfigDict(
|
||||||
env_file=_resolve_env_file(),
|
env_file=_resolve_env_file(),
|
||||||
env_prefix="ERYAO_",
|
env_prefix="ERYAO_",
|
||||||
|
|||||||
@@ -1,34 +0,0 @@
|
|||||||
input_template: |
|
|
||||||
你正在执行一次"自动化记忆回顾与整理"任务。
|
|
||||||
|
|
||||||
任务目标:
|
|
||||||
1) 回顾最近两天的聊天与上下文,识别用户长期偏好、习惯和关键事实的变化。
|
|
||||||
2) 对已经失效、被否定或明显过期的信息执行遗忘。
|
|
||||||
3) 对新增且有证据支持的信息执行写入。
|
|
||||||
4) 严禁编造;没有证据就不要写入。
|
|
||||||
5) 只更新最小必要字段,避免过度覆盖。
|
|
||||||
|
|
||||||
输出要求:
|
|
||||||
- 必须使用以下固定格式输出:
|
|
||||||
<----------【周期任务输出】---------->
|
|
||||||
【记忆回顾】<一句人性化总结,说明今天主要发生了什么>
|
|
||||||
【新增记忆】<按"X条:要点1;要点2"描述;没有则写"0条">
|
|
||||||
【遗忘记忆】<按"X条:要点1;要点2"描述;没有则写"0条">
|
|
||||||
【未来展望】<基于本次记忆变化,给出1-2条温和、可执行的后续建议;若暂无建议则说明"可继续观察">
|
|
||||||
|
|
||||||
表达风格:
|
|
||||||
- 语言自然、温和、可读,像助理在做每日回顾。
|
|
||||||
- 结论先行,避免空话,不要输出与任务无关的闲聊内容。
|
|
||||||
enabled_tools:
|
|
||||||
- memory.write
|
|
||||||
- memory.forget
|
|
||||||
context:
|
|
||||||
source: latest_chat
|
|
||||||
window_mode: day
|
|
||||||
window_count: 2
|
|
||||||
schedule:
|
|
||||||
type: daily
|
|
||||||
run_at:
|
|
||||||
hour: 8
|
|
||||||
minute: 0
|
|
||||||
weekdays: null
|
|
||||||
@@ -1,158 +0,0 @@
|
|||||||
version: "1.0"
|
|
||||||
routes:
|
|
||||||
- route_id: auth.boot
|
|
||||||
path: /boot
|
|
||||||
description: Bootstraps auth session and redirects to login or home.
|
|
||||||
category: auth
|
|
||||||
auth_required: false
|
|
||||||
- route_id: auth.login
|
|
||||||
path: /login
|
|
||||||
description: Login entry for unauthenticated users.
|
|
||||||
category: auth
|
|
||||||
auth_required: false
|
|
||||||
- route_id: home.main
|
|
||||||
path: /
|
|
||||||
description: Main assistant home screen.
|
|
||||||
category: home
|
|
||||||
auth_required: true
|
|
||||||
- route_id: message.invite_list
|
|
||||||
path: /messages/invites
|
|
||||||
description: Lists message invitations.
|
|
||||||
category: messages
|
|
||||||
auth_required: true
|
|
||||||
- route_id: message.invite_detail
|
|
||||||
path: /messages/invites/{id}
|
|
||||||
description: Shows details for a single invitation.
|
|
||||||
category: messages
|
|
||||||
auth_required: true
|
|
||||||
path_params:
|
|
||||||
- id
|
|
||||||
- route_id: contacts.list
|
|
||||||
path: /contacts
|
|
||||||
description: Contact list and quick relationship actions.
|
|
||||||
category: contacts
|
|
||||||
auth_required: true
|
|
||||||
- route_id: contacts.add
|
|
||||||
path: /contacts/add
|
|
||||||
description: Create or edit a contact profile.
|
|
||||||
category: contacts
|
|
||||||
auth_required: true
|
|
||||||
- route_id: calendar.dayweek
|
|
||||||
path: /calendar/dayweek
|
|
||||||
description: Day and week calendar view.
|
|
||||||
category: calendar
|
|
||||||
auth_required: true
|
|
||||||
query_params:
|
|
||||||
- date
|
|
||||||
- from
|
|
||||||
- route_id: calendar.month
|
|
||||||
path: /calendar/month
|
|
||||||
description: Month calendar overview.
|
|
||||||
category: calendar
|
|
||||||
auth_required: true
|
|
||||||
query_params:
|
|
||||||
- from
|
|
||||||
- route_id: calendar.event_detail
|
|
||||||
path: /calendar/events/{id}
|
|
||||||
description: Detail page for one calendar event.
|
|
||||||
category: calendar
|
|
||||||
auth_required: true
|
|
||||||
path_params:
|
|
||||||
- id
|
|
||||||
- route_id: calendar.event_create
|
|
||||||
path: /calendar/events/new
|
|
||||||
description: Create page for one calendar event.
|
|
||||||
category: calendar
|
|
||||||
auth_required: true
|
|
||||||
query_params:
|
|
||||||
- date
|
|
||||||
- route_id: calendar.event_edit
|
|
||||||
path: /calendar/events/{id}/edit
|
|
||||||
description: Edit page for one calendar event.
|
|
||||||
category: calendar
|
|
||||||
auth_required: true
|
|
||||||
path_params:
|
|
||||||
- id
|
|
||||||
- route_id: calendar.event_share
|
|
||||||
path: /calendar/events/{id}/share
|
|
||||||
description: Share settings page for one calendar event.
|
|
||||||
category: calendar
|
|
||||||
auth_required: true
|
|
||||||
path_params:
|
|
||||||
- id
|
|
||||||
- route_id: todo.list
|
|
||||||
path: /todo
|
|
||||||
description: Todo quadrants and backlog overview.
|
|
||||||
category: todo
|
|
||||||
auth_required: true
|
|
||||||
- route_id: todo.create
|
|
||||||
path: /todo/new
|
|
||||||
description: Create page for one todo item.
|
|
||||||
category: todo
|
|
||||||
auth_required: true
|
|
||||||
- route_id: todo.detail
|
|
||||||
path: /todo/{id}
|
|
||||||
description: Detail page for one todo item.
|
|
||||||
category: todo
|
|
||||||
auth_required: true
|
|
||||||
path_params:
|
|
||||||
- id
|
|
||||||
- route_id: todo.edit
|
|
||||||
path: /todo/{id}/edit
|
|
||||||
description: Dedicated subpage for editing one todo item (not an in-page modal).
|
|
||||||
category: todo
|
|
||||||
auth_required: true
|
|
||||||
path_params:
|
|
||||||
- id
|
|
||||||
- route_id: settings.main
|
|
||||||
path: /settings
|
|
||||||
description: Settings hub page.
|
|
||||||
category: settings
|
|
||||||
auth_required: true
|
|
||||||
- route_id: settings.features
|
|
||||||
path: /settings/features
|
|
||||||
description: Automation job list page.
|
|
||||||
category: settings
|
|
||||||
auth_required: true
|
|
||||||
- route_id: settings.job_new
|
|
||||||
path: /settings/job/new
|
|
||||||
description: Create page for one automation job.
|
|
||||||
category: settings
|
|
||||||
auth_required: true
|
|
||||||
- route_id: settings.job_detail
|
|
||||||
path: /settings/job/{id}
|
|
||||||
description: Detail page for one automation job.
|
|
||||||
category: settings
|
|
||||||
auth_required: true
|
|
||||||
path_params:
|
|
||||||
- id
|
|
||||||
- route_id: settings.memory
|
|
||||||
path: /settings/memory
|
|
||||||
description: Memory preferences and controls.
|
|
||||||
category: settings
|
|
||||||
auth_required: true
|
|
||||||
- route_id: settings.memory_user
|
|
||||||
path: /settings/memory/user
|
|
||||||
description: User memory summary view.
|
|
||||||
category: settings
|
|
||||||
auth_required: true
|
|
||||||
- route_id: settings.memory_work
|
|
||||||
path: /settings/memory/work
|
|
||||||
description: Work memory summary view.
|
|
||||||
category: settings
|
|
||||||
auth_required: true
|
|
||||||
- route_id: settings.memory_user_edit
|
|
||||||
path: /settings/memory/user/edit
|
|
||||||
description: Edit user memory details.
|
|
||||||
category: settings
|
|
||||||
auth_required: true
|
|
||||||
- route_id: settings.memory_work_edit
|
|
||||||
path: /settings/memory/work/edit
|
|
||||||
description: Edit work memory details.
|
|
||||||
category: settings
|
|
||||||
auth_required: true
|
|
||||||
- route_id: settings.edit_profile
|
|
||||||
path: /edit-profile
|
|
||||||
description: Profile editing page.
|
|
||||||
category: settings
|
|
||||||
auth_required: true
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from sqlalchemy import JSON
|
from sqlalchemy import JSON
|
||||||
from sqlalchemy.dialects.mysql import JSON as MySQLJSON
|
from sqlalchemy.dialects.postgresql import JSONB
|
||||||
|
|
||||||
json_type = JSON().with_variant(MySQLJSON, "mysql")
|
json_jsonb = JSON().with_variant(JSONB, "postgresql")
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
__all__ = []
|
||||||
|
|||||||
@@ -5,9 +5,6 @@ import subprocess
|
|||||||
import sys
|
import sys
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from apscheduler.schedulers.asyncio import AsyncIOScheduler
|
|
||||||
from apscheduler.triggers.interval import IntervalTrigger
|
|
||||||
from core.automation.scheduler import run_automation_scheduler_scan
|
|
||||||
from core.config.initial.init_data import initialize_data
|
from core.config.initial.init_data import initialize_data
|
||||||
from core.config.settings import config
|
from core.config.settings import config
|
||||||
from core.logging import get_logger
|
from core.logging import get_logger
|
||||||
@@ -16,7 +13,6 @@ logger = get_logger("core.runtime.cli")
|
|||||||
|
|
||||||
|
|
||||||
def _resolve_alembic_path() -> Path:
|
def _resolve_alembic_path() -> Path:
|
||||||
"""Resolve alembic.ini path relative to project root."""
|
|
||||||
project_root = Path(__file__).parents[3]
|
project_root = Path(__file__).parents[3]
|
||||||
alembic_path = project_root / "alembic" / "alembic.ini"
|
alembic_path = project_root / "alembic" / "alembic.ini"
|
||||||
if not alembic_path.exists():
|
if not alembic_path.exists():
|
||||||
@@ -25,7 +21,6 @@ def _resolve_alembic_path() -> Path:
|
|||||||
|
|
||||||
|
|
||||||
def _redact_sensitive(text: str) -> str:
|
def _redact_sensitive(text: str) -> str:
|
||||||
"""Redact sensitive information from log output."""
|
|
||||||
import re
|
import re
|
||||||
|
|
||||||
SENSITIVE_KEYS = ("password", "token", "secret", "api_key")
|
SENSITIVE_KEYS = ("password", "token", "secret", "api_key")
|
||||||
@@ -40,7 +35,6 @@ def _redact_sensitive(text: str) -> str:
|
|||||||
|
|
||||||
|
|
||||||
def run_migrations() -> bool:
|
def run_migrations() -> bool:
|
||||||
"""Run alembic migrations in a subprocess to avoid event loop conflicts."""
|
|
||||||
import os
|
import os
|
||||||
|
|
||||||
logger.info("Running alembic migrations")
|
logger.info("Running alembic migrations")
|
||||||
@@ -75,7 +69,6 @@ def run_migrations() -> bool:
|
|||||||
|
|
||||||
|
|
||||||
async def run_init_data() -> bool:
|
async def run_init_data() -> bool:
|
||||||
"""Initialize bootstrap data."""
|
|
||||||
logger.info("Running init-data")
|
logger.info("Running init-data")
|
||||||
try:
|
try:
|
||||||
result = await initialize_data()
|
result = await initialize_data()
|
||||||
@@ -90,7 +83,6 @@ async def run_init_data() -> bool:
|
|||||||
|
|
||||||
|
|
||||||
async def bootstrap() -> bool:
|
async def bootstrap() -> bool:
|
||||||
"""Run migrations followed by init-data."""
|
|
||||||
logger.info("Starting bootstrap (migrate + init-data)")
|
logger.info("Starting bootstrap (migrate + init-data)")
|
||||||
|
|
||||||
if not run_migrations():
|
if not run_migrations():
|
||||||
@@ -105,52 +97,11 @@ async def bootstrap() -> bool:
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
async def run_automation_scheduler_forever() -> None:
|
|
||||||
if not config.automation_scheduler.enabled:
|
|
||||||
logger.info("Automation scheduler disabled by config")
|
|
||||||
return
|
|
||||||
|
|
||||||
interval_seconds = int(config.automation_scheduler.interval_seconds)
|
|
||||||
batch_limit = int(config.automation_scheduler.batch_limit)
|
|
||||||
logger.info(
|
|
||||||
"Starting automation scheduler",
|
|
||||||
interval_seconds=interval_seconds,
|
|
||||||
batch_limit=batch_limit,
|
|
||||||
)
|
|
||||||
|
|
||||||
async def scan_job() -> None:
|
|
||||||
try:
|
|
||||||
await run_automation_scheduler_scan(limit=batch_limit)
|
|
||||||
except Exception as exc:
|
|
||||||
logger.exception("Automation scheduler scan failed", error=str(exc))
|
|
||||||
|
|
||||||
scheduler = AsyncIOScheduler()
|
|
||||||
scheduler.add_job(
|
|
||||||
scan_job,
|
|
||||||
trigger=IntervalTrigger(seconds=interval_seconds),
|
|
||||||
id="automation_scheduler_scan",
|
|
||||||
name="Automation scheduler scan",
|
|
||||||
replace_existing=True,
|
|
||||||
max_instances=1,
|
|
||||||
coalesce=True,
|
|
||||||
)
|
|
||||||
scheduler.start()
|
|
||||||
|
|
||||||
stop_event = asyncio.Event()
|
|
||||||
try:
|
|
||||||
await stop_event.wait()
|
|
||||||
finally:
|
|
||||||
scheduler.shutdown(wait=False)
|
|
||||||
|
|
||||||
|
|
||||||
def main() -> int:
|
def main() -> int:
|
||||||
"""CLI entry point."""
|
|
||||||
if len(sys.argv) < 2:
|
if len(sys.argv) < 2:
|
||||||
logger.error("No command provided")
|
logger.error("No command provided")
|
||||||
logger.info("Usage: python -m core.runtime.cli <command>")
|
logger.info("Usage: python -m core.runtime.cli <command>")
|
||||||
logger.info(
|
logger.info("Available commands: migrate, init-data, bootstrap")
|
||||||
"Available commands: migrate, init-data, bootstrap, automation-scheduler"
|
|
||||||
)
|
|
||||||
return 1
|
return 1
|
||||||
|
|
||||||
command = sys.argv[1]
|
command = sys.argv[1]
|
||||||
@@ -161,14 +112,9 @@ def main() -> int:
|
|||||||
success = asyncio.run(run_init_data())
|
success = asyncio.run(run_init_data())
|
||||||
elif command == "bootstrap":
|
elif command == "bootstrap":
|
||||||
success = asyncio.run(bootstrap())
|
success = asyncio.run(bootstrap())
|
||||||
elif command == "automation-scheduler":
|
|
||||||
asyncio.run(run_automation_scheduler_forever())
|
|
||||||
return 0
|
|
||||||
else:
|
else:
|
||||||
logger.error("Unknown command", command=command)
|
logger.error("Unknown command", command=command)
|
||||||
logger.info(
|
logger.info("Available commands: migrate, init-data, bootstrap")
|
||||||
"Available commands: migrate, init-data, bootstrap, automation-scheduler"
|
|
||||||
)
|
|
||||||
return 1
|
return 1
|
||||||
|
|
||||||
return 0 if success else 1
|
return 0 if success else 1
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
__all__ = []
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
from core.taskiq.app import broker, worker_agent_broker, worker_general_broker
|
||||||
|
|
||||||
|
__all__ = ["broker", "worker_agent_broker", "worker_general_broker"]
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from taskiq_redis import ListQueueBroker, RedisAsyncResultBackend
|
||||||
|
|
||||||
|
from core.config.settings import config
|
||||||
|
from core.logging import configure_logging, log_service_banner
|
||||||
|
|
||||||
|
|
||||||
|
configure_logging(config)
|
||||||
|
log_service_banner(
|
||||||
|
service_name=config.runtime.service_name,
|
||||||
|
environment=config.runtime.environment,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _build_broker(queue_name: str) -> ListQueueBroker:
|
||||||
|
return ListQueueBroker(
|
||||||
|
url=config.taskiq_broker_url,
|
||||||
|
queue_name=queue_name,
|
||||||
|
).with_result_backend(
|
||||||
|
RedisAsyncResultBackend(redis_url=config.taskiq_result_backend_url)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
worker_agent_broker = _build_broker("agent")
|
||||||
|
worker_general_broker = _build_broker("general")
|
||||||
|
|
||||||
|
broker = worker_agent_broker
|
||||||
|
|
||||||
|
__all__ = ["broker", "worker_agent_broker", "worker_general_broker"]
|
||||||
@@ -1,12 +1,11 @@
|
|||||||
from . import user, divination, payment, notification, feedback, version, log, violation
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from models.llm import Llm
|
||||||
|
from models.llm_factory import LlmFactory
|
||||||
|
from models.system_agents import SystemAgents
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"user",
|
"Llm",
|
||||||
"divination",
|
"LlmFactory",
|
||||||
"payment",
|
"SystemAgents",
|
||||||
"notification",
|
|
||||||
"feedback",
|
|
||||||
"version",
|
|
||||||
"log",
|
|
||||||
"violation",
|
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -1,41 +0,0 @@
|
|||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from core.db.base import Base, TimestampMixin
|
|
||||||
from sqlalchemy import BigInteger, DateTime, Integer, String, Text, func
|
|
||||||
from sqlalchemy.orm import Mapped, mapped_column
|
|
||||||
|
|
||||||
|
|
||||||
class DivinationRecord(TimestampMixin, Base):
|
|
||||||
__tablename__ = "user_divination_records"
|
|
||||||
|
|
||||||
id: Mapped[int] = mapped_column(BigInteger, primary_key=True, autoincrement=True)
|
|
||||||
user_id: Mapped[int] = mapped_column(BigInteger, nullable=False)
|
|
||||||
trace_id: Mapped[str] = mapped_column(String(64), nullable=False)
|
|
||||||
question: Mapped[str] = mapped_column(Text, nullable=False)
|
|
||||||
question_type: Mapped[str] = mapped_column(String(50), nullable=False)
|
|
||||||
divination_data: Mapped[str] = mapped_column(Text, nullable=False)
|
|
||||||
deepseek_request: Mapped[str] = mapped_column(Text, nullable=False)
|
|
||||||
deepseek_response: Mapped[str | None] = mapped_column(Text, nullable=True)
|
|
||||||
interpretation_result: Mapped[str | None] = mapped_column(Text, nullable=True)
|
|
||||||
api_success: Mapped[bool] = mapped_column(Integer, nullable=False, default=0)
|
|
||||||
error_message: Mapped[str | None] = mapped_column(Text, nullable=True)
|
|
||||||
api_duration_ms: Mapped[int | None] = mapped_column(BigInteger, nullable=True)
|
|
||||||
phone_number: Mapped[str | None] = mapped_column(String(20), nullable=True)
|
|
||||||
|
|
||||||
|
|
||||||
class DivinationHistory(TimestampMixin, Base):
|
|
||||||
__tablename__ = "user_divination_history"
|
|
||||||
|
|
||||||
id: Mapped[int] = mapped_column(BigInteger, primary_key=True, autoincrement=True)
|
|
||||||
user_id: Mapped[int] = mapped_column(BigInteger, nullable=False)
|
|
||||||
phone_number: Mapped[str] = mapped_column(String(20), nullable=False)
|
|
||||||
local_record_id: Mapped[int | None] = mapped_column(BigInteger, nullable=True)
|
|
||||||
json_data: Mapped[str] = mapped_column(Text, nullable=False)
|
|
||||||
ai_result: Mapped[str] = mapped_column(Text, nullable=False)
|
|
||||||
question_type: Mapped[str] = mapped_column(String(50), nullable=False)
|
|
||||||
question: Mapped[str] = mapped_column(Text, nullable=False)
|
|
||||||
timestamp: Mapped[int] = mapped_column(BigInteger, nullable=False)
|
|
||||||
is_active: Mapped[bool] = mapped_column(Integer, nullable=False, default=1)
|
|
||||||
sync_time: Mapped[str] = mapped_column(
|
|
||||||
DateTime, nullable=False, server_default=func.now()
|
|
||||||
)
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from core.db.base import Base
|
|
||||||
from sqlalchemy import BigInteger, DateTime, String, Text, func
|
|
||||||
from sqlalchemy.orm import Mapped, mapped_column
|
|
||||||
|
|
||||||
|
|
||||||
class UserFeedback(Base):
|
|
||||||
__tablename__ = "user_feedback"
|
|
||||||
|
|
||||||
id: Mapped[int] = mapped_column(BigInteger, primary_key=True, autoincrement=True)
|
|
||||||
user_id: Mapped[int] = mapped_column(BigInteger, nullable=False)
|
|
||||||
phone_number: Mapped[str] = mapped_column(String(20), nullable=False)
|
|
||||||
content: Mapped[str] = mapped_column(Text, nullable=False)
|
|
||||||
created_at: Mapped[str] = mapped_column(
|
|
||||||
DateTime, nullable=False, server_default=func.now()
|
|
||||||
)
|
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
from sqlalchemy import ForeignKey, String
|
||||||
|
from sqlalchemy.dialects.postgresql import UUID
|
||||||
|
from sqlalchemy.orm import Mapped, mapped_column
|
||||||
|
|
||||||
|
from core.db.base import Base, SoftDeleteMixin, TimestampMixin
|
||||||
|
|
||||||
|
|
||||||
|
class Llm(TimestampMixin, SoftDeleteMixin, Base):
|
||||||
|
__tablename__: str = "llms"
|
||||||
|
|
||||||
|
id: Mapped[uuid.UUID] = mapped_column(
|
||||||
|
UUID(as_uuid=True), primary_key=True, default=uuid.uuid4
|
||||||
|
)
|
||||||
|
factory_id: Mapped[uuid.UUID] = mapped_column(
|
||||||
|
UUID(as_uuid=True),
|
||||||
|
ForeignKey("llm_factory.id", ondelete="RESTRICT"),
|
||||||
|
nullable=False,
|
||||||
|
index=True,
|
||||||
|
)
|
||||||
|
model_code: Mapped[str] = mapped_column(
|
||||||
|
String(50), nullable=False, unique=True, index=True
|
||||||
|
)
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
from sqlalchemy import String, Text
|
||||||
|
from sqlalchemy.dialects.postgresql import UUID
|
||||||
|
from sqlalchemy.orm import Mapped, mapped_column
|
||||||
|
|
||||||
|
from core.db.base import Base, SoftDeleteMixin, TimestampMixin
|
||||||
|
|
||||||
|
|
||||||
|
class LlmFactory(TimestampMixin, SoftDeleteMixin, Base):
|
||||||
|
__tablename__: str = "llm_factory"
|
||||||
|
|
||||||
|
id: Mapped[uuid.UUID] = mapped_column(
|
||||||
|
UUID(as_uuid=True), primary_key=True, default=uuid.uuid4
|
||||||
|
)
|
||||||
|
name: Mapped[str] = mapped_column(
|
||||||
|
String(50), nullable=False, unique=True, index=True
|
||||||
|
)
|
||||||
|
request_url: Mapped[str] = mapped_column(String(255), nullable=False)
|
||||||
|
avatar: Mapped[str | None] = mapped_column(Text, nullable=True)
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from core.db.base import Base
|
|
||||||
from sqlalchemy import BigInteger, DateTime, Integer, String, Text, func
|
|
||||||
from sqlalchemy.orm import Mapped, mapped_column
|
|
||||||
|
|
||||||
|
|
||||||
class NetworkAccessLog(Base):
|
|
||||||
__tablename__ = "network_access_logs"
|
|
||||||
|
|
||||||
id: Mapped[int] = mapped_column(BigInteger, primary_key=True, autoincrement=True)
|
|
||||||
user_id: Mapped[int | None] = mapped_column(BigInteger, nullable=True)
|
|
||||||
phone_number: Mapped[str | None] = mapped_column(String(20), nullable=True)
|
|
||||||
client_ip: Mapped[str] = mapped_column(String(45), nullable=False)
|
|
||||||
client_port: Mapped[int | None] = mapped_column(Integer, nullable=True)
|
|
||||||
server_ip: Mapped[str] = mapped_column(String(45), nullable=False)
|
|
||||||
server_port: Mapped[int] = mapped_column(Integer, nullable=False)
|
|
||||||
http_method: Mapped[str] = mapped_column(String(10), nullable=False)
|
|
||||||
request_path: Mapped[str] = mapped_column(String(500), nullable=False)
|
|
||||||
request_url: Mapped[str] = mapped_column(String(1000), nullable=False)
|
|
||||||
user_agent: Mapped[str | None] = mapped_column(String(1000), nullable=True)
|
|
||||||
device_info: Mapped[str | None] = mapped_column(Text, nullable=True)
|
|
||||||
response_status: Mapped[int | None] = mapped_column(Integer, nullable=True)
|
|
||||||
processing_time_ms: Mapped[int | None] = mapped_column(BigInteger, nullable=True)
|
|
||||||
request_size: Mapped[int | None] = mapped_column(BigInteger, nullable=True)
|
|
||||||
response_size: Mapped[int | None] = mapped_column(BigInteger, nullable=True)
|
|
||||||
x_forwarded_for: Mapped[str | None] = mapped_column(String(500), nullable=True)
|
|
||||||
x_real_ip: Mapped[str | None] = mapped_column(String(45), nullable=True)
|
|
||||||
referer: Mapped[str | None] = mapped_column(String(1000), nullable=True)
|
|
||||||
operation_type: Mapped[str | None] = mapped_column(String(50), nullable=True)
|
|
||||||
operation_result: Mapped[str | None] = mapped_column(String(20), nullable=True)
|
|
||||||
error_message: Mapped[str | None] = mapped_column(Text, nullable=True)
|
|
||||||
session_id: Mapped[str | None] = mapped_column(String(100), nullable=True)
|
|
||||||
access_time: Mapped[str] = mapped_column(
|
|
||||||
DateTime, nullable=False, server_default=func.now()
|
|
||||||
)
|
|
||||||
created_at: Mapped[str] = mapped_column(
|
|
||||||
DateTime, nullable=False, server_default=func.now()
|
|
||||||
)
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from core.db.base import Base
|
|
||||||
from sqlalchemy import BigInteger, String, Text
|
|
||||||
from sqlalchemy.orm import Mapped, mapped_column
|
|
||||||
|
|
||||||
|
|
||||||
class Notification(Base):
|
|
||||||
__tablename__ = "notification"
|
|
||||||
|
|
||||||
id: Mapped[str] = mapped_column(String(64), primary_key=True)
|
|
||||||
title: Mapped[str] = mapped_column(String(255), nullable=False)
|
|
||||||
content: Mapped[str] = mapped_column(Text, nullable=False)
|
|
||||||
timestamp: Mapped[int] = mapped_column(BigInteger, nullable=False)
|
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from core.db.base import Base, TimestampMixin
|
|
||||||
from sqlalchemy import BigInteger, DateTime, Integer, String, Text, func
|
|
||||||
from sqlalchemy.orm import Mapped, mapped_column
|
|
||||||
|
|
||||||
|
|
||||||
class PaymentOrder(TimestampMixin, Base):
|
|
||||||
__tablename__ = "payment_order"
|
|
||||||
|
|
||||||
id: Mapped[int] = mapped_column(BigInteger, primary_key=True, autoincrement=True)
|
|
||||||
order_no: Mapped[str] = mapped_column(String(64), unique=True, nullable=False)
|
|
||||||
user_id: Mapped[int] = mapped_column(BigInteger, nullable=False)
|
|
||||||
amount: Mapped[str] = mapped_column(String(20), nullable=False)
|
|
||||||
coin_count: Mapped[int] = mapped_column(Integer, nullable=False)
|
|
||||||
subject: Mapped[str] = mapped_column(String(256), nullable=False)
|
|
||||||
body: Mapped[str | None] = mapped_column(String(512), nullable=True)
|
|
||||||
channel: Mapped[str] = mapped_column(String(16), nullable=False)
|
|
||||||
status: Mapped[str] = mapped_column(String(16), nullable=False, default="CREATED")
|
|
||||||
trade_no: Mapped[str | None] = mapped_column(String(64), nullable=True)
|
|
||||||
payment_time: Mapped[str | None] = mapped_column(DateTime, nullable=True)
|
|
||||||
|
|
||||||
|
|
||||||
class PaymentRecord(Base):
|
|
||||||
__tablename__ = "payment_record"
|
|
||||||
|
|
||||||
id: Mapped[int] = mapped_column(BigInteger, primary_key=True, autoincrement=True)
|
|
||||||
order_no: Mapped[str] = mapped_column(String(64), nullable=False)
|
|
||||||
trade_no: Mapped[str] = mapped_column(String(64), nullable=False)
|
|
||||||
user_id: Mapped[int] = mapped_column(BigInteger, nullable=False)
|
|
||||||
channel: Mapped[str] = mapped_column(String(16), nullable=False)
|
|
||||||
notify_type: Mapped[str] = mapped_column(String(16), nullable=False)
|
|
||||||
trade_status: Mapped[str] = mapped_column(String(32), nullable=False)
|
|
||||||
notify_data: Mapped[str] = mapped_column(Text, nullable=False)
|
|
||||||
process_status: Mapped[str] = mapped_column(String(16), nullable=False)
|
|
||||||
process_message: Mapped[str | None] = mapped_column(String(512), nullable=True)
|
|
||||||
coin_added: Mapped[bool] = mapped_column(Integer, nullable=False, default=0)
|
|
||||||
created_at: Mapped[str] = mapped_column(
|
|
||||||
DateTime, nullable=False, server_default=func.now()
|
|
||||||
)
|
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
from sqlalchemy import JSON, ForeignKey, String
|
||||||
|
from sqlalchemy.dialects.postgresql import JSONB, UUID
|
||||||
|
from sqlalchemy.orm import Mapped, mapped_column
|
||||||
|
|
||||||
|
from core.db.base import Base, TimestampMixin
|
||||||
|
|
||||||
|
|
||||||
|
class SystemAgents(TimestampMixin, Base):
|
||||||
|
__tablename__: str = "system_agents"
|
||||||
|
|
||||||
|
agent_type: Mapped[str] = mapped_column(
|
||||||
|
String(20),
|
||||||
|
primary_key=True,
|
||||||
|
)
|
||||||
|
llm_id: Mapped[uuid.UUID] = mapped_column(
|
||||||
|
UUID(as_uuid=True),
|
||||||
|
ForeignKey("llms.id", ondelete="RESTRICT"),
|
||||||
|
nullable=False,
|
||||||
|
)
|
||||||
|
status: Mapped[str] = mapped_column(
|
||||||
|
String(20),
|
||||||
|
nullable=False,
|
||||||
|
)
|
||||||
|
config: Mapped[dict] = mapped_column(
|
||||||
|
JSON().with_variant(JSONB, "postgresql"),
|
||||||
|
nullable=False,
|
||||||
|
server_default="{}",
|
||||||
|
)
|
||||||
@@ -1,52 +0,0 @@
|
|||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from core.db.base import Base, TimestampMixin
|
|
||||||
from sqlalchemy import BigInteger, DateTime, Integer, String, Text, func
|
|
||||||
from sqlalchemy.orm import Mapped, mapped_column
|
|
||||||
|
|
||||||
|
|
||||||
class User(TimestampMixin, Base):
|
|
||||||
__tablename__ = "user_profile"
|
|
||||||
|
|
||||||
id: Mapped[int] = mapped_column(BigInteger, primary_key=True, autoincrement=True)
|
|
||||||
phone_number: Mapped[str] = mapped_column(String(20), unique=True, nullable=False)
|
|
||||||
nickname: Mapped[str] = mapped_column(String(50), nullable=False, default="")
|
|
||||||
gender: Mapped[str] = mapped_column(String(10), nullable=False, default="男")
|
|
||||||
birthday: Mapped[str] = mapped_column(
|
|
||||||
String(20), nullable=False, default="2000-01-01"
|
|
||||||
)
|
|
||||||
signature: Mapped[str] = mapped_column(String(255), nullable=False, default="")
|
|
||||||
|
|
||||||
|
|
||||||
class UserToken(Base):
|
|
||||||
__tablename__ = "user_tokens"
|
|
||||||
|
|
||||||
id: Mapped[int] = mapped_column(BigInteger, primary_key=True, autoincrement=True)
|
|
||||||
user_id: Mapped[int] = mapped_column(BigInteger, nullable=False)
|
|
||||||
token: Mapped[str] = mapped_column(String(255), nullable=False)
|
|
||||||
expire_time: Mapped[str] = mapped_column(DateTime, nullable=False)
|
|
||||||
created_at: Mapped[str] = mapped_column(
|
|
||||||
DateTime, nullable=False, server_default=func.now()
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class VerificationCode(Base):
|
|
||||||
__tablename__ = "verification_codes"
|
|
||||||
|
|
||||||
id: Mapped[int] = mapped_column(BigInteger, primary_key=True, autoincrement=True)
|
|
||||||
phone_number: Mapped[str] = mapped_column(String(20), nullable=False)
|
|
||||||
code: Mapped[str] = mapped_column(String(6), nullable=False)
|
|
||||||
expiration_time: Mapped[str] = mapped_column(DateTime, nullable=False)
|
|
||||||
created_at: Mapped[str] = mapped_column(
|
|
||||||
DateTime, nullable=False, server_default=func.now()
|
|
||||||
)
|
|
||||||
used: Mapped[bool] = mapped_column(Integer, nullable=False, default=0)
|
|
||||||
|
|
||||||
|
|
||||||
class UserCoin(Base):
|
|
||||||
__tablename__ = "user_coin"
|
|
||||||
|
|
||||||
id: Mapped[int] = mapped_column(BigInteger, primary_key=True, autoincrement=True)
|
|
||||||
user_id: Mapped[int] = mapped_column(BigInteger, nullable=False, unique=True)
|
|
||||||
phone_number: Mapped[str] = mapped_column(String(20), nullable=False)
|
|
||||||
coin_balance: Mapped[int] = mapped_column(Integer, nullable=False, default=3)
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from core.db.base import Base, TimestampMixin
|
|
||||||
from sqlalchemy import BigInteger, Integer, String, Text
|
|
||||||
from sqlalchemy.orm import Mapped, mapped_column
|
|
||||||
|
|
||||||
|
|
||||||
class AppVersion(TimestampMixin, Base):
|
|
||||||
__tablename__ = "app_version"
|
|
||||||
|
|
||||||
id: Mapped[int] = mapped_column(BigInteger, primary_key=True, autoincrement=True)
|
|
||||||
version_name: Mapped[str] = mapped_column(String(20), unique=True, nullable=False)
|
|
||||||
version_code: Mapped[int] = mapped_column(Integer, unique=True, nullable=False)
|
|
||||||
min_supported_version: Mapped[str] = mapped_column(String(20), nullable=False)
|
|
||||||
min_supported_code: Mapped[int] = mapped_column(Integer, nullable=False)
|
|
||||||
is_force_update: Mapped[bool] = mapped_column(Integer, nullable=False, default=0)
|
|
||||||
update_message: Mapped[str | None] = mapped_column(Text, nullable=True)
|
|
||||||
download_url: Mapped[str | None] = mapped_column(String(500), nullable=True)
|
|
||||||
is_active: Mapped[bool] = mapped_column(Integer, nullable=False, default=1)
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from core.db.base import Base
|
|
||||||
from sqlalchemy import BigInteger, DateTime, Float, Integer, String, Text, func
|
|
||||||
from sqlalchemy.orm import Mapped, mapped_column
|
|
||||||
|
|
||||||
|
|
||||||
class SensitiveWordViolation(Base):
|
|
||||||
__tablename__ = "sensitive_word_violations"
|
|
||||||
|
|
||||||
id: Mapped[int] = mapped_column(BigInteger, primary_key=True, autoincrement=True)
|
|
||||||
user_id: Mapped[int] = mapped_column(BigInteger, nullable=False)
|
|
||||||
content_type: Mapped[str] = mapped_column(String(20), nullable=False)
|
|
||||||
original_content: Mapped[str] = mapped_column(Text, nullable=False)
|
|
||||||
violation_type: Mapped[str] = mapped_column(String(30), nullable=False)
|
|
||||||
detection_service: Mapped[str] = mapped_column(
|
|
||||||
String(20), nullable=False, default="LOCAL"
|
|
||||||
)
|
|
||||||
risk_level: Mapped[str | None] = mapped_column(String(50), nullable=True)
|
|
||||||
confidence: Mapped[float | None] = mapped_column(Float, nullable=True)
|
|
||||||
aliyun_response: Mapped[str | None] = mapped_column(Text, nullable=True)
|
|
||||||
matched_words: Mapped[str] = mapped_column(Text, nullable=False)
|
|
||||||
client_ip: Mapped[str | None] = mapped_column(String(45), nullable=True)
|
|
||||||
user_agent: Mapped[str | None] = mapped_column(Text, nullable=True)
|
|
||||||
violation_time: Mapped[str] = mapped_column(
|
|
||||||
DateTime, nullable=False, server_default=func.now()
|
|
||||||
)
|
|
||||||
created_at: Mapped[str] = mapped_column(
|
|
||||||
DateTime, nullable=False, server_default=func.now()
|
|
||||||
)
|
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
"""Backend reusable schemas package."""
|
||||||
|
|||||||
@@ -0,0 +1,68 @@
|
|||||||
|
from schemas.agent.forwarded_props import (
|
||||||
|
ClientTimeContext,
|
||||||
|
ForwardedPropsPayload,
|
||||||
|
parse_forwarded_props_client_time,
|
||||||
|
parse_forwarded_props_runtime_mode,
|
||||||
|
)
|
||||||
|
from schemas.agent.forwarded_props import RuntimeMode
|
||||||
|
from schemas.agent.runtime_models import (
|
||||||
|
AgentOutput,
|
||||||
|
ConstraintItem,
|
||||||
|
ExecutionMode,
|
||||||
|
KeyEntity,
|
||||||
|
NormalizedTaskInput,
|
||||||
|
ResultTyping,
|
||||||
|
ResultType,
|
||||||
|
RouterAgentOutput,
|
||||||
|
RunStatus,
|
||||||
|
TaskType,
|
||||||
|
TaskTyping,
|
||||||
|
ToolAgentOutput,
|
||||||
|
ToolStatus,
|
||||||
|
WorkerAgentOutputLite,
|
||||||
|
WorkerAgentOutputRich,
|
||||||
|
resolve_worker_output_model,
|
||||||
|
)
|
||||||
|
from schemas.agent.system_agent import AgentType, SystemAgentLLMConfig
|
||||||
|
from schemas.agent.visibility import SystemVisibilityBit, VisibilityMask, bit_mask
|
||||||
|
from schemas.agent.ui_hints import (
|
||||||
|
UiHintAction,
|
||||||
|
UiHintIntent,
|
||||||
|
UiHintSection,
|
||||||
|
UiHintStatus,
|
||||||
|
UiHintsPayload,
|
||||||
|
)
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"AgentType",
|
||||||
|
"AgentOutput",
|
||||||
|
"ConstraintItem",
|
||||||
|
"ExecutionMode",
|
||||||
|
"ForwardedPropsPayload",
|
||||||
|
"KeyEntity",
|
||||||
|
"NormalizedTaskInput",
|
||||||
|
"ResultTyping",
|
||||||
|
"ClientTimeContext",
|
||||||
|
"ResultType",
|
||||||
|
"RouterAgentOutput",
|
||||||
|
"RunStatus",
|
||||||
|
"RuntimeMode",
|
||||||
|
"TaskType",
|
||||||
|
"TaskTyping",
|
||||||
|
"SystemAgentLLMConfig",
|
||||||
|
"SystemVisibilityBit",
|
||||||
|
"ToolAgentOutput",
|
||||||
|
"ToolStatus",
|
||||||
|
"UiHintAction",
|
||||||
|
"UiHintIntent",
|
||||||
|
"UiHintSection",
|
||||||
|
"UiHintStatus",
|
||||||
|
"UiHintsPayload",
|
||||||
|
"VisibilityMask",
|
||||||
|
"WorkerAgentOutputLite",
|
||||||
|
"WorkerAgentOutputRich",
|
||||||
|
"bit_mask",
|
||||||
|
"parse_forwarded_props_client_time",
|
||||||
|
"parse_forwarded_props_runtime_mode",
|
||||||
|
"resolve_worker_output_model",
|
||||||
|
]
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from datetime import datetime
|
||||||
|
from enum import Enum
|
||||||
|
import re
|
||||||
|
from zoneinfo import ZoneInfo, ZoneInfoNotFoundError
|
||||||
|
|
||||||
|
from pydantic import (
|
||||||
|
BaseModel,
|
||||||
|
ConfigDict,
|
||||||
|
Field,
|
||||||
|
StrictInt,
|
||||||
|
ValidationError,
|
||||||
|
field_validator,
|
||||||
|
)
|
||||||
|
|
||||||
|
_RFC3339_WITH_TZ_PATTERN = re.compile(
|
||||||
|
r"^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})$"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ClientTimeContext(BaseModel):
|
||||||
|
model_config = ConfigDict(extra="forbid")
|
||||||
|
|
||||||
|
device_timezone: str = Field(
|
||||||
|
...,
|
||||||
|
description="IANA timezone from client device, e.g. America/Los_Angeles.",
|
||||||
|
)
|
||||||
|
client_now_iso: str = Field(
|
||||||
|
...,
|
||||||
|
description="RFC3339 datetime with timezone offset from client device.",
|
||||||
|
)
|
||||||
|
client_epoch_ms: StrictInt = Field(
|
||||||
|
...,
|
||||||
|
ge=0,
|
||||||
|
description="Unix epoch milliseconds from client device.",
|
||||||
|
)
|
||||||
|
|
||||||
|
@field_validator("device_timezone")
|
||||||
|
@classmethod
|
||||||
|
def validate_device_timezone(cls, value: str) -> str:
|
||||||
|
try:
|
||||||
|
ZoneInfo(value)
|
||||||
|
except ZoneInfoNotFoundError as exc:
|
||||||
|
raise ValueError("invalid client_time.device_timezone") from exc
|
||||||
|
return value
|
||||||
|
|
||||||
|
@field_validator("client_now_iso")
|
||||||
|
@classmethod
|
||||||
|
def validate_client_now_iso(cls, value: str) -> str:
|
||||||
|
if not _RFC3339_WITH_TZ_PATTERN.fullmatch(value):
|
||||||
|
raise ValueError("invalid client_time.client_now_iso")
|
||||||
|
normalized = value.replace("Z", "+00:00")
|
||||||
|
try:
|
||||||
|
parsed = datetime.fromisoformat(normalized)
|
||||||
|
except ValueError as exc:
|
||||||
|
raise ValueError("invalid client_time.client_now_iso") from exc
|
||||||
|
if parsed.tzinfo is None:
|
||||||
|
raise ValueError("invalid client_time.client_now_iso")
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
class RuntimeMode(str, Enum):
|
||||||
|
CHAT = "chat"
|
||||||
|
AUTOMATION = "automation"
|
||||||
|
|
||||||
|
|
||||||
|
class ForwardedPropsPayload(BaseModel):
|
||||||
|
model_config = ConfigDict(extra="forbid")
|
||||||
|
|
||||||
|
runtime_mode: RuntimeMode
|
||||||
|
client_time: ClientTimeContext | None = None
|
||||||
|
|
||||||
|
|
||||||
|
def parse_forwarded_props(forwarded_props: object) -> ForwardedPropsPayload:
|
||||||
|
if not isinstance(forwarded_props, dict):
|
||||||
|
raise ValueError("invalid RunAgentInput.forwardedProps")
|
||||||
|
try:
|
||||||
|
return ForwardedPropsPayload.model_validate(forwarded_props)
|
||||||
|
except ValidationError as exc:
|
||||||
|
raise ValueError("invalid RunAgentInput.forwardedProps") from exc
|
||||||
|
|
||||||
|
|
||||||
|
def parse_forwarded_props_client_time(
|
||||||
|
forwarded_props: object,
|
||||||
|
) -> ClientTimeContext | None:
|
||||||
|
payload = parse_forwarded_props(forwarded_props)
|
||||||
|
return payload.client_time
|
||||||
|
|
||||||
|
|
||||||
|
def parse_forwarded_props_runtime_mode(forwarded_props: object) -> RuntimeMode:
|
||||||
|
payload = parse_forwarded_props(forwarded_props)
|
||||||
|
return payload.runtime_mode
|
||||||
@@ -0,0 +1,177 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from enum import Enum
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from pydantic import BaseModel, ConfigDict, Field, field_validator
|
||||||
|
|
||||||
|
from schemas.agent.ui_hints import UiHintsPayload
|
||||||
|
|
||||||
|
|
||||||
|
class TaskType(str, Enum):
|
||||||
|
KNOWLEDGE = "knowledge"
|
||||||
|
RECOMMENDATION = "recommendation"
|
||||||
|
PLANNING = "planning"
|
||||||
|
SCHEDULING = "scheduling"
|
||||||
|
REMINDER_MANAGEMENT = "reminder_management"
|
||||||
|
TODO_MANAGEMENT = "todo_management"
|
||||||
|
COMMUNICATION_DRAFTING = "communication_drafting"
|
||||||
|
INFORMATION_ORGANIZATION = "information_organization"
|
||||||
|
STATUS_TRACKING = "status_tracking"
|
||||||
|
TRANSACTION_ASSIST = "transaction_assist"
|
||||||
|
ACTION_EXECUTION = "action_execution"
|
||||||
|
TROUBLESHOOTING = "troubleshooting"
|
||||||
|
UNKNOWN = "unknown"
|
||||||
|
|
||||||
|
|
||||||
|
class ResultType(str, Enum):
|
||||||
|
DIRECT_ANSWER = "direct_answer"
|
||||||
|
OPTIONS_WITH_RECOMMENDATION = "options_with_recommendation"
|
||||||
|
ACTION_PLAN = "action_plan"
|
||||||
|
SCHEDULE_PROPOSAL = "schedule_proposal"
|
||||||
|
TODO_LIST = "todo_list"
|
||||||
|
DRAFT_MESSAGE = "draft_message"
|
||||||
|
SUMMARY = "summary"
|
||||||
|
PROGRESS_SUMMARY = "progress_summary"
|
||||||
|
DIAGNOSIS_REPORT = "diagnosis_report"
|
||||||
|
STRUCTURED_PAYLOAD = "structured_payload"
|
||||||
|
EXECUTION_REPORT = "execution_report"
|
||||||
|
CLARIFICATION_REQUEST = "clarification_request"
|
||||||
|
SAFETY_BLOCK = "safety_block"
|
||||||
|
ERROR_REPORT = "error_report"
|
||||||
|
UNKNOWN = "unknown"
|
||||||
|
|
||||||
|
|
||||||
|
class TaskTyping(BaseModel):
|
||||||
|
model_config = ConfigDict(extra="forbid")
|
||||||
|
|
||||||
|
primary: TaskType
|
||||||
|
secondary: list[TaskType] = Field(default_factory=list, max_length=3)
|
||||||
|
|
||||||
|
|
||||||
|
class ResultTyping(BaseModel):
|
||||||
|
model_config = ConfigDict(extra="forbid")
|
||||||
|
|
||||||
|
primary: ResultType
|
||||||
|
secondary: list[ResultType] = Field(default_factory=list, max_length=3)
|
||||||
|
|
||||||
|
|
||||||
|
class ExecutionMode(str, Enum):
|
||||||
|
ONESTEP = "onestep"
|
||||||
|
TOOL_ASSISTED = "tool_assisted"
|
||||||
|
MULTISTEP = "multistep"
|
||||||
|
|
||||||
|
|
||||||
|
class RunStatus(str, Enum):
|
||||||
|
SUCCESS = "success"
|
||||||
|
PARTIAL_SUCCESS = "partial_success"
|
||||||
|
FAILED = "failed"
|
||||||
|
|
||||||
|
|
||||||
|
class ToolStatus(str, Enum):
|
||||||
|
SUCCESS = "success"
|
||||||
|
FAILURE = "failure"
|
||||||
|
PARTIAL = "partial"
|
||||||
|
|
||||||
|
|
||||||
|
class KeyEntity(BaseModel):
|
||||||
|
model_config = ConfigDict(extra="forbid")
|
||||||
|
|
||||||
|
name: str
|
||||||
|
type: str
|
||||||
|
value: str | None = None
|
||||||
|
|
||||||
|
@field_validator("value", mode="before")
|
||||||
|
@classmethod
|
||||||
|
def normalize_value(cls, value: object) -> object:
|
||||||
|
if value is None:
|
||||||
|
return None
|
||||||
|
if isinstance(value, str):
|
||||||
|
return value
|
||||||
|
if isinstance(value, bool | int | float):
|
||||||
|
return str(value)
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
class ConstraintItem(BaseModel):
|
||||||
|
model_config = ConfigDict(extra="forbid")
|
||||||
|
|
||||||
|
key: str
|
||||||
|
value: str
|
||||||
|
required: bool = True
|
||||||
|
|
||||||
|
@field_validator("value", mode="before")
|
||||||
|
@classmethod
|
||||||
|
def normalize_value(cls, value: object) -> object:
|
||||||
|
if isinstance(value, bool | int | float):
|
||||||
|
return str(value)
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
class NormalizedTaskInput(BaseModel):
|
||||||
|
model_config = ConfigDict(extra="forbid")
|
||||||
|
|
||||||
|
user_text: str
|
||||||
|
multimodal_summary: list[str] = Field(default_factory=list)
|
||||||
|
context_summary: str = Field(default="", max_length=2000)
|
||||||
|
|
||||||
|
|
||||||
|
class RouterAgentOutput(BaseModel):
|
||||||
|
model_config = ConfigDict(extra="forbid")
|
||||||
|
|
||||||
|
normalized_task_input: NormalizedTaskInput
|
||||||
|
key_entities: list[KeyEntity] = Field(default_factory=list)
|
||||||
|
constraints: list[ConstraintItem] = Field(default_factory=list)
|
||||||
|
task_typing: TaskTyping
|
||||||
|
execution_mode: ExecutionMode
|
||||||
|
result_typing: ResultTyping
|
||||||
|
|
||||||
|
|
||||||
|
class ErrorInfo(BaseModel):
|
||||||
|
model_config = ConfigDict(extra="forbid")
|
||||||
|
|
||||||
|
code: str
|
||||||
|
message: str
|
||||||
|
retryable: bool = False
|
||||||
|
details: dict[str, Any] | None = None
|
||||||
|
|
||||||
|
|
||||||
|
class ToolAgentOutput(BaseModel):
|
||||||
|
model_config = ConfigDict(extra="forbid")
|
||||||
|
|
||||||
|
tool_name: str
|
||||||
|
tool_call_id: str
|
||||||
|
tool_call_args: dict[str, Any] | None = None
|
||||||
|
status: ToolStatus
|
||||||
|
result: str
|
||||||
|
error: ErrorInfo | None = None
|
||||||
|
|
||||||
|
|
||||||
|
class WorkerAgentOutputLite(BaseModel):
|
||||||
|
model_config = ConfigDict(extra="forbid")
|
||||||
|
|
||||||
|
status: RunStatus = RunStatus.SUCCESS
|
||||||
|
answer: str
|
||||||
|
key_points: list[str] = Field(default_factory=list)
|
||||||
|
result_type: ResultType = ResultType.UNKNOWN
|
||||||
|
suggested_actions: list[str] = Field(default_factory=list)
|
||||||
|
error: ErrorInfo | None = None
|
||||||
|
|
||||||
|
|
||||||
|
class WorkerAgentOutputRich(WorkerAgentOutputLite):
|
||||||
|
ui_hints: UiHintsPayload | None = None
|
||||||
|
|
||||||
|
|
||||||
|
class AgentOutput(WorkerAgentOutputRich):
|
||||||
|
model_config = ConfigDict(extra="forbid")
|
||||||
|
|
||||||
|
|
||||||
|
WorkerAgentOutput = WorkerAgentOutputLite | WorkerAgentOutputRich
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_worker_output_model(
|
||||||
|
execution_mode: ExecutionMode,
|
||||||
|
) -> type[WorkerAgentOutputLite]:
|
||||||
|
if execution_mode == ExecutionMode.ONESTEP:
|
||||||
|
return WorkerAgentOutputLite
|
||||||
|
return WorkerAgentOutputRich
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from enum import Enum
|
||||||
|
|
||||||
|
from pydantic import BaseModel, Field
|
||||||
|
|
||||||
|
|
||||||
|
class AgentType(str, Enum):
|
||||||
|
ROUTER = "router"
|
||||||
|
WORKER = "worker"
|
||||||
|
|
||||||
|
|
||||||
|
class ContextBuildStrategy(str, Enum):
|
||||||
|
DAY = "day"
|
||||||
|
NUMBER = "number"
|
||||||
|
|
||||||
|
|
||||||
|
class ContextMessagesConfig(BaseModel):
|
||||||
|
mode: ContextBuildStrategy = ContextBuildStrategy.NUMBER
|
||||||
|
count: int = Field(default=20, ge=1, le=200)
|
||||||
|
|
||||||
|
|
||||||
|
class SystemAgentLLMConfig(BaseModel):
|
||||||
|
temperature: float | None = Field(default=None, ge=0.0, le=2.0)
|
||||||
|
max_tokens: int | None = Field(default=None, ge=1)
|
||||||
|
timeout_seconds: float | None = Field(default=30.0, gt=0.0, le=300.0)
|
||||||
|
context_messages: ContextMessagesConfig = Field(
|
||||||
|
default_factory=ContextMessagesConfig
|
||||||
|
)
|
||||||
|
enabled_tools: list[str] = Field(default_factory=list, max_length=32)
|
||||||
@@ -0,0 +1,349 @@
|
|||||||
|
"""
|
||||||
|
UiHints - 描述性 UI 提示
|
||||||
|
|
||||||
|
设计原则:
|
||||||
|
- 描述性而非渲染性: 告诉编译器“要展示什么”,而不是“如何渲染”
|
||||||
|
- 最小化 token: 保持字段简洁
|
||||||
|
- 可编译: 可机械转换为 UiSchemaRenderer
|
||||||
|
- 尽量无损: hints 中的主要内容字段应尽量被保留到 renderer 中
|
||||||
|
|
||||||
|
Version: 2.1
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from enum import Enum
|
||||||
|
import re
|
||||||
|
from typing import Any, ClassVar, Literal
|
||||||
|
|
||||||
|
from pydantic import BaseModel, ConfigDict, Field
|
||||||
|
from pydantic import field_validator
|
||||||
|
|
||||||
|
_NAVIGATION_PATH_PATTERN = re.compile(r"^/[A-Za-z0-9/_-]*$")
|
||||||
|
_NAVIGATION_PARAM_KEY_PATTERN = re.compile(r"^[A-Za-z][A-Za-z0-9_]{0,31}$")
|
||||||
|
_MAX_NAVIGATION_PARAMS = 8
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Enums
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
|
||||||
|
class UiHintStatus(str, Enum):
|
||||||
|
INFO = "info"
|
||||||
|
SUCCESS = "success"
|
||||||
|
WARNING = "warning"
|
||||||
|
ERROR = "error"
|
||||||
|
PENDING = "pending"
|
||||||
|
|
||||||
|
|
||||||
|
class UiHintIntent(str, Enum):
|
||||||
|
"""主要展示意图(弱提示,不应决定字段生死)"""
|
||||||
|
|
||||||
|
MESSAGE = "message" # 普通消息/说明
|
||||||
|
DATA = "data" # 数据/结果摘要
|
||||||
|
LIST = "list" # 列表为主
|
||||||
|
STATUS = "status" # 状态结果为主
|
||||||
|
FORM = "form" # 结构化内容(当前不表示真实输入表单)
|
||||||
|
MIXED = "mixed" # 混合内容
|
||||||
|
|
||||||
|
|
||||||
|
class UiHintActionStyle(str, Enum):
|
||||||
|
PRIMARY = "primary"
|
||||||
|
SECONDARY = "secondary"
|
||||||
|
GHOST = "ghost"
|
||||||
|
DANGER = "danger"
|
||||||
|
|
||||||
|
|
||||||
|
class UiHintTextFormat(str, Enum):
|
||||||
|
PLAIN = "plain"
|
||||||
|
MARKDOWN = "markdown"
|
||||||
|
|
||||||
|
|
||||||
|
class UiHintActionType(str, Enum):
|
||||||
|
NAVIGATION = "navigation"
|
||||||
|
URL = "url"
|
||||||
|
EVENT = "event"
|
||||||
|
TOOL = "tool"
|
||||||
|
COPY = "copy"
|
||||||
|
PAYLOAD = "payload"
|
||||||
|
|
||||||
|
|
||||||
|
class UiHintIconSource(str, Enum):
|
||||||
|
ICON = "icon"
|
||||||
|
EMOJI = "emoji"
|
||||||
|
URL = "url"
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Base Config
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
|
||||||
|
class UiHintBaseModel(BaseModel):
|
||||||
|
model_config: ClassVar[ConfigDict] = ConfigDict(
|
||||||
|
extra="forbid",
|
||||||
|
populate_by_name=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Action Targets
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
|
||||||
|
class UiHintActionNavigation(UiHintBaseModel):
|
||||||
|
type: Literal["navigation"]
|
||||||
|
path: str = Field(..., description="Internal route path.")
|
||||||
|
params: dict[str, Any] | None = Field(default=None, description="Route params.")
|
||||||
|
|
||||||
|
@field_validator("path")
|
||||||
|
@classmethod
|
||||||
|
def validate_navigation_path(cls, value: str) -> str:
|
||||||
|
path = value.strip()
|
||||||
|
if not path:
|
||||||
|
raise ValueError("navigation path must not be empty")
|
||||||
|
if len(path) > 256:
|
||||||
|
raise ValueError("navigation path is too long")
|
||||||
|
if path.startswith("//") or "://" in path:
|
||||||
|
raise ValueError("navigation path must be internal")
|
||||||
|
if "?" in path or "#" in path:
|
||||||
|
raise ValueError("navigation path must not contain query or fragment")
|
||||||
|
if ":" in path:
|
||||||
|
raise ValueError("navigation path must be concrete without placeholders")
|
||||||
|
if _NAVIGATION_PATH_PATTERN.fullmatch(path) is None:
|
||||||
|
raise ValueError("navigation path contains unsupported characters")
|
||||||
|
return path
|
||||||
|
|
||||||
|
@field_validator("params")
|
||||||
|
@classmethod
|
||||||
|
def validate_navigation_params(
|
||||||
|
cls, value: dict[str, Any] | None
|
||||||
|
) -> dict[str, Any] | None:
|
||||||
|
if value is None:
|
||||||
|
return None
|
||||||
|
if len(value) > _MAX_NAVIGATION_PARAMS:
|
||||||
|
raise ValueError("navigation params exceed limit")
|
||||||
|
|
||||||
|
normalized: dict[str, Any] = {}
|
||||||
|
for key, param_value in value.items():
|
||||||
|
if _NAVIGATION_PARAM_KEY_PATTERN.fullmatch(key) is None:
|
||||||
|
raise ValueError("navigation param key is invalid")
|
||||||
|
if isinstance(param_value, (str, int, float, bool)):
|
||||||
|
normalized[key] = param_value
|
||||||
|
continue
|
||||||
|
raise ValueError("navigation params must be scalar")
|
||||||
|
return normalized
|
||||||
|
|
||||||
|
|
||||||
|
class UiHintActionUrl(UiHintBaseModel):
|
||||||
|
type: Literal["url"]
|
||||||
|
url: str = Field(..., description="External URL.")
|
||||||
|
target: Literal["_self", "_blank"] | None = Field(default=None)
|
||||||
|
|
||||||
|
|
||||||
|
class UiHintActionEvent(UiHintBaseModel):
|
||||||
|
type: Literal["event"]
|
||||||
|
event: str = Field(..., description="Frontend event name.")
|
||||||
|
payload: dict[str, Any] | None = Field(default=None)
|
||||||
|
|
||||||
|
|
||||||
|
class UiHintActionTool(UiHintBaseModel):
|
||||||
|
type: Literal["tool"]
|
||||||
|
tool_id: str = Field(alias="toolId", description="Tool identifier.")
|
||||||
|
params: dict[str, Any] | None = Field(default=None)
|
||||||
|
|
||||||
|
|
||||||
|
class UiHintActionCopy(UiHintBaseModel):
|
||||||
|
type: Literal["copy"]
|
||||||
|
content: str = Field(..., description="Content to copy.")
|
||||||
|
success_message: str | None = Field(alias="successMessage", default=None)
|
||||||
|
|
||||||
|
|
||||||
|
class UiHintActionPayload(UiHintBaseModel):
|
||||||
|
type: Literal["payload"]
|
||||||
|
payload: dict[str, Any] = Field(..., description="Structured payload.")
|
||||||
|
submit_to: str | None = Field(alias="submitTo", default=None)
|
||||||
|
|
||||||
|
|
||||||
|
UiHintActionTarget = (
|
||||||
|
UiHintActionNavigation
|
||||||
|
| UiHintActionUrl
|
||||||
|
| UiHintActionEvent
|
||||||
|
| UiHintActionTool
|
||||||
|
| UiHintActionCopy
|
||||||
|
| UiHintActionPayload
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class UiHintAction(UiHintBaseModel):
|
||||||
|
label: str = Field(..., description="Button label.")
|
||||||
|
style: UiHintActionStyle | None = Field(default=None, description="Button style.")
|
||||||
|
disabled: bool = Field(default=False, description="Disabled state.")
|
||||||
|
action: UiHintActionTarget = Field(..., description="Action to execute.")
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Small Descriptive Models
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
|
||||||
|
class UiHintIcon(UiHintBaseModel):
|
||||||
|
source: UiHintIconSource = Field(default=UiHintIconSource.ICON)
|
||||||
|
value: str = Field(..., description="Icon identifier / emoji / url.")
|
||||||
|
color: str | None = Field(default=None)
|
||||||
|
size: int | None = Field(default=None)
|
||||||
|
|
||||||
|
|
||||||
|
class UiHintKvItem(UiHintBaseModel):
|
||||||
|
key: str = Field(..., description="Key identifier.")
|
||||||
|
label: str | None = Field(default=None, description="Display label.")
|
||||||
|
value: Any = Field(default=None, description="Value.")
|
||||||
|
copyable: bool = Field(default=False, description="Allow copy.")
|
||||||
|
|
||||||
|
|
||||||
|
class UiHintListItem(UiHintBaseModel):
|
||||||
|
id: str | None = Field(default=None)
|
||||||
|
title: str = Field(..., description="Item title.")
|
||||||
|
subtitle: str | None = Field(default=None)
|
||||||
|
description: str | None = Field(default=None)
|
||||||
|
icon: UiHintIcon | None = Field(default=None)
|
||||||
|
status: UiHintStatus | None = Field(default=None)
|
||||||
|
actions: list[UiHintAction] = Field(default_factory=list)
|
||||||
|
|
||||||
|
@field_validator("status", mode="before")
|
||||||
|
@classmethod
|
||||||
|
def normalize_status(cls, value: object) -> object:
|
||||||
|
if value is None:
|
||||||
|
return None
|
||||||
|
if isinstance(value, dict):
|
||||||
|
status_type = value.get("type")
|
||||||
|
if isinstance(status_type, str):
|
||||||
|
return status_type
|
||||||
|
status_value = value.get("status")
|
||||||
|
if isinstance(status_value, str):
|
||||||
|
return status_value
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
class UiHintSection(UiHintBaseModel):
|
||||||
|
title: str | None = Field(default=None, description="Section title.")
|
||||||
|
description: str | None = Field(default=None, description="Section description.")
|
||||||
|
icon: UiHintIcon | None = Field(default=None, description="Section icon.")
|
||||||
|
|
||||||
|
content: str | None = Field(default=None, description="Main text content.")
|
||||||
|
content_format: UiHintTextFormat = Field(
|
||||||
|
default=UiHintTextFormat.PLAIN,
|
||||||
|
alias="contentFormat",
|
||||||
|
description="Section content text format.",
|
||||||
|
)
|
||||||
|
|
||||||
|
items: list[UiHintKvItem] = Field(default_factory=list, description="KV items.")
|
||||||
|
list_items: list[UiHintListItem] = Field(
|
||||||
|
default_factory=list,
|
||||||
|
alias="listItems",
|
||||||
|
description="List items.",
|
||||||
|
)
|
||||||
|
actions: list[UiHintAction] = Field(default_factory=list, description="Actions.")
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Root Payload
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
|
||||||
|
class UiHintsPayload(UiHintBaseModel):
|
||||||
|
"""
|
||||||
|
描述性 UI 提示
|
||||||
|
|
||||||
|
设计目标:
|
||||||
|
- agent 输出尽可能短
|
||||||
|
- 不表达布局细节
|
||||||
|
- 编译器负责转换为完整 UiSchemaRenderer
|
||||||
|
"""
|
||||||
|
|
||||||
|
model_config: ClassVar[ConfigDict] = ConfigDict(
|
||||||
|
extra="forbid",
|
||||||
|
populate_by_name=True,
|
||||||
|
json_schema_extra={
|
||||||
|
"examples": [
|
||||||
|
{
|
||||||
|
"intent": "status",
|
||||||
|
"status": "success",
|
||||||
|
"title": "日程已创建",
|
||||||
|
"body": "本次创建已成功完成。",
|
||||||
|
"items": [
|
||||||
|
{"key": "title", "label": "主题", "value": "Q1 规划会议"},
|
||||||
|
{"key": "time", "label": "时间", "value": "2026-03-15 14:00"},
|
||||||
|
],
|
||||||
|
"actions": [
|
||||||
|
{
|
||||||
|
"label": "查看详情",
|
||||||
|
"style": "primary",
|
||||||
|
"action": {
|
||||||
|
"type": "navigation",
|
||||||
|
"path": "/calendar/evt_123",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "删除",
|
||||||
|
"style": "danger",
|
||||||
|
"action": {
|
||||||
|
"type": "tool",
|
||||||
|
"toolId": "calendar.delete",
|
||||||
|
"params": {"eventId": "evt_123"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
version: str = Field(default="2.1")
|
||||||
|
|
||||||
|
intent: UiHintIntent = Field(
|
||||||
|
default=UiHintIntent.MESSAGE,
|
||||||
|
description="Primary display intent.",
|
||||||
|
)
|
||||||
|
status: UiHintStatus = Field(
|
||||||
|
default=UiHintStatus.INFO,
|
||||||
|
description="Overall status.",
|
||||||
|
)
|
||||||
|
|
||||||
|
title: str | None = Field(default=None, description="Top-level title.")
|
||||||
|
description: str | None = Field(default=None, description="Top-level description.")
|
||||||
|
|
||||||
|
body: str | None = Field(default=None, description="Top-level main body text.")
|
||||||
|
body_format: UiHintTextFormat = Field(
|
||||||
|
default=UiHintTextFormat.PLAIN,
|
||||||
|
alias="bodyFormat",
|
||||||
|
description="Body text format.",
|
||||||
|
)
|
||||||
|
|
||||||
|
items: list[UiHintKvItem] = Field(
|
||||||
|
default_factory=list,
|
||||||
|
description="Top-level key-value items.",
|
||||||
|
)
|
||||||
|
list_items: list[UiHintListItem] = Field(
|
||||||
|
default_factory=list,
|
||||||
|
alias="listItems",
|
||||||
|
description="Top-level list items.",
|
||||||
|
)
|
||||||
|
sections: list[UiHintSection] = Field(
|
||||||
|
default_factory=list,
|
||||||
|
description="Grouped sections.",
|
||||||
|
)
|
||||||
|
actions: list[UiHintAction] = Field(
|
||||||
|
default_factory=list,
|
||||||
|
description="Top-level actions.",
|
||||||
|
)
|
||||||
|
|
||||||
|
icon: UiHintIcon | None = Field(
|
||||||
|
default=None,
|
||||||
|
description="Top-level icon.",
|
||||||
|
)
|
||||||
|
meta: dict[str, Any] = Field(
|
||||||
|
default_factory=dict,
|
||||||
|
description="Extra meta, e.g. requestId/toolId/traceId/userId.",
|
||||||
|
)
|
||||||
@@ -0,0 +1,628 @@
|
|||||||
|
"""
|
||||||
|
UI Schema Renderer Protocol
|
||||||
|
|
||||||
|
目标:
|
||||||
|
- 只保留“基础组件 + 布局容器”
|
||||||
|
- 最终返回一个 UiSchemaRenderer
|
||||||
|
- 前端只需要递归渲染 root 布局树即可
|
||||||
|
|
||||||
|
Version: 2.0
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from enum import Enum
|
||||||
|
from typing import Any, Literal, TypedDict, Union
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Enums
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
|
||||||
|
class UiStatus(str, Enum):
|
||||||
|
INFO = "info"
|
||||||
|
SUCCESS = "success"
|
||||||
|
WARNING = "warning"
|
||||||
|
ERROR = "error"
|
||||||
|
PENDING = "pending"
|
||||||
|
|
||||||
|
|
||||||
|
class IconSource(str, Enum):
|
||||||
|
ICON = "icon"
|
||||||
|
EMOJI = "emoji"
|
||||||
|
URL = "url"
|
||||||
|
|
||||||
|
|
||||||
|
class TextFormat(str, Enum):
|
||||||
|
PLAIN = "plain"
|
||||||
|
MARKDOWN = "markdown"
|
||||||
|
|
||||||
|
|
||||||
|
class TextRole(str, Enum):
|
||||||
|
TITLE = "title"
|
||||||
|
SUBTITLE = "subtitle"
|
||||||
|
BODY = "body"
|
||||||
|
CAPTION = "caption"
|
||||||
|
CODE = "code"
|
||||||
|
|
||||||
|
|
||||||
|
class ButtonStyle(str, Enum):
|
||||||
|
PRIMARY = "primary"
|
||||||
|
SECONDARY = "secondary"
|
||||||
|
GHOST = "ghost"
|
||||||
|
DANGER = "danger"
|
||||||
|
|
||||||
|
|
||||||
|
class LayoutDirection(str, Enum):
|
||||||
|
VERTICAL = "vertical"
|
||||||
|
HORIZONTAL = "horizontal"
|
||||||
|
|
||||||
|
|
||||||
|
class LayoutAppearance(str, Enum):
|
||||||
|
PLAIN = "plain"
|
||||||
|
CARD = "card"
|
||||||
|
SECTION = "section"
|
||||||
|
|
||||||
|
|
||||||
|
class LayoutAlign(str, Enum):
|
||||||
|
START = "start"
|
||||||
|
CENTER = "center"
|
||||||
|
END = "end"
|
||||||
|
STRETCH = "stretch"
|
||||||
|
|
||||||
|
|
||||||
|
class LayoutJustify(str, Enum):
|
||||||
|
START = "start"
|
||||||
|
CENTER = "center"
|
||||||
|
END = "end"
|
||||||
|
SPACE_BETWEEN = "space-between"
|
||||||
|
|
||||||
|
|
||||||
|
class RendererTheme(str, Enum):
|
||||||
|
DEFAULT = "default"
|
||||||
|
LIGHT = "light"
|
||||||
|
DARK = "dark"
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Meta
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
|
||||||
|
class UiMeta(TypedDict, total=False):
|
||||||
|
requestId: str
|
||||||
|
toolId: str
|
||||||
|
traceId: str
|
||||||
|
userId: str
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Action Payloads
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
|
||||||
|
class NavigateAction(TypedDict, total=False):
|
||||||
|
type: Literal["navigation"]
|
||||||
|
path: str
|
||||||
|
params: dict[str, Any]
|
||||||
|
|
||||||
|
|
||||||
|
class UrlAction(TypedDict, total=False):
|
||||||
|
type: Literal["url"]
|
||||||
|
url: str
|
||||||
|
target: Literal["_self", "_blank"]
|
||||||
|
|
||||||
|
|
||||||
|
class EventAction(TypedDict, total=False):
|
||||||
|
type: Literal["event"]
|
||||||
|
event: str
|
||||||
|
payload: dict[str, Any]
|
||||||
|
|
||||||
|
|
||||||
|
class ToolAction(TypedDict, total=False):
|
||||||
|
type: Literal["tool"]
|
||||||
|
toolId: str
|
||||||
|
params: dict[str, Any]
|
||||||
|
|
||||||
|
|
||||||
|
class CopyAction(TypedDict, total=False):
|
||||||
|
type: Literal["copy"]
|
||||||
|
content: str
|
||||||
|
successMessage: str
|
||||||
|
|
||||||
|
|
||||||
|
class PayloadAction(TypedDict, total=False):
|
||||||
|
type: Literal["payload"]
|
||||||
|
payload: dict[str, Any]
|
||||||
|
submitTo: str
|
||||||
|
|
||||||
|
|
||||||
|
UiActionPayload = Union[
|
||||||
|
NavigateAction,
|
||||||
|
UrlAction,
|
||||||
|
EventAction,
|
||||||
|
ToolAction,
|
||||||
|
CopyAction,
|
||||||
|
PayloadAction,
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Shared Small Types
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
|
||||||
|
class UiIconSpec(TypedDict, total=False):
|
||||||
|
source: str
|
||||||
|
value: str
|
||||||
|
color: str
|
||||||
|
size: int
|
||||||
|
|
||||||
|
|
||||||
|
class UiKvItem(TypedDict, total=False):
|
||||||
|
key: str
|
||||||
|
label: str
|
||||||
|
value: Any
|
||||||
|
copyable: bool
|
||||||
|
|
||||||
|
|
||||||
|
class UiBaseNode(TypedDict, total=False):
|
||||||
|
id: str
|
||||||
|
visible: bool
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Primitive Components
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
|
||||||
|
class UiTextNode(UiBaseNode, total=False):
|
||||||
|
type: Literal["text"]
|
||||||
|
content: str
|
||||||
|
format: str # TextFormat
|
||||||
|
role: str # TextRole
|
||||||
|
status: str # UiStatus
|
||||||
|
maxLines: int
|
||||||
|
|
||||||
|
|
||||||
|
class UiIconNode(UiBaseNode, total=False):
|
||||||
|
type: Literal["icon"]
|
||||||
|
source: str # IconSource
|
||||||
|
value: str
|
||||||
|
color: str
|
||||||
|
size: int
|
||||||
|
|
||||||
|
|
||||||
|
class UiBadgeNode(UiBaseNode, total=False):
|
||||||
|
type: Literal["badge"]
|
||||||
|
label: str
|
||||||
|
status: str # UiStatus
|
||||||
|
|
||||||
|
|
||||||
|
class UiButtonNode(UiBaseNode, total=False):
|
||||||
|
type: Literal["button"]
|
||||||
|
label: str
|
||||||
|
style: str # ButtonStyle
|
||||||
|
disabled: bool
|
||||||
|
icon: UiIconSpec
|
||||||
|
action: UiActionPayload
|
||||||
|
|
||||||
|
|
||||||
|
class UiKvNode(UiBaseNode, total=False):
|
||||||
|
type: Literal["kv"]
|
||||||
|
items: list[UiKvItem]
|
||||||
|
columns: int
|
||||||
|
|
||||||
|
|
||||||
|
class UiDividerNode(UiBaseNode, total=False):
|
||||||
|
type: Literal["divider"]
|
||||||
|
inset: int
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Layout Containers
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
|
||||||
|
class UiStackNode(UiBaseNode, total=False):
|
||||||
|
type: Literal["stack"]
|
||||||
|
direction: str # LayoutDirection
|
||||||
|
gap: int
|
||||||
|
appearance: str # LayoutAppearance
|
||||||
|
status: str # UiStatus
|
||||||
|
align: str # LayoutAlign
|
||||||
|
justify: str # LayoutJustify
|
||||||
|
wrap: bool
|
||||||
|
children: list["UiNode"]
|
||||||
|
|
||||||
|
|
||||||
|
class UiGridNode(UiBaseNode, total=False):
|
||||||
|
type: Literal["grid"]
|
||||||
|
columns: int
|
||||||
|
gap: int
|
||||||
|
appearance: str # LayoutAppearance
|
||||||
|
status: str # UiStatus
|
||||||
|
children: list["UiNode"]
|
||||||
|
|
||||||
|
|
||||||
|
UiNode = Union[
|
||||||
|
UiTextNode,
|
||||||
|
UiIconNode,
|
||||||
|
UiBadgeNode,
|
||||||
|
UiButtonNode,
|
||||||
|
UiKvNode,
|
||||||
|
UiDividerNode,
|
||||||
|
UiStackNode,
|
||||||
|
UiGridNode,
|
||||||
|
]
|
||||||
|
|
||||||
|
UiLayoutNode = Union[UiStackNode, UiGridNode]
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Root Renderer
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
|
||||||
|
class UiSchemaRenderer(TypedDict, total=False):
|
||||||
|
version: str
|
||||||
|
locale: str
|
||||||
|
status: str # UiStatus
|
||||||
|
theme: str # RendererTheme
|
||||||
|
meta: UiMeta
|
||||||
|
root: UiLayoutNode
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Root Builder
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
|
||||||
|
def build_renderer(
|
||||||
|
root: UiLayoutNode,
|
||||||
|
*,
|
||||||
|
version: str = "2.0",
|
||||||
|
locale: str = "zh-CN",
|
||||||
|
status: UiStatus = UiStatus.INFO,
|
||||||
|
theme: RendererTheme = RendererTheme.DEFAULT,
|
||||||
|
meta: UiMeta | None = None,
|
||||||
|
) -> UiSchemaRenderer:
|
||||||
|
renderer: UiSchemaRenderer = {
|
||||||
|
"version": version,
|
||||||
|
"locale": locale,
|
||||||
|
"status": status.value,
|
||||||
|
"theme": theme.value,
|
||||||
|
"root": root,
|
||||||
|
}
|
||||||
|
if meta:
|
||||||
|
renderer["meta"] = meta
|
||||||
|
return renderer
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Primitive Builders
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
|
||||||
|
def build_text(
|
||||||
|
content: str,
|
||||||
|
*,
|
||||||
|
node_id: str | None = None,
|
||||||
|
format: TextFormat = TextFormat.PLAIN,
|
||||||
|
role: TextRole = TextRole.BODY,
|
||||||
|
status: UiStatus | None = None,
|
||||||
|
max_lines: int | None = None,
|
||||||
|
visible: bool = True,
|
||||||
|
) -> UiTextNode:
|
||||||
|
node: UiTextNode = {
|
||||||
|
"type": "text",
|
||||||
|
"content": content,
|
||||||
|
"format": format.value,
|
||||||
|
"role": role.value,
|
||||||
|
"visible": visible,
|
||||||
|
}
|
||||||
|
if node_id:
|
||||||
|
node["id"] = node_id
|
||||||
|
if status:
|
||||||
|
node["status"] = status.value
|
||||||
|
if max_lines is not None:
|
||||||
|
node["maxLines"] = max_lines
|
||||||
|
return node
|
||||||
|
|
||||||
|
|
||||||
|
def build_icon(
|
||||||
|
source: IconSource,
|
||||||
|
value: str,
|
||||||
|
*,
|
||||||
|
node_id: str | None = None,
|
||||||
|
color: str | None = None,
|
||||||
|
size: int | None = None,
|
||||||
|
visible: bool = True,
|
||||||
|
) -> UiIconNode:
|
||||||
|
node: UiIconNode = {
|
||||||
|
"type": "icon",
|
||||||
|
"source": source.value,
|
||||||
|
"value": value,
|
||||||
|
"visible": visible,
|
||||||
|
}
|
||||||
|
if node_id:
|
||||||
|
node["id"] = node_id
|
||||||
|
if color:
|
||||||
|
node["color"] = color
|
||||||
|
if size is not None:
|
||||||
|
node["size"] = size
|
||||||
|
return node
|
||||||
|
|
||||||
|
|
||||||
|
def build_badge(
|
||||||
|
label: str,
|
||||||
|
*,
|
||||||
|
node_id: str | None = None,
|
||||||
|
status: UiStatus = UiStatus.INFO,
|
||||||
|
visible: bool = True,
|
||||||
|
) -> UiBadgeNode:
|
||||||
|
node: UiBadgeNode = {
|
||||||
|
"type": "badge",
|
||||||
|
"label": label,
|
||||||
|
"status": status.value,
|
||||||
|
"visible": visible,
|
||||||
|
}
|
||||||
|
if node_id:
|
||||||
|
node["id"] = node_id
|
||||||
|
return node
|
||||||
|
|
||||||
|
|
||||||
|
def build_button(
|
||||||
|
label: str,
|
||||||
|
action: UiActionPayload,
|
||||||
|
*,
|
||||||
|
node_id: str | None = None,
|
||||||
|
style: ButtonStyle = ButtonStyle.PRIMARY,
|
||||||
|
disabled: bool = False,
|
||||||
|
icon: UiIconSpec | None = None,
|
||||||
|
visible: bool = True,
|
||||||
|
) -> UiButtonNode:
|
||||||
|
node: UiButtonNode = {
|
||||||
|
"type": "button",
|
||||||
|
"label": label,
|
||||||
|
"style": style.value,
|
||||||
|
"disabled": disabled,
|
||||||
|
"action": action,
|
||||||
|
"visible": visible,
|
||||||
|
}
|
||||||
|
if node_id:
|
||||||
|
node["id"] = node_id
|
||||||
|
if icon:
|
||||||
|
node["icon"] = icon
|
||||||
|
return node
|
||||||
|
|
||||||
|
|
||||||
|
def build_kv(
|
||||||
|
items: list[UiKvItem],
|
||||||
|
*,
|
||||||
|
node_id: str | None = None,
|
||||||
|
columns: int = 1,
|
||||||
|
visible: bool = True,
|
||||||
|
) -> UiKvNode:
|
||||||
|
node: UiKvNode = {
|
||||||
|
"type": "kv",
|
||||||
|
"items": items,
|
||||||
|
"columns": columns,
|
||||||
|
"visible": visible,
|
||||||
|
}
|
||||||
|
if node_id:
|
||||||
|
node["id"] = node_id
|
||||||
|
return node
|
||||||
|
|
||||||
|
|
||||||
|
def build_divider(
|
||||||
|
*,
|
||||||
|
node_id: str | None = None,
|
||||||
|
inset: int = 0,
|
||||||
|
visible: bool = True,
|
||||||
|
) -> UiDividerNode:
|
||||||
|
node: UiDividerNode = {
|
||||||
|
"type": "divider",
|
||||||
|
"inset": inset,
|
||||||
|
"visible": visible,
|
||||||
|
}
|
||||||
|
if node_id:
|
||||||
|
node["id"] = node_id
|
||||||
|
return node
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Layout Builders
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
|
||||||
|
def build_stack(
|
||||||
|
children: list[UiNode],
|
||||||
|
*,
|
||||||
|
node_id: str | None = None,
|
||||||
|
direction: LayoutDirection = LayoutDirection.VERTICAL,
|
||||||
|
gap: int = 12,
|
||||||
|
appearance: LayoutAppearance = LayoutAppearance.PLAIN,
|
||||||
|
status: UiStatus | None = None,
|
||||||
|
align: LayoutAlign = LayoutAlign.START,
|
||||||
|
justify: LayoutJustify = LayoutJustify.START,
|
||||||
|
wrap: bool = False,
|
||||||
|
visible: bool = True,
|
||||||
|
) -> UiStackNode:
|
||||||
|
node: UiStackNode = {
|
||||||
|
"type": "stack",
|
||||||
|
"direction": direction.value,
|
||||||
|
"gap": gap,
|
||||||
|
"appearance": appearance.value,
|
||||||
|
"align": align.value,
|
||||||
|
"justify": justify.value,
|
||||||
|
"wrap": wrap,
|
||||||
|
"children": children,
|
||||||
|
"visible": visible,
|
||||||
|
}
|
||||||
|
if node_id:
|
||||||
|
node["id"] = node_id
|
||||||
|
if status:
|
||||||
|
node["status"] = status.value
|
||||||
|
return node
|
||||||
|
|
||||||
|
|
||||||
|
def build_grid(
|
||||||
|
children: list[UiNode],
|
||||||
|
*,
|
||||||
|
columns: int,
|
||||||
|
node_id: str | None = None,
|
||||||
|
gap: int = 12,
|
||||||
|
appearance: LayoutAppearance = LayoutAppearance.PLAIN,
|
||||||
|
status: UiStatus | None = None,
|
||||||
|
visible: bool = True,
|
||||||
|
) -> UiGridNode:
|
||||||
|
node: UiGridNode = {
|
||||||
|
"type": "grid",
|
||||||
|
"columns": columns,
|
||||||
|
"gap": gap,
|
||||||
|
"appearance": appearance.value,
|
||||||
|
"children": children,
|
||||||
|
"visible": visible,
|
||||||
|
}
|
||||||
|
if node_id:
|
||||||
|
node["id"] = node_id
|
||||||
|
if status:
|
||||||
|
node["status"] = status.value
|
||||||
|
return node
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Small Action Builders
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
|
||||||
|
def action_navigation(
|
||||||
|
path: str, params: dict[str, Any] | None = None
|
||||||
|
) -> NavigateAction:
|
||||||
|
action: NavigateAction = {"type": "navigation", "path": path}
|
||||||
|
if params:
|
||||||
|
action["params"] = params
|
||||||
|
return action
|
||||||
|
|
||||||
|
|
||||||
|
def action_url(url: str, target: Literal["_self", "_blank"] = "_blank") -> UrlAction:
|
||||||
|
return {"type": "url", "url": url, "target": target}
|
||||||
|
|
||||||
|
|
||||||
|
def action_event(event: str, payload: dict[str, Any] | None = None) -> EventAction:
|
||||||
|
action: EventAction = {"type": "event", "event": event}
|
||||||
|
if payload:
|
||||||
|
action["payload"] = payload
|
||||||
|
return action
|
||||||
|
|
||||||
|
|
||||||
|
def action_tool(tool_id: str, params: dict[str, Any] | None = None) -> ToolAction:
|
||||||
|
action: ToolAction = {"type": "tool", "toolId": tool_id}
|
||||||
|
if params:
|
||||||
|
action["params"] = params
|
||||||
|
return action
|
||||||
|
|
||||||
|
|
||||||
|
def action_copy(content: str, success_message: str | None = None) -> CopyAction:
|
||||||
|
action: CopyAction = {"type": "copy", "content": content}
|
||||||
|
if success_message:
|
||||||
|
action["successMessage"] = success_message
|
||||||
|
return action
|
||||||
|
|
||||||
|
|
||||||
|
def action_payload(
|
||||||
|
payload: dict[str, Any], submit_to: str | None = None
|
||||||
|
) -> PayloadAction:
|
||||||
|
action: PayloadAction = {"type": "payload", "payload": payload}
|
||||||
|
if submit_to:
|
||||||
|
action["submitTo"] = submit_to
|
||||||
|
return action
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Derived Helpers (协议外的便捷封装,不是基础原语)
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
|
||||||
|
def build_card(
|
||||||
|
children: list[UiNode],
|
||||||
|
*,
|
||||||
|
node_id: str | None = None,
|
||||||
|
gap: int = 12,
|
||||||
|
status: UiStatus | None = None,
|
||||||
|
) -> UiStackNode:
|
||||||
|
return build_stack(
|
||||||
|
children,
|
||||||
|
node_id=node_id,
|
||||||
|
direction=LayoutDirection.VERTICAL,
|
||||||
|
gap=gap,
|
||||||
|
appearance=LayoutAppearance.CARD,
|
||||||
|
status=status,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def build_section(
|
||||||
|
title: str,
|
||||||
|
children: list[UiNode],
|
||||||
|
*,
|
||||||
|
node_id: str | None = None,
|
||||||
|
description: str | None = None,
|
||||||
|
status: UiStatus | None = None,
|
||||||
|
gap: int = 12,
|
||||||
|
) -> UiStackNode:
|
||||||
|
header_nodes: list[UiNode] = [build_text(title, role=TextRole.TITLE)]
|
||||||
|
if description:
|
||||||
|
header_nodes.append(build_text(description, role=TextRole.CAPTION))
|
||||||
|
|
||||||
|
all_children = header_nodes + children
|
||||||
|
return build_stack(
|
||||||
|
all_children,
|
||||||
|
node_id=node_id,
|
||||||
|
direction=LayoutDirection.VERTICAL,
|
||||||
|
gap=gap,
|
||||||
|
appearance=LayoutAppearance.SECTION,
|
||||||
|
status=status,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def build_status_panel(
|
||||||
|
title: str,
|
||||||
|
message: str,
|
||||||
|
*,
|
||||||
|
status: UiStatus,
|
||||||
|
primary_button: UiButtonNode | None = None,
|
||||||
|
secondary_button: UiButtonNode | None = None,
|
||||||
|
node_id: str | None = None,
|
||||||
|
) -> UiStackNode:
|
||||||
|
status_label = f"ui.status.{status.value}"
|
||||||
|
children: list[UiNode] = [
|
||||||
|
build_stack(
|
||||||
|
[
|
||||||
|
build_text(title, role=TextRole.TITLE),
|
||||||
|
build_badge(label=status_label, status=status),
|
||||||
|
],
|
||||||
|
direction=LayoutDirection.HORIZONTAL,
|
||||||
|
gap=8,
|
||||||
|
align=LayoutAlign.CENTER,
|
||||||
|
justify=LayoutJustify.SPACE_BETWEEN,
|
||||||
|
),
|
||||||
|
build_text(message, role=TextRole.BODY, status=status),
|
||||||
|
]
|
||||||
|
|
||||||
|
actions: list[UiNode] = []
|
||||||
|
if primary_button:
|
||||||
|
actions.append(primary_button)
|
||||||
|
if secondary_button:
|
||||||
|
actions.append(secondary_button)
|
||||||
|
|
||||||
|
if actions:
|
||||||
|
children.append(
|
||||||
|
build_stack(
|
||||||
|
actions,
|
||||||
|
direction=LayoutDirection.HORIZONTAL,
|
||||||
|
gap=8,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
return build_card(children, node_id=node_id, status=status)
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from enum import IntEnum
|
||||||
|
|
||||||
|
from pydantic import BaseModel, ConfigDict, Field, field_validator
|
||||||
|
|
||||||
|
|
||||||
|
class SystemVisibilityBit(IntEnum):
|
||||||
|
UI_HISTORY = 0
|
||||||
|
CONTEXT_ASSEMBLY = 1
|
||||||
|
|
||||||
|
|
||||||
|
class VisibilityMask(BaseModel):
|
||||||
|
model_config = ConfigDict(extra="forbid")
|
||||||
|
|
||||||
|
value: int = Field(..., ge=0, le=(1 << 63) - 1)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_bits(cls, *, bits: list[int]) -> "VisibilityMask":
|
||||||
|
mask = 0
|
||||||
|
for bit in bits:
|
||||||
|
validate_visibility_bit(bit=bit)
|
||||||
|
mask |= 1 << bit
|
||||||
|
return cls(value=mask)
|
||||||
|
|
||||||
|
def contains(self, *, bit: int) -> bool:
|
||||||
|
validate_visibility_bit(bit=bit)
|
||||||
|
return bool(self.value & (1 << bit))
|
||||||
|
|
||||||
|
|
||||||
|
class VisibilityBitRef(BaseModel):
|
||||||
|
model_config = ConfigDict(extra="forbid")
|
||||||
|
|
||||||
|
bit: int = Field(..., ge=0, le=63)
|
||||||
|
|
||||||
|
@field_validator("bit")
|
||||||
|
@classmethod
|
||||||
|
def _validate_bit(cls, value: int) -> int:
|
||||||
|
validate_visibility_bit(bit=value)
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
def validate_visibility_bit(*, bit: int) -> None:
|
||||||
|
if bit < 0 or bit > 63:
|
||||||
|
raise ValueError("visibility bit must be in range [0, 63]")
|
||||||
|
|
||||||
|
|
||||||
|
def bit_mask(*, bit: int) -> int:
|
||||||
|
validate_visibility_bit(bit=bit)
|
||||||
|
return 1 << bit
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|||||||