Add dynamic analyst runtime updates and deployment guides

This commit is contained in:
2026-04-07 09:39:37 +08:00
parent 80ce63da5a
commit 62c7341cf6
45 changed files with 1886 additions and 159 deletions

View File

@@ -0,0 +1,602 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
RED='\033[0;31m'
CYAN='\033[0;36m'
NC='\033[0m'
NON_INTERACTIVE=false
AUTO_INSTALL_DEPS=""
AUTO_INSTALL_SYSTEMD=""
AUTO_START_SYSTEMD=""
AUTO_INSTALL_NGINX=""
AUTO_RELOAD_NGINX=""
AUTO_USE_TLS=""
AUTO_USE_DOCKER=""
log() {
echo -e "${GREEN}[bigtime]${NC} $*"
}
warn() {
echo -e "${YELLOW}[bigtime]${NC} $*"
}
fail() {
echo -e "${RED}[bigtime]${NC} $*" >&2
exit 1
}
ask() {
local prompt="$1"
local default="${2:-}"
if ${NON_INTERACTIVE}; then
printf '%s' "${default}"
return
fi
local value
if [[ -n "${default}" ]]; then
read -r -p "${prompt} [${default}]: " value
printf '%s' "${value:-$default}"
else
read -r -p "${prompt}: " value
printf '%s' "${value}"
fi
}
ask_required() {
local prompt="$1"
local default="${2:-}"
local value=""
while [[ -z "${value}" ]]; do
value="$(ask "${prompt}" "${default}")"
if [[ -z "${value}" ]]; then
warn "该项不能为空,请重新输入。"
fi
done
printf '%s' "${value}"
}
validate_domain_like() {
local value="$1"
[[ -z "${value}" ]] && return 1
[[ "${value}" =~ ^[A-Za-z0-9.-]+$ ]]
}
validate_file_parent_exists_or_rootable() {
local value="$1"
local parent
parent="$(dirname "${value}")"
[[ -d "${parent}" ]] || [[ "${parent}" == "/etc/bigtime" ]] || [[ "${parent}" == "/etc/nginx/conf.d" ]]
}
validate_numeric() {
local value="$1"
[[ "${value}" =~ ^[0-9]+([.][0-9]+)?$ ]]
}
confirm() {
local prompt="$1"
local default="${2:-Y}"
local override="${3:-}"
if [[ -n "${override}" ]]; then
[[ "${override}" =~ ^[Yy]([Ee][Ss])?$|^true$|^1$ ]]
return
fi
if ${NON_INTERACTIVE}; then
[[ "${default}" == "Y" ]]
return
fi
local suffix="[Y/n]"
[[ "${default}" == "N" ]] && suffix="[y/N]"
local value
read -r -p "${prompt} ${suffix}: " value
value="${value:-$default}"
[[ "${value}" =~ ^[Yy]$ ]]
}
command_exists() {
command -v "$1" >/dev/null 2>&1
}
detect_pkg_manager() {
if command_exists apt-get; then
echo "apt"
return
fi
if command_exists dnf; then
echo "dnf"
return
fi
if command_exists yum; then
echo "yum"
return
fi
echo ""
}
install_packages() {
local pkg_manager="$1"
case "${pkg_manager}" in
apt)
sudo apt-get update
sudo apt-get install -y python3 python3-venv python3-pip nginx curl git build-essential nodejs npm
;;
dnf)
sudo dnf install -y python3 python3-pip nginx curl git gcc-c++ make nodejs npm
;;
yum)
sudo yum install -y python3 python3-pip nginx curl git gcc-c++ make nodejs npm
;;
*)
warn "未识别包管理器,跳过依赖安装。请手动安装 python3、venv、pip、nginx、node、npm。"
;;
esac
}
render_systemd_unit() {
local service_name="$1"
local app_module="$2"
local port="$3"
local workers="$4"
local memory_max="$5"
local unit_path="$6"
sudo tee "${unit_path}" >/dev/null <<EOF
[Unit]
Description=BigTime ${service_name}
After=network.target
[Service]
Type=simple
User=${SERVICE_USER}
Group=${SERVICE_GROUP}
WorkingDirectory=${APP_DIR}
EnvironmentFile=${ENV_FILE}
ExecStart=${PYTHON_BIN} -m uvicorn ${app_module} --host 127.0.0.1 --port ${port} --workers ${workers} --log-level warning --no-access-log
Restart=always
RestartSec=3
TimeoutStopSec=30
KillMode=mixed
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=full
ProtectHome=true
LimitNOFILE=65535
TasksMax=4096
MemoryMax=${memory_max}
[Install]
WantedBy=multi-user.target
EOF
}
render_nginx_conf() {
local target="$1"
local use_tls="$2"
if [[ "${use_tls}" == "yes" ]]; then
sudo tee "${target}" >/dev/null <<EOF
server {
listen 80;
server_name ${DOMAIN};
root ${APP_DIR}/frontend/dist;
location /.well-known/acme-challenge/ {
allow all;
}
location / {
return 301 https://\$host\$request_uri;
}
}
server {
listen 443 ssl http2;
server_name ${DOMAIN};
root ${APP_DIR}/frontend/dist;
index index.html;
ssl_certificate ${SSL_CERT_PATH};
ssl_certificate_key ${SSL_KEY_PATH};
include /etc/letsencrypt/options-ssl-nginx.conf;
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
location /ws {
proxy_pass http://127.0.0.1:8765;
proxy_http_version 1.1;
proxy_set_header Upgrade \$http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host \$host;
proxy_set_header X-Real-IP \$remote_addr;
proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto \$scheme;
proxy_read_timeout 300s;
}
location /api/runtime/ {
proxy_pass http://127.0.0.1:8003;
proxy_http_version 1.1;
proxy_set_header Host \$host;
proxy_set_header X-Real-IP \$remote_addr;
proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto \$scheme;
proxy_read_timeout 300s;
}
location /api/dynamic-team/ {
proxy_pass http://127.0.0.1:8003;
proxy_http_version 1.1;
proxy_set_header Host \$host;
proxy_set_header X-Real-IP \$remote_addr;
proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto \$scheme;
proxy_read_timeout 300s;
}
location /api/trading/ {
proxy_pass http://127.0.0.1:8001;
proxy_http_version 1.1;
proxy_set_header Host \$host;
proxy_set_header X-Real-IP \$remote_addr;
proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto \$scheme;
proxy_read_timeout 300s;
}
location /api/news/ {
proxy_pass http://127.0.0.1:8002;
proxy_http_version 1.1;
proxy_set_header Host \$host;
proxy_set_header X-Real-IP \$remote_addr;
proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto \$scheme;
proxy_read_timeout 300s;
}
location /api/ {
proxy_pass http://127.0.0.1:8000;
proxy_http_version 1.1;
proxy_set_header Host \$host;
proxy_set_header X-Real-IP \$remote_addr;
proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto \$scheme;
proxy_read_timeout 300s;
}
location / {
try_files \$uri \$uri/ /index.html;
}
}
EOF
else
sudo tee "${target}" >/dev/null <<EOF
server {
listen 80;
server_name ${DOMAIN};
root ${APP_DIR}/frontend/dist;
index index.html;
location /.well-known/acme-challenge/ {
allow all;
}
location /ws {
proxy_pass http://127.0.0.1:8765;
proxy_http_version 1.1;
proxy_set_header Upgrade \$http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host \$host;
proxy_set_header X-Real-IP \$remote_addr;
proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto \$scheme;
proxy_read_timeout 300s;
}
location /api/runtime/ {
proxy_pass http://127.0.0.1:8003;
proxy_http_version 1.1;
proxy_set_header Host \$host;
proxy_set_header X-Real-IP \$remote_addr;
proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto \$scheme;
proxy_read_timeout 300s;
}
location /api/dynamic-team/ {
proxy_pass http://127.0.0.1:8003;
proxy_http_version 1.1;
proxy_set_header Host \$host;
proxy_set_header X-Real-IP \$remote_addr;
proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto \$scheme;
proxy_read_timeout 300s;
}
location /api/trading/ {
proxy_pass http://127.0.0.1:8001;
proxy_http_version 1.1;
proxy_set_header Host \$host;
proxy_set_header X-Real-IP \$remote_addr;
proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto \$scheme;
proxy_read_timeout 300s;
}
location /api/news/ {
proxy_pass http://127.0.0.1:8002;
proxy_http_version 1.1;
proxy_set_header Host \$host;
proxy_set_header X-Real-IP \$remote_addr;
proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto \$scheme;
proxy_read_timeout 300s;
}
location /api/ {
proxy_pass http://127.0.0.1:8000;
proxy_http_version 1.1;
proxy_set_header Host \$host;
proxy_set_header X-Real-IP \$remote_addr;
proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto \$scheme;
proxy_read_timeout 300s;
}
location / {
try_files \$uri \$uri/ /index.html;
}
}
EOF
fi
}
write_env_file() {
sudo mkdir -p "$(dirname "${ENV_FILE}")"
sudo tee "${ENV_FILE}" >/dev/null <<EOF
AGENT_SERVICE_URL=http://127.0.0.1:8000
TRADING_SERVICE_URL=http://127.0.0.1:8001
NEWS_SERVICE_URL=http://127.0.0.1:8002
RUNTIME_SERVICE_URL=http://127.0.0.1:8003
TICKERS=${TICKERS}
FIN_DATA_SOURCE=${FIN_DATA_SOURCE}
FINANCIAL_DATASETS_API_KEY=${FINANCIAL_DATASETS_API_KEY}
FINNHUB_API_KEY=${FINNHUB_API_KEY}
POLYGON_API_KEY=${POLYGON_API_KEY}
OPENAI_API_KEY=${OPENAI_API_KEY}
OPENAI_BASE_URL=${OPENAI_BASE_URL}
DASHSCOPE_API_KEY=${DASHSCOPE_API_KEY}
MODEL_NAME=${MODEL_NAME}
MEMORY_API_KEY=${MEMORY_API_KEY}
SKILL_SANDBOX_MODE=${SKILL_SANDBOX_MODE}
MAX_COMM_CYCLES=${MAX_COMM_CYCLES}
MARGIN_REQUIREMENT=${MARGIN_REQUIREMENT}
EOF
}
usage() {
cat <<EOF
Usage:
./deploy/install-production.sh [options]
Options:
--non-interactive Run with defaults / env overrides only
--app-dir PATH Application directory
--service-user USER systemd service user
--service-group GROUP systemd service group
--domain DOMAIN Public domain
--env-file PATH Environment file path
--python-bin PATH Python executable path
--tickers CSV Default tickers
--fin-data-source NAME finnhub/yfinance/financial_datasets
--model-name NAME Default model name
--max-comm-cycles N Conference rounds
--margin-requirement NUM Margin requirement
--use-docker-sandbox Set SKILL_SANDBOX_MODE=docker
--no-docker-sandbox Set SKILL_SANDBOX_MODE=none
--with-tls Generate HTTPS nginx config
--without-tls Generate HTTP nginx config
--install-deps Auto install dependencies
--skip-install-deps Skip dependency installation
--install-systemd Install systemd units
--skip-install-systemd Skip systemd unit installation
--start-systemd Enable/start services
--skip-start-systemd Do not start services
--install-nginx Install nginx config
--skip-install-nginx Skip nginx config installation
--reload-nginx Run nginx -t and reload
--skip-reload-nginx Skip nginx reload
--ssl-cert-path PATH TLS certificate path
--ssl-key-path PATH TLS key path
--help Show this help
EOF
}
while [[ $# -gt 0 ]]; do
case "$1" in
--non-interactive) NON_INTERACTIVE=true ;;
--app-dir) APP_DIR="${2:?missing value}"; shift ;;
--service-user) SERVICE_USER="${2:?missing value}"; shift ;;
--service-group) SERVICE_GROUP="${2:?missing value}"; shift ;;
--domain) DOMAIN="${2:?missing value}"; shift ;;
--env-file) ENV_FILE="${2:?missing value}"; shift ;;
--python-bin) PYTHON_BIN="${2:?missing value}"; shift ;;
--tickers) TICKERS="${2:?missing value}"; shift ;;
--fin-data-source) FIN_DATA_SOURCE="${2:?missing value}"; shift ;;
--model-name) MODEL_NAME="${2:?missing value}"; shift ;;
--max-comm-cycles) MAX_COMM_CYCLES="${2:?missing value}"; shift ;;
--margin-requirement) MARGIN_REQUIREMENT="${2:?missing value}"; shift ;;
--use-docker-sandbox) AUTO_USE_DOCKER="Y" ;;
--no-docker-sandbox) AUTO_USE_DOCKER="N" ;;
--with-tls) AUTO_USE_TLS="Y" ;;
--without-tls) AUTO_USE_TLS="N" ;;
--install-deps) AUTO_INSTALL_DEPS="Y" ;;
--skip-install-deps) AUTO_INSTALL_DEPS="N" ;;
--install-systemd) AUTO_INSTALL_SYSTEMD="Y" ;;
--skip-install-systemd) AUTO_INSTALL_SYSTEMD="N" ;;
--start-systemd) AUTO_START_SYSTEMD="Y" ;;
--skip-start-systemd) AUTO_START_SYSTEMD="N" ;;
--install-nginx) AUTO_INSTALL_NGINX="Y" ;;
--skip-install-nginx) AUTO_INSTALL_NGINX="N" ;;
--reload-nginx) AUTO_RELOAD_NGINX="Y" ;;
--skip-reload-nginx) AUTO_RELOAD_NGINX="N" ;;
--ssl-cert-path) SSL_CERT_PATH="${2:?missing value}"; shift ;;
--ssl-key-path) SSL_KEY_PATH="${2:?missing value}"; shift ;;
--help) usage; exit 0 ;;
*) fail "Unknown option: $1" ;;
esac
shift
done
main() {
echo -e "${CYAN}BigTime 生产部署向导${NC}"
echo ""
echo -e "${YELLOW}说明:${NC} 这个脚本适合从一台空机器开始部署当前项目。"
echo -e "${YELLOW}默认推荐:${NC} split-service + systemd + nginx + 静态前端。"
echo ""
if confirm "尝试自动安装基础依赖python3/nginx/node 等)?" "Y" "${AUTO_INSTALL_DEPS}"; then
PKG_MANAGER="$(detect_pkg_manager)"
install_packages "${PKG_MANAGER}"
fi
echo -e "${CYAN}基础配置${NC}"
APP_DIR="${APP_DIR:-$(ask_required '应用部署目录(仓库根目录,建议绝对路径)' "${REPO_ROOT}")}"
[[ -d "${APP_DIR}" ]] || fail "应用目录不存在: ${APP_DIR}"
SERVICE_USER="${SERVICE_USER:-$(ask_required 'systemd 运行用户' "$(id -un)")}"
id "${SERVICE_USER}" >/dev/null 2>&1 || warn "用户 ${SERVICE_USER} 当前不存在,请确认后续 systemd 配置。"
SERVICE_GROUP="${SERVICE_GROUP:-$(ask_required 'systemd 运行用户组' "$(id -gn)")}"
DOMAIN="${DOMAIN:-$(ask_required '部署域名(可填写 IP 或 localhost' 'localhost')}"
validate_domain_like "${DOMAIN}" || warn "域名/IP 形态看起来不标准,请再次确认: ${DOMAIN}"
ENV_FILE="${ENV_FILE:-$(ask_required '环境变量文件路径' '/etc/bigtime/bigtime.env')}"
validate_file_parent_exists_or_rootable "${ENV_FILE}" || warn "环境文件父目录当前不存在,脚本会尝试创建: $(dirname "${ENV_FILE}")"
PYTHON_BIN="${PYTHON_BIN:-$(ask 'Python 可执行文件路径' "${APP_DIR}/.venv/bin/python")}"
[[ -n "${PYTHON_BIN}" ]] || fail "Python 路径不能为空"
echo ""
echo -e "${CYAN}运行参数${NC}"
TICKERS="${TICKERS:-$(ask '默认股票池(逗号分隔)' 'AAPL,MSFT,GOOGL,AMZN,NVDA,META,TSLA,AMD,NFLX,AVGO,PLTR,COIN')}"
FIN_DATA_SOURCE="${FIN_DATA_SOURCE:-$(ask '行情数据源finnhub/yfinance/financial_datasets' 'finnhub')}"
MODEL_NAME="${MODEL_NAME:-$(ask '默认模型名' 'qwen3-max')}"
MAX_COMM_CYCLES="${MAX_COMM_CYCLES:-$(ask_required '最大讨论轮数' '2')}"
validate_numeric "${MAX_COMM_CYCLES}" || fail "最大讨论轮数必须是数字: ${MAX_COMM_CYCLES}"
MARGIN_REQUIREMENT="${MARGIN_REQUIREMENT:-$(ask_required '保证金比例' '0.5')}"
validate_numeric "${MARGIN_REQUIREMENT}" || fail "保证金比例必须是数字: ${MARGIN_REQUIREMENT}"
echo ""
echo -e "${CYAN}密钥配置${NC}"
FINANCIAL_DATASETS_API_KEY="${FINANCIAL_DATASETS_API_KEY:-$(ask 'FINANCIAL_DATASETS_API_KEY可留空' '')}"
FINNHUB_API_KEY="${FINNHUB_API_KEY:-$(ask 'FINNHUB_API_KEYlive 模式建议填写)' '')}"
POLYGON_API_KEY="${POLYGON_API_KEY:-$(ask 'POLYGON_API_KEY可留空' '')}"
OPENAI_API_KEY="${OPENAI_API_KEY:-$(ask 'OPENAI_API_KEY可留空' '')}"
OPENAI_BASE_URL="${OPENAI_BASE_URL:-$(ask 'OPENAI_BASE_URL可留空' '')}"
DASHSCOPE_API_KEY="${DASHSCOPE_API_KEY:-$(ask 'DASHSCOPE_API_KEY可留空' '')}"
MEMORY_API_KEY="${MEMORY_API_KEY:-$(ask 'MEMORY_API_KEY可留空' '')}"
if [[ "${FIN_DATA_SOURCE}" == "finnhub" && -z "${FINNHUB_API_KEY}" ]]; then
warn "你选择了 finnhub 作为数据源,但 FINNHUB_API_KEY 为空。live 模式下通常会失败。"
fi
if [[ -z "${OPENAI_API_KEY}" && -z "${DASHSCOPE_API_KEY}" ]]; then
warn "OPENAI_API_KEY 和 DASHSCOPE_API_KEY 都为空,模型调用可能无法工作。"
fi
if confirm "使用 Docker 沙盒执行技能?" "N" "${AUTO_USE_DOCKER}"; then
SKILL_SANDBOX_MODE="docker"
else
SKILL_SANDBOX_MODE="none"
fi
echo ""
echo -e "${CYAN}当前部署摘要${NC}"
echo " 应用目录: ${APP_DIR}"
echo " 运行用户: ${SERVICE_USER}:${SERVICE_GROUP}"
echo " 域名: ${DOMAIN}"
echo " 环境文件: ${ENV_FILE}"
echo " Python: ${PYTHON_BIN}"
echo " 数据源: ${FIN_DATA_SOURCE}"
echo " 模型: ${MODEL_NAME}"
echo " 沙盒模式: ${SKILL_SANDBOX_MODE}"
echo ""
if ! confirm "确认以上配置并继续写入系统文件?" "Y"; then
fail "用户取消部署。"
fi
if [[ ! -x "${PYTHON_BIN}" ]]; then
warn "未找到 ${PYTHON_BIN},准备创建虚拟环境。"
python3 -m venv "${APP_DIR}/.venv"
"${APP_DIR}/.venv/bin/python" -m pip install --upgrade pip
PYTHON_BIN="${APP_DIR}/.venv/bin/python"
fi
log "安装后端依赖"
"${PYTHON_BIN}" -m pip install -e "${APP_DIR}"
log "构建前端"
(cd "${APP_DIR}/frontend" && npm ci && npm run build)
log "写入环境变量文件 ${ENV_FILE}"
write_env_file
if confirm "生成并安装 systemd unit" "Y" "${AUTO_INSTALL_SYSTEMD}"; then
render_systemd_unit "Agent Service" "backend.apps.agent_service:app" "8000" "1" "1024M" "/etc/systemd/system/bigtime-agent.service"
render_systemd_unit "Trading Service" "backend.apps.trading_service:app" "8001" "1" "768M" "/etc/systemd/system/bigtime-trading.service"
render_systemd_unit "News Service" "backend.apps.news_service:app" "8002" "1" "768M" "/etc/systemd/system/bigtime-news.service"
render_systemd_unit "Runtime Service" "backend.apps.runtime_service:app" "8003" "1" "1536M" "/etc/systemd/system/bigtime-runtime.service"
sudo systemctl daemon-reload
if confirm "立即启用并启动 bigtime-* 服务?" "Y" "${AUTO_START_SYSTEMD}"; then
sudo systemctl enable --now bigtime-agent.service
sudo systemctl enable --now bigtime-trading.service
sudo systemctl enable --now bigtime-news.service
sudo systemctl enable --now bigtime-runtime.service
fi
fi
if confirm "生成并安装 nginx 配置?" "Y" "${AUTO_INSTALL_NGINX}"; then
local use_tls="no"
if confirm "使用 HTTPS/Let's Encrypt 证书路径?" "N" "${AUTO_USE_TLS}"; then
use_tls="yes"
SSL_CERT_PATH="${SSL_CERT_PATH:-$(ask_required 'SSL 证书 fullchain.pem 路径' "/etc/letsencrypt/live/${DOMAIN}/fullchain.pem")}"
SSL_KEY_PATH="${SSL_KEY_PATH:-$(ask_required 'SSL 私钥 privkey.pem 路径' "/etc/letsencrypt/live/${DOMAIN}/privkey.pem")}"
[[ -f "${SSL_CERT_PATH}" ]] || warn "证书文件当前不存在: ${SSL_CERT_PATH}"
[[ -f "${SSL_KEY_PATH}" ]] || warn "私钥文件当前不存在: ${SSL_KEY_PATH}"
else
SSL_CERT_PATH=""
SSL_KEY_PATH=""
fi
NGINX_TARGET="/etc/nginx/conf.d/bigtime.conf"
render_nginx_conf "${NGINX_TARGET}" "${use_tls}"
if confirm "立即执行 nginx -t 并 reload" "Y" "${AUTO_RELOAD_NGINX}"; then
sudo nginx -t
sudo systemctl reload nginx
fi
fi
echo ""
log "部署向导完成"
echo "应用目录: ${APP_DIR}"
echo "环境文件: ${ENV_FILE}"
echo "Python: ${PYTHON_BIN}"
echo "沙盒模式: ${SKILL_SANDBOX_MODE}"
echo ""
echo "验证建议:"
echo " curl http://127.0.0.1:8003/health"
echo " curl http://127.0.0.1:8003/api/runtime/current"
echo " sudo systemctl status bigtime-runtime.service"
echo " tail -f ${APP_DIR}/runs/<run_id>/logs/gateway.log"
}
main "$@"