Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,7 @@ config/*storage-state*.json
config/release-state.json
config/release-storage-state.json
.env
.env.before-*
browser-profile/
data/
logs/
30 changes: 30 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,24 @@ uvicorn license_agent.main:app --host 0.0.0.0 --port 8000
http://你的服务器:8000/dingtalk/webhook
```

## 无公网 IP:启用钉钉 Stream 模式

如果部署机器没有公网 IP 或无法暴露 `/dingtalk/webhook`,可以启用钉钉 Stream 模式。Stream 模式由 Agent 主动连接钉钉,接收用户消息不需要公网回调地址。

在 `.env` 中配置钉钉开放平台应用凭证:

```bash
DINGTALK_STREAM_ENABLED=true
DINGTALK_STREAM_CLIENT_ID=你的钉钉应用Client ID或App Key
DINGTALK_STREAM_CLIENT_SECRET=你的钉钉应用Client Secret或App Secret
DINGTALK_STREAM_REPLY_TIMEOUT_SECONDS=3
DINGTALK_STREAM_ASYNC_SUBMIT=true
```

然后按原方式启动服务即可,`tools/run_agent.py` 和 systemd 都会在 FastAPI 启动时同时启动 Stream 长连接。HTTP webhook 会保留,方便有公网地址时继续使用。

注意:Stream 只解决“接收钉钉消息不需要公网 IP”。下载完成通知可以继续使用自定义机器人 webhook,也可以把 `DINGTALK_NOTIFY_MODE=openapi` 配成企业内部应用机器人 OpenAPI 单聊通知;但如果通知内容是 `/license-files/{token}` 短期下载链接,`PUBLIC_BASE_URL` 仍然需要是用户能访问的地址。没有公网文件地址时,后续应改接钉钉文件上传或对象存储链接。

## Linux 服务器部署

服务器侧脚本和 systemd 模板已经放在 `deploy/` 目录。部署时先在服务器拉取 GitHub 仓库:
Expand Down Expand Up @@ -84,6 +102,18 @@ uvicorn license_agent.main:app --host 0.0.0.0 --port 8000

`RELEASE_DRY_RUN=true` 时,Agent 会把 Release 表单填到提交前并保存截图,不会点击提交。确认流程稳定后,再改为:

生产部署推荐改用服务器持久 Chromium 目录,避免本地登录态上传后因服务端会话、
出口 IP 或浏览器环境变化而失效:

```bash
unset RELEASE_STORAGE_STATE
export RELEASE_USER_DATA_DIR=/AI/lisence-agent/browser-profile
export RELEASE_DATA_DIR=/AI/lisence-agent/data
```

首次登录通过 SSH 隧道和仅监听服务器回环地址的 VNC 完成,具体步骤见
`deploy/README.md`。“storage state 文件”和“持久浏览器目录”只能选择一种。

```bash
export RELEASE_DRY_RUN=false
```
Expand Down
15 changes: 15 additions & 0 deletions config/settings.example.env
Original file line number Diff line number Diff line change
@@ -1,3 +1,18 @@
# DingTalk Stream mode: receive messages without a public callback URL.
DINGTALK_STREAM_ENABLED=false
DINGTALK_STREAM_CLIENT_ID=
DINGTALK_STREAM_CLIENT_SECRET=
DINGTALK_STREAM_REPLY_TIMEOUT_SECONDS=3
DINGTALK_STREAM_ASYNC_SUBMIT=true

# DingTalk OpenAPI app robot notification for non-webhook robots.
DINGTALK_NOTIFY_MODE=openapi
DINGTALK_OPENAPI_CLIENT_ID=
DINGTALK_OPENAPI_CLIENT_SECRET=
DINGTALK_OPENAPI_CORP_ID=
DINGTALK_OPENAPI_ROBOT_CODE=

# DingTalk custom robot webhook for download-finished notifications. Keep this secret.
DINGTALK_WEBHOOK_SECRET=
DINGTALK_ACCESS_TOKEN=
DINGTALK_WEBHOOK_URL=
Expand Down
72 changes: 71 additions & 1 deletion deploy/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,45 @@ sudo bash deploy/install_server.sh

## 3. 上传 Release 登录态

### 推荐:在服务器建立持久浏览器登录态

生产环境优先让登录和授权申请都发生在同一台服务器上。服务器无需桌面环境,
只需安装 `Xvfb` 和 `x11vnc`;VNC 仅监听回环地址并通过 SSH 隧道访问。

```bash
ssh root@10.2.36.19 'apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y x11vnc'
ssh root@10.2.36.19 'cd /AI/lisence-agent && bash deploy/login_release_vnc.sh'
```

脚本会要求设置临时 VNC 密码。随后在 Mac 另开终端建立隧道:

```bash
ssh -N -L 5901:127.0.0.1:5900 root@10.2.36.19
```

打开 Finder 的“前往服务器”,连接 `vnc://127.0.0.1:5901`。完成钉钉登录并确认
授权申请表单已经显示后,在服务器停止可视浏览器并立即用全新的无头进程验证:

```bash
ssh root@10.2.36.19 \
'cd /AI/lisence-agent && bash deploy/stop_release_vnc.sh'
```

服务器 `.env` 使用以下配置,并删除或注释 `RELEASE_STORAGE_STATE`:

```bash
RELEASE_USER_DATA_DIR=/AI/lisence-agent/browser-profile
RELEASE_DATA_DIR=/AI/lisence-agent/data
RELEASE_DRY_RUN=true
RELEASE_HEADLESS=true
```

`RELEASE_USER_DATA_DIR` 与 `RELEASE_STORAGE_STATE` 不能同时配置。VNC 端口无需、也不应
加入云防火墙放行规则。登录完成后 VNC 与 Xvfb 都会停止,Agent 日常以无头模式复用
同一浏览器目录。

### 兼容方式:上传 storage state

在你的本地电脑先重新生成并实时校验登录态:

```bash
Expand Down Expand Up @@ -91,6 +130,37 @@ DINGTALK_WEBHOOK_URL=https://oapi.dingtalk.com/robot/send?access_token=xxx
DINGTALK_WEBHOOK_SECRET=钉钉机器人加签密钥
```

如果服务器没有公网 IP,接收钉钉消息可以改用 Stream 模式,不需要配置机器人 HTTP 回调地址:

```bash
DINGTALK_STREAM_ENABLED=true
DINGTALK_STREAM_CLIENT_ID=你的钉钉应用Client ID或App Key
DINGTALK_STREAM_CLIENT_SECRET=你的钉钉应用Client Secret或App Secret
DINGTALK_STREAM_REPLY_TIMEOUT_SECONDS=3
DINGTALK_STREAM_ASYNC_SUBMIT=true
```

`DINGTALK_STREAM_CLIENT_SECRET` 是 Stream 长连接认证密钥;`DINGTALK_WEBHOOK_SECRET` 是固定机器人 webhook 加签密钥,二者不要混用。

下载完成后的短期链接通知有两种模式:

```bash
# 默认:自定义机器人 webhook 通知
DINGTALK_NOTIFY_MODE=webhook
DINGTALK_WEBHOOK_URL=https://oapi.dingtalk.com/robot/send?access_token=xxx
DINGTALK_WEBHOOK_SECRET=钉钉机器人加签密钥

# Stream/企业内部应用机器人:OpenAPI 单聊通知
DINGTALK_NOTIFY_MODE=openapi
DINGTALK_OPENAPI_CORP_ID=你的企业Corp ID
DINGTALK_OPENAPI_ROBOT_CODE=你的机器人Code
# 可选;为空时复用 DINGTALK_STREAM_CLIENT_ID / DINGTALK_STREAM_CLIENT_SECRET
DINGTALK_OPENAPI_CLIENT_ID=
DINGTALK_OPENAPI_CLIENT_SECRET=
```

无论使用哪种通知模式,短期链接里的 `PUBLIC_BASE_URL` 都需要用户能访问。没有公网文件地址时,后续应改接钉钉文件上传或对象存储链接。

生成下载链接密钥:

```bash
Expand Down Expand Up @@ -140,7 +210,7 @@ curl http://127.0.0.1:8000/health
http://你的服务器公网IP:8000/dingtalk/webhook
```

如果前面接了 HTTPS 域名或反向代理,使用你的公网域名地址。
如果前面接了 HTTPS 域名或反向代理,使用你的公网域名地址。没有公网地址时,启用上面的 Stream 模式即可,不需要填写 HTTP 回调地址。

## 7. 更新代码

Expand Down
23 changes: 22 additions & 1 deletion deploy/agent.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ LICENSE_EXECUTOR=web
RELEASE_DRY_RUN=true
RELEASE_HEADLESS=true
RELEASE_STORAGE_STATE=/opt/license-agent/secrets/release-storage-state.json
# Production alternative: keep login and automation on the same server.
# Do not configure this together with RELEASE_STORAGE_STATE.
# RELEASE_USER_DATA_DIR=/AI/lisence-agent/browser-profile
RELEASE_DATA_DIR=/AI/lisence-agent/data
RELEASE_SCREENSHOT_DIR=/opt/license-agent/traces
RELEASE_LICENSE_BASE_URL=https://release.chaitin.net/license
RELEASE_DOWNLOAD_DIR=/opt/license-agent/downloads
Expand All @@ -30,7 +34,24 @@ PUBLIC_BASE_URL=
DOWNLOAD_TOKEN_SECRET=
DOWNLOAD_LINK_TTL_SECONDS=86400

# DingTalk robot webhook. Keep this secret.
# DingTalk Stream mode: receive messages without a public callback URL.
DINGTALK_STREAM_ENABLED=false
DINGTALK_STREAM_CLIENT_ID=
DINGTALK_STREAM_CLIENT_SECRET=
DINGTALK_STREAM_REPLY_TIMEOUT_SECONDS=3
DINGTALK_STREAM_ASYNC_SUBMIT=true

# Download-finished notification mode: webhook or openapi.
DINGTALK_NOTIFY_MODE=webhook

# DingTalk app robot OpenAPI notification for Stream/non-webhook robots.
# If these are empty, client id/secret fall back to DINGTALK_STREAM_CLIENT_ID/SECRET.
DINGTALK_OPENAPI_CLIENT_ID=
DINGTALK_OPENAPI_CLIENT_SECRET=
DINGTALK_OPENAPI_CORP_ID=
DINGTALK_OPENAPI_ROBOT_CODE=

# DingTalk custom robot webhook for download-finished notifications. Keep this secret.
DINGTALK_WEBHOOK_URL=
DINGTALK_WEBHOOK_SECRET=

Expand Down
16 changes: 13 additions & 3 deletions deploy/check_server.sh
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
#!/usr/bin/env bash
set -euo pipefail

APP_DIR="${APP_DIR:-/opt/license-agent}"
APP_DIR="${APP_DIR:-/AI/lisence-agent}"
ENV_FILE="${ENV_FILE:-$APP_DIR/.env}"

if [[ ! -f "$ENV_FILE" ]]; then
Expand All @@ -16,14 +16,24 @@ set +a
echo "Checking directories..."
test -d "${RELEASE_SCREENSHOT_DIR:-$APP_DIR/traces}"
test -d "${RELEASE_DOWNLOAD_DIR:-$APP_DIR/downloads}"
test -f "${RELEASE_STORAGE_STATE:?RELEASE_STORAGE_STATE is required}"
if [[ -n "${RELEASE_USER_DATA_DIR:-}" ]]; then
test -d "$RELEASE_USER_DATA_DIR"
LOGIN_ARGS=(--user-data-dir "$RELEASE_USER_DATA_DIR")
elif [[ -n "${RELEASE_STORAGE_STATE:-}" ]]; then
test -f "$RELEASE_STORAGE_STATE"
LOGIN_ARGS=(--storage "$RELEASE_STORAGE_STATE")
else
echo "Set RELEASE_USER_DATA_DIR or RELEASE_STORAGE_STATE"
exit 1
fi

echo "Checking Python package..."
"$APP_DIR/.venv/bin/python" -m unittest discover "$APP_DIR/tests"

echo "Checking Release login state..."
"$APP_DIR/.venv/bin/python" "$APP_DIR/tools/check_release_login.py" \
--storage "$RELEASE_STORAGE_STATE" \
"${LOGIN_ARGS[@]}" \
--url "${AUTH_WEB_BASE_URL:-https://release.chaitin.net/license/apply}" \
--live

echo "Checking health endpoint..."
Expand Down
55 changes: 55 additions & 0 deletions deploy/login_release_vnc.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
#!/usr/bin/env bash
set -euo pipefail

APP_DIR="${APP_DIR:-/AI/lisence-agent}"
DISPLAY_NUMBER="${DISPLAY_NUMBER:-99}"
VNC_PORT="${VNC_PORT:-5900}"
RELEASE_USER_DATA_DIR="${RELEASE_USER_DATA_DIR:-$APP_DIR/browser-profile}"
RUNTIME_DIR="${RELEASE_LOGIN_RUNTIME_DIR:-$APP_DIR/data/release-login}"
DISPLAY=":$DISPLAY_NUMBER"

for command_name in Xvfb x11vnc; do
command -v "$command_name" >/dev/null || {
echo "Missing command: $command_name"
exit 1
}
done

if [[ -f "$APP_DIR/logs/agent.pid" ]] && kill -0 "$(cat "$APP_DIR/logs/agent.pid")" 2>/dev/null; then
echo "Stop license-agent before interactive login: $(cat "$APP_DIR/logs/agent.pid")"
exit 1
fi

mkdir -p "$RELEASE_USER_DATA_DIR" "$RUNTIME_DIR"
chmod 700 "$RELEASE_USER_DATA_DIR" "$RUNTIME_DIR"

PASSWORD_FILE="$RUNTIME_DIR/vnc.pass"
if [[ ! -f "$PASSWORD_FILE" ]]; then
read -r -s -p "Set a temporary VNC password: " VNC_PASSWORD
echo
[[ -n "$VNC_PASSWORD" ]] || { echo "VNC password cannot be empty"; exit 1; }
x11vnc -storepasswd "$VNC_PASSWORD" "$PASSWORD_FILE" >/dev/null
unset VNC_PASSWORD
fi
chmod 600 "$PASSWORD_FILE"

Xvfb "$DISPLAY" -screen 0 1440x900x24 -nolisten tcp \
>"$RUNTIME_DIR/xvfb.log" 2>&1 &
echo "$!" >"$RUNTIME_DIR/xvfb.pid"
sleep 1

DISPLAY="$DISPLAY" "$APP_DIR/.venv/bin/python" "$APP_DIR/tools/open_release_profile.py" \
--user-data-dir "$RELEASE_USER_DATA_DIR" \
>"$RUNTIME_DIR/browser.log" 2>&1 &
echo "$!" >"$RUNTIME_DIR/browser.pid"
sleep 2

x11vnc -display "$DISPLAY" -localhost -rfbport "$VNC_PORT" \
-rfbauth "$PASSWORD_FILE" -forever -shared \
>"$RUNTIME_DIR/x11vnc.log" 2>&1 &
echo "$!" >"$RUNTIME_DIR/x11vnc.pid"

echo "Release login browser started."
echo "On your Mac run: ssh -N -L 5901:127.0.0.1:$VNC_PORT root@10.2.36.19"
echo "Then open: vnc://127.0.0.1:5901"
echo "After login, run: bash $APP_DIR/deploy/stop_release_vnc.sh"
33 changes: 33 additions & 0 deletions deploy/stop_release_vnc.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
#!/usr/bin/env bash
set -euo pipefail

APP_DIR="${APP_DIR:-/AI/lisence-agent}"
RELEASE_USER_DATA_DIR="${RELEASE_USER_DATA_DIR:-$APP_DIR/browser-profile}"
RUNTIME_DIR="${RELEASE_LOGIN_RUNTIME_DIR:-$APP_DIR/data/release-login}"

stop_process() {
local name="$1"
local pid_file="$RUNTIME_DIR/$name.pid"
if [[ -f "$pid_file" ]]; then
local pid
pid="$(cat "$pid_file")"
if kill -0 "$pid" 2>/dev/null; then
kill "$pid"
for _ in {1..20}; do
kill -0 "$pid" 2>/dev/null || break
sleep 0.25
done
fi
rm -f "$pid_file"
fi
}

stop_process browser
stop_process x11vnc
stop_process xvfb

RELEASE_DATA_DIR="${RELEASE_DATA_DIR:-$APP_DIR/data}" \
"$APP_DIR/.venv/bin/python" "$APP_DIR/tools/check_release_login.py" \
--user-data-dir "$RELEASE_USER_DATA_DIR" \
--url "${AUTH_WEB_BASE_URL:-https://release.chaitin.net/license/apply}" \
--live
Loading
Loading