refactor: 重构 Tool Result 契约,移除 ui_hints 统一使用 result 字段

- ToolAgentOutput 移除 result_summary 和 ui_hints,统一使用 result 字段
- 日历/用户查找工具移除 ui_hints 输出,改为机器可读的结构化结果
- Agent History 移除 tool 消息的 ui_hints 处理逻辑
- App 版本检查改为 manifest.json 方式,支持多渠道发布
- 更新 settings 配置和测试用例适配新结构
This commit is contained in:
qzl
2026-03-17 12:18:09 +08:00
parent c26cdbbc27
commit aa30fe0ce6
44 changed files with 984 additions and 655 deletions
+4 -7
View File
@@ -19,6 +19,8 @@ SOCIAL_WEB__WORKERS=2
############
# LiteLLM Proxy 网关配置
############
# 可选:覆盖官方 LiteLLM 镜像(默认使用 compose 内置 digest
# SOCIAL_LITELLM_IMAGE=ghcr.io/berriai/litellm@sha256:b959a1816fa454a14d2842242d0fa1cd0d39f96fc94d3a1f4e1de4e48e2398c6
SOCIAL_LITELLM__PORT=3875
############
@@ -85,11 +87,6 @@ SOCIAL_LLM__PROVIDER_KEYS__ZAI=
############
# App 版本更新配置
############
# 安装包目录,相对于项目根目录下的 deploy/static/
SOCIAL_APP_VERSION__RELEASES_DIR=releases
# 当前版本号(语义化版本)
SOCIAL_APP_VERSION__CURRENT_VERSION=0.1.0
# 当前构建号(整数,每次打包递增)
SOCIAL_APP_VERSION__CURRENT_BUILD=1
# 下载链接基础域名(生产环境需配置)
SOCIAL_APP_VERSION__MANIFEST_PATH=deploy/static/releases/manifest.json
SOCIAL_APP_VERSION__RELEASE_PATH_PREFIX=releases
SOCIAL_APP_VERSION__DOWNLOAD_BASE_URL=
+7 -2
View File
@@ -81,7 +81,8 @@ cp deploy/.env.prod.example deploy/.env.prod
### 2) 启动常驻服务
```bash
docker compose --env-file deploy/.env.prod -f deploy/docker-compose.prod.yml up -d redis litellm web worker-critical worker-default worker-bulk
docker compose --env-file deploy/.env.prod -f deploy/docker-compose.prod.yml up -d redis litellm-config-job
docker compose --env-file deploy/.env.prod -f deploy/docker-compose.prod.yml up -d litellm web worker-critical worker-default worker-bulk
```
### 3) 执行一次性 bootstrap
@@ -118,7 +119,11 @@ docker compose --env-file deploy/.env.prod -f deploy/docker-compose.prod.yml up
- 如果 nginx 运行在宿主机,`web` 需要保留 `127.0.0.1:host_port:container_port` 端口映射。
- 如果 nginx 也运行在 Docker 同网络内,可以移除 `web.ports`,改为容器内反向代理(例如 `proxy_pass http://web:5775`)。
### App 安装包下载代理(必须配置)
在 nginx 增加静态目录映射:`location /releases/ { alias /你的项目绝对路径/deploy/static/releases/; }`,这样 `https://你的域名/releases/xxx.apk` 可直接下载安装包。并在 `deploy/.env.prod` 设置 `SOCIAL_APP_VERSION__DOWNLOAD_BASE_URL=https://你的域名``SOCIAL_APP_VERSION__RELEASE_PATH_PREFIX=releases`,确保 `check-updates` 返回的 `download_url` 指向该路径。
## 已知约束
- LiteLLM 会在容器启动时动态生成 `/tmp/litellm-proxy-config.yaml`,依赖 `SOCIAL_LLM__PROVIDER_KEYS__*` 已配置
- LiteLLM 配置由 `litellm-config-job` 一次性生成到共享 volume`litellm_config`)。若更新了 LLM 目录或 Provider Key,需重新执行 `up -d litellm-config-job` 后重启 `litellm`
- `init-job` 为一次性任务,不长期驻留。
+178
View File
@@ -0,0 +1,178 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(git rev-parse --show-toplevel)"
APPS_DIR="$ROOT_DIR/apps"
OUTPUT_APK="$APPS_DIR/build/app/outputs/flutter-apk/app-release.apk"
RELEASES_DIR="$ROOT_DIR/deploy/static/releases"
MANIFEST_FILE="$RELEASES_DIR/manifest.json"
BACKEND_HOST=""
CHANNEL="release"
RELEASE_NOTES=""
while [[ $# -gt 0 ]]; do
case "$1" in
--backend-host)
BACKEND_HOST="$2"
shift 2
;;
--channel)
CHANNEL="$2"
shift 2
;;
--release-notes)
RELEASE_NOTES="$2"
shift 2
;;
*)
printf 'Unknown arg: %s\n' "$1" >&2
exit 1
;;
esac
done
if [[ -z "$BACKEND_HOST" ]]; then
printf 'Usage: %s --backend-host <ip-or-domain> [--channel release] [--release-notes "..."]\n' "$0" >&2
exit 1
fi
if [[ "$BACKEND_HOST" == *":"* ]]; then
printf 'Invalid backend host: do not include port (%s)\n' "$BACKEND_HOST" >&2
exit 1
fi
if ! [[ "$CHANNEL" =~ ^[a-z0-9_-]+$ ]]; then
printf 'Invalid channel: %s\n' "$CHANNEL" >&2
exit 1
fi
BACKEND_URL="http://$BACKEND_HOST"
readarray -t VERSION_INFO < <(
python - <<'PY'
import re
from pathlib import Path
text = Path("apps/pubspec.yaml").read_text(encoding="utf-8")
match = re.search(r"^version:\s*([0-9]+\.[0-9]+\.[0-9]+)\+([0-9]+)\s*$", text, re.MULTILINE)
if not match:
raise SystemExit("pubspec.yaml version format invalid")
print(match.group(1))
print(match.group(2))
PY
)
VERSION_NAME="${VERSION_INFO[0]}"
CURRENT_VERSION_CODE="${VERSION_INFO[1]}"
NEXT_VERSION_CODE="$((CURRENT_VERSION_CODE + 1))"
FILE_NAME="social-app-android-v${VERSION_NAME}+${NEXT_VERSION_CODE}-${CHANNEL}.apk"
TARGET_APK="$RELEASES_DIR/$FILE_NAME"
mkdir -p "$RELEASES_DIR"
pushd "$APPS_DIR" >/dev/null
flutter build apk --release \
--build-name="$VERSION_NAME" \
--build-number="$NEXT_VERSION_CODE" \
--dart-define="BACKEND_URL=$BACKEND_URL" \
--target-platform=android-arm64 \
--split-per-abi \
--target "lib/main.dart"
popd >/dev/null
SOURCE_APK="$APPS_DIR/build/app/outputs/flutter-apk/app-arm64-v8a-release.apk"
if [[ ! -f "$SOURCE_APK" ]]; then
SOURCE_APK="$OUTPUT_APK"
fi
cp "$SOURCE_APK" "$TARGET_APK"
FILE_SIZE="$(stat -c%s "$TARGET_APK")"
SHA256="$(sha256sum "$TARGET_APK" | cut -d' ' -f1)"
python - <<'PY' "$ROOT_DIR/apps/pubspec.yaml" "$VERSION_NAME" "$NEXT_VERSION_CODE"
import re
import sys
from pathlib import Path
pubspec_path = Path(sys.argv[1])
version_name = sys.argv[2]
version_code = sys.argv[3]
content = pubspec_path.read_text(encoding="utf-8")
updated, count = re.subn(
r"^version:\s*[0-9]+\.[0-9]+\.[0-9]+\+[0-9]+\s*$",
f"version: {version_name}+{version_code}",
content,
count=1,
flags=re.MULTILINE,
)
if count != 1:
raise SystemExit("failed to update version in pubspec.yaml")
pubspec_path.write_text(updated, encoding="utf-8")
PY
RELEASE_NOTES="$RELEASE_NOTES" python - <<'PY' "$MANIFEST_FILE" "$FILE_NAME" "$VERSION_NAME" "$NEXT_VERSION_CODE" "$CHANNEL" "$FILE_SIZE" "$SHA256"
import json
import os
import sys
from pathlib import Path
manifest_path = Path(sys.argv[1])
file_name = sys.argv[2]
version_name = sys.argv[3]
version_code = int(sys.argv[4])
channel = sys.argv[5]
file_size = int(sys.argv[6])
sha256 = sys.argv[7]
release_notes = os.environ.get("RELEASE_NOTES", "")
if manifest_path.is_file():
data = json.loads(manifest_path.read_text(encoding="utf-8"))
else:
data = {"releases": []}
releases = data.get("releases") or []
releases = [
item
for item in releases
if not (
item.get("platform") == "android"
and item.get("channel") == channel
and item.get("version_code") == version_code
)
]
releases.append(
{
"platform": "android",
"channel": channel,
"version_name": version_name,
"version_code": version_code,
"min_supported_version_code": version_code,
"file_name": file_name,
"release_notes": release_notes or None,
"file_size": file_size,
"sha256": sha256,
}
)
releases.sort(
key=lambda item: (
item.get("platform", ""),
item.get("channel", ""),
item.get("version_code", 0),
)
)
manifest_path.write_text(
json.dumps({"releases": releases}, indent=2, ensure_ascii=True) + "\n",
encoding="utf-8",
)
PY
printf 'Build completed\n'
printf 'Version: %s+%s\n' "$VERSION_NAME" "$NEXT_VERSION_CODE"
printf 'Previous buildNumber: %s\n' "$CURRENT_VERSION_CODE"
printf 'Backend URL: %s\n' "$BACKEND_URL"
printf 'Package: %s\n' "$TARGET_APK"
printf 'SHA256: %s\n' "$SHA256"
+31 -11
View File
@@ -17,19 +17,38 @@ services:
timeout: 3s
retries: 10
litellm:
litellm-config-job:
image: ${SOCIAL_BACKEND_IMAGE:-social-app-backend:prod}
container_name: social-prod-litellm
restart: unless-stopped
container_name: social-prod-litellm-config-job
restart: "no"
env_file:
- ./.env.prod
environment:
- PYTHONPATH=/app/backend/src
- PYTHONDONTWRITEBYTECODE=1
- SOCIAL_REDIS__HOST=redis
- SOCIAL_REDIS__PORT=6379
command: >
sh -c '.venv/bin/python backend/scripts/build_litellm_proxy_config.py --output /tmp/litellm-proxy-config.yaml && .venv/bin/litellm --config /tmp/litellm-proxy-config.yaml --host ${SOCIAL_LITELLM__BIND_HOST:-0.0.0.0} --port ${SOCIAL_LITELLM__PORT:-3875}'
.venv/bin/python backend/scripts/build_litellm_proxy_config.py --output /config/litellm-proxy-config.yaml
volumes:
- litellm_config:/config
depends_on:
redis:
condition: service_healthy
litellm:
image: ${SOCIAL_LITELLM_IMAGE:-ghcr.io/berriai/litellm@sha256:b959a1816fa454a14d2842242d0fa1cd0d39f96fc94d3a1f4e1de4e48e2398c6}
container_name: social-prod-litellm
restart: unless-stopped
env_file:
- ./.env.prod
command: >
--config /config/litellm-proxy-config.yaml --host ${SOCIAL_LITELLM__BIND_HOST:-0.0.0.0} --port ${SOCIAL_LITELLM__PORT:-3875}
volumes:
- litellm_config:/config:ro
depends_on:
redis:
condition: service_healthy
litellm-config-job:
condition: service_completed_successfully
healthcheck:
test:
[
@@ -68,7 +87,7 @@ services:
condition: service_healthy
volumes:
- ../logs:/app/logs
- ./static/releases:/app/static/releases:ro
- ./static/releases:/app/deploy/static/releases:ro
healthcheck:
test:
[
@@ -105,7 +124,7 @@ services:
condition: service_healthy
volumes:
- ../logs:/app/logs
- ./static/releases:/app/static/releases:ro
- ./static/releases:/app/deploy/static/releases:ro
worker-default:
image: ${SOCIAL_BACKEND_IMAGE:-social-app-backend:prod}
@@ -131,7 +150,7 @@ services:
condition: service_healthy
volumes:
- ../logs:/app/logs
- ./static/releases:/app/static/releases:ro
- ./static/releases:/app/deploy/static/releases:ro
worker-bulk:
image: ${SOCIAL_BACKEND_IMAGE:-social-app-backend:prod}
@@ -157,7 +176,7 @@ services:
condition: service_healthy
volumes:
- ../logs:/app/logs
- ./static/releases:/app/static/releases:ro
- ./static/releases:/app/deploy/static/releases:ro
init-job:
image: ${SOCIAL_BACKEND_IMAGE:-social-app-backend:prod}
@@ -182,9 +201,10 @@ services:
condition: service_healthy
volumes:
- ../logs:/app/logs
- ./static/releases:/app/static/releases:ro
- ./static/releases:/app/deploy/static/releases:ro
profiles:
- job
volumes:
redis_data:
litellm_config:
+26
View File
@@ -0,0 +1,26 @@
{
"releases": [
{
"platform": "android",
"channel": "release",
"version_name": "0.1.0",
"version_code": 1,
"min_supported_version_code": 1,
"file_name": "social-app-android-v0.1.0+1-release.apk",
"release_notes": "\u95ee\u9898\u4fee\u590d\u548c\u4f53\u9a8c\u4f18\u5316",
"file_size": 21371504,
"sha256": "6cf53601f36e0037b6de909ea3567d1e18a1bcec1164e1b70d88c1802eafd44b"
},
{
"platform": "android",
"channel": "release",
"version_name": "0.1.0",
"version_code": 2,
"min_supported_version_code": 2,
"file_name": "social-app-android-v0.1.0+2-release.apk",
"release_notes": "\u95ee\u9898\u4fee\u590d\u548c\u4f53\u9a8c\u4f18\u5316",
"file_size": 21371504,
"sha256": "8f769bda3ba5414dfd5712ac026d8a13663990b7e83a4e92b8e85caf9945d5eb"
}
]
}