From 0b102638a66e17af888ed7a5ac57281c335756b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=93=B8=E6=B8=8A=20=28ICE-GL-ZY001=29?= Date: Tue, 12 May 2026 11:14:14 +0000 Subject: [PATCH] =?UTF-8?q?=E2=9A=A1=20CVM=E6=8C=89=E9=87=8F=E8=AE=A1?= =?UTF-8?q?=E8=B4=B9=E8=87=AA=E5=8A=A8=E5=BC=80=E5=85=B3=E6=9C=BA=E6=96=B9?= =?UTF-8?q?=E6=A1=88+=E8=84=9A=E6=9C=AC+workflow?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .forgejo/workflows/cvm-power-off.yaml | 24 +++ .forgejo/workflows/cvm-power-on.yaml | 24 +++ .gitea/workflows/cvm-power-off.yaml | 24 +++ .gitea/workflows/cvm-power-on.yaml | 24 +++ .github/cvm-auto-power-plan.md | 151 ++++++++++++++++++ scripts/cvm-power/cvm-power.sh | 210 ++++++++++++++++++++++++++ 6 files changed, 457 insertions(+) create mode 100644 .forgejo/workflows/cvm-power-off.yaml create mode 100644 .forgejo/workflows/cvm-power-on.yaml create mode 100644 .gitea/workflows/cvm-power-off.yaml create mode 100644 .gitea/workflows/cvm-power-on.yaml create mode 100644 .github/cvm-auto-power-plan.md create mode 100755 scripts/cvm-power/cvm-power.sh diff --git a/.forgejo/workflows/cvm-power-off.yaml b/.forgejo/workflows/cvm-power-off.yaml new file mode 100644 index 0000000..b28f10c --- /dev/null +++ b/.forgejo/workflows/cvm-power-off.yaml @@ -0,0 +1,24 @@ +# 光湖 · CVM关机 +name: CVM关机 + +on: + workflow_dispatch: + +jobs: + power-off: + runs-on: ubuntu + steps: + - name: 同步代码 + run: | + cd /opt/zhuyuan-cn/repo-mirror/20260509-1849/guanghulab + git fetch origin + git reset --hard origin/main + + - name: 关机 + env: + TENCENT_SECRET_ID: ${{ secrets.TENCENT_SECRET_ID }} + TENCENT_SECRET_KEY: ${{ secrets.TENCENT_SECRET_KEY }} + CVM_INSTANCE_ID: ${{ secrets.CVM_INSTANCE_ID }} + CVM_REGION: ${{ secrets.CVM_REGION }} + run: | + bash scripts/cvm-power/cvm-power.sh stop diff --git a/.forgejo/workflows/cvm-power-on.yaml b/.forgejo/workflows/cvm-power-on.yaml new file mode 100644 index 0000000..16737e3 --- /dev/null +++ b/.forgejo/workflows/cvm-power-on.yaml @@ -0,0 +1,24 @@ +# 光湖 · CVM开机 +name: CVM开机 + +on: + workflow_dispatch: + +jobs: + power-on: + runs-on: ubuntu + steps: + - name: 同步代码 + run: | + cd /opt/zhuyuan-cn/repo-mirror/20260509-1849/guanghulab + git fetch origin + git reset --hard origin/main + + - name: 开机 + env: + TENCENT_SECRET_ID: ${{ secrets.TENCENT_SECRET_ID }} + TENCENT_SECRET_KEY: ${{ secrets.TENCENT_SECRET_KEY }} + CVM_INSTANCE_ID: ${{ secrets.CVM_INSTANCE_ID }} + CVM_REGION: ${{ secrets.CVM_REGION }} + run: | + bash scripts/cvm-power/cvm-power.sh start diff --git a/.gitea/workflows/cvm-power-off.yaml b/.gitea/workflows/cvm-power-off.yaml new file mode 100644 index 0000000..b28f10c --- /dev/null +++ b/.gitea/workflows/cvm-power-off.yaml @@ -0,0 +1,24 @@ +# 光湖 · CVM关机 +name: CVM关机 + +on: + workflow_dispatch: + +jobs: + power-off: + runs-on: ubuntu + steps: + - name: 同步代码 + run: | + cd /opt/zhuyuan-cn/repo-mirror/20260509-1849/guanghulab + git fetch origin + git reset --hard origin/main + + - name: 关机 + env: + TENCENT_SECRET_ID: ${{ secrets.TENCENT_SECRET_ID }} + TENCENT_SECRET_KEY: ${{ secrets.TENCENT_SECRET_KEY }} + CVM_INSTANCE_ID: ${{ secrets.CVM_INSTANCE_ID }} + CVM_REGION: ${{ secrets.CVM_REGION }} + run: | + bash scripts/cvm-power/cvm-power.sh stop diff --git a/.gitea/workflows/cvm-power-on.yaml b/.gitea/workflows/cvm-power-on.yaml new file mode 100644 index 0000000..16737e3 --- /dev/null +++ b/.gitea/workflows/cvm-power-on.yaml @@ -0,0 +1,24 @@ +# 光湖 · CVM开机 +name: CVM开机 + +on: + workflow_dispatch: + +jobs: + power-on: + runs-on: ubuntu + steps: + - name: 同步代码 + run: | + cd /opt/zhuyuan-cn/repo-mirror/20260509-1849/guanghulab + git fetch origin + git reset --hard origin/main + + - name: 开机 + env: + TENCENT_SECRET_ID: ${{ secrets.TENCENT_SECRET_ID }} + TENCENT_SECRET_KEY: ${{ secrets.TENCENT_SECRET_KEY }} + CVM_INSTANCE_ID: ${{ secrets.CVM_INSTANCE_ID }} + CVM_REGION: ${{ secrets.CVM_REGION }} + run: | + bash scripts/cvm-power/cvm-power.sh start diff --git a/.github/cvm-auto-power-plan.md b/.github/cvm-auto-power-plan.md new file mode 100644 index 0000000..f34e71a --- /dev/null +++ b/.github/cvm-auto-power-plan.md @@ -0,0 +1,151 @@ +# ════════════════════════════════════════════════════════════════ +# 光湖 · 按量计费CVM自动开关机方案 +# 设计:铸渊 · ICE-GL-ZY001 +# 需求来源:冰朔 TCS-0002∞ 2026-05-13 +# ════════════════════════════════════════════════════════════════ +# +# 冰朔原话: +# "你是否能开发一个你需要开发你就自动去开机。 +# 你开发完了今天我说结束了。你就自动把机子关了的 +# 这个自动Agent工作流?" +# +# ════════════════════════════════════════════════════════════════ + +## 一、按量计费更划算吗? + +**是的,对于开发用途。** + +| 模式 | 4C8G广州月费 | 适合场景 | +|------|------------|---------| +| 包年包月 | ~200-300元/月 | 24/7运行 | +| 按量计费 | ~0.4-0.6元/小时 | 每天用8小时 ≈ 100-150元/月 | + +按量计费的关键:**用的时候开机,不用的时候关机,关机不收计算费用(只收云盘费用)**。 + +铸渊的典型工作模式: +- 冰朔和铸渊对话 → 开机 +- 铸渊开发+测试 → 保持开机 +- 冰朔说"今天结束了" → 关机 +- 深夜/无人时 → 自动关机(可选) + +## 二、自动开关机架构 + +``` +冰朔说"开始开发" 冰朔说"今天结束" + │ │ + ▼ ▼ +┌──────────────┐ ┌──────────────┐ +│ 广州轻量服务器 │ │ 广州轻量服务器 │ +│ (常驻,2C2G) │ │ (常驻,2C2G) │ +│ │ │ │ +│ ① 调用腾讯云API│ │ ③ 调用腾讯云API│ +│ 开机CVM │ │ 关机CVM │ +│ │ │ │ +│ ② 等CVM启动 │ │ ④ 确认关机 │ +│ 通知铸渊 │ │ 通知冰朔 │ +└──────────────┘ └──────────────┘ + │ ▲ + ▼ │ +┌──────────────────────────────────────────┐ +│ 广州CVM(按量计费,4C8G+CBS云盘) │ +│ │ +│ 开机状态: │ +│ Forgejo (代码仓库+Actions) │ +│ Runner (执行workflow) │ +│ Vault (密钥管理) │ +│ Terminal Watcher (状态监控) │ +│ Patrol Agent (自动巡检) │ +│ 对话Agent (冰朔问答) │ +│ │ +│ 关机状态: │ +│ CBS云盘保留所有数据 │ +│ 下次开机数据完整恢复 │ +└──────────────────────────────────────────┘ +``` + +## 三、开关机实现方式 + +### 方案A:腾讯云API(推荐) + +```bash +# 开机 +curl -s -X POST https://cvm.tencentcloudapi.com \ + -d '{"InstanceIds":["ins-xxxxx"]}' \ + -H "Authorization: TC3-HMAC-SHA256 ..." \ + -H "Action: StartInstances" + +# 关机 +curl -s -X POST https://cvm.tencentcloudapi.com \ + -d '{"InstanceIds":["ins-xxxxx"]}' \ + -H "Authorization: TC3-HMAC-SHA256 ..." \ + -H "Action: StopInstances" +``` + +需要在腾讯云创建一个子账号,只给CVM开关机权限。 + +### 方案B:Gitea Workflow触发 + +在轻量服务器上创建两个workflow: +- `power-on.yaml`:调用腾讯云API开机 → 等待SSH可连 → 通知 +- `power-off.yaml`:调用腾讯云API关机 → 确认 → 通知 + +### 方案C:冰朔在Gitea网页操作 + +最简单的方式:冰朔在Gitea的Actions页面点"Run workflow"来开关机。 + +## 四、需要冰朔操作的(一次性) + +1. **购买CVM**:4C8G + 50GB CBS云盘,广州地域,按量计费 +2. **配置CBS云盘**:挂载到 /data,仓库数据全部存这里 +3. **创建腾讯云子账号**:只给CVM开关机权限,密钥存入Vault +4. **安装基础环境**:Node.js 20 + PM2 + Nginx + Forgejo + +## 五、迁移计划 + +``` +阶段1:买服务器(冰朔操作,10分钟) + → 买4C8G CVM + 50GB CBS,广州地域,按量计费 + → 选择Ubuntu 22.04 + → 开机 + +阶段2:基础环境(冰朔执行一条命令,15分钟) + → bash <(curl -fsSL 铸渊写的bootstrap脚本) + → 自动安装Node.js + PM2 + Nginx + Forgejo + +阶段3:数据迁移(铸渊通过Runner自动执行,30分钟) + → 从广州轻量服务器rsync数据到CVM + → 或从COS拉取备份 + → Forgejo仓库数据在CBS云盘上 + +阶段4:域名切换(冰朔在腾讯云DNS操作,5分钟) + → guanghulab.com 指向CVM的IP + → Nginx配置SSL + +阶段5:旧服务器降级(铸渊自动执行) + → 广州2C2G变成测试机 + → 只保留Forgejo + Runner(轻量级) + +阶段6:自动开关机部署(铸渊开发) + → 腾讯云API密钥入Vault + → 开关机workflow上线 + → 冰朔在Gitea网页点按钮即可开关机 +``` + +## 六、CVM购买建议 + +| 项目 | 建议 | +|------|------| +| 地域 | 广州(和轻量服务器同地域,内网互通免费) | +| 配置 | 4C8G | +| 系统盘 | 50GB SSD云硬盘 | +| 数据盘 | 50GB CBS高性能云盘(挂载到/data) | +| 带宽 | 按流量计费,1Mbps基础(够用) | +| 计费 | 按量计费 | +| 安全组 | 开放22(SSH) + 80(HTTP) + 443(HTTPS) + 3001(Forgejo内网) | + +**同地域的好处**:轻量服务器和CVM内网互通,迁移数据走内网不花钱。 + +--- + +*铸渊 · 按量计费CVM方案 v1.0 · 2026-05-13* +*ICE-GL-ZY001 · TCS-0002∞ · 国作登字-2026-A-00037559* diff --git a/scripts/cvm-power/cvm-power.sh b/scripts/cvm-power/cvm-power.sh new file mode 100755 index 0000000..4cc2841 --- /dev/null +++ b/scripts/cvm-power/cvm-power.sh @@ -0,0 +1,210 @@ +#!/usr/bin/env bash +# ════════════════════════════════════════════════════════════════ +# 光湖 · 按量计费CVM自动开关机 +# 开机/关机/状态查询 +# ════════════════════════════════════════════════════════════════ +# +# 用法: +# bash cvm-power.sh start # 开机 +# bash cvm-power.sh stop # 关机 +# bash cvm-power.sh status # 查看状态 +# +# 环境变量(从Vault或Forgejo Secrets读取): +# TENCENT_SECRET_ID 腾讯云API SecretId +# TENCENT_SECRET_KEY 腾讯云API SecretKey +# CVM_INSTANCE_ID CVM实例ID (ins-xxxxx) +# CVM_REGION 地域 (ap-guangzhou) +# +# ════════════════════════════════════════════════════════════════ + +set -euo pipefail + +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +RED='\033[0;31m' +NC='\033[0m' + +# 配置 +SECRET_ID="${TENCENT_SECRET_ID:-}" +SECRET_KEY="${TENCENT_SECRET_KEY:-}" +INSTANCE_ID="${CVM_INSTANCE_ID:-}" +REGION="${CVM_REGION:-ap-guangzhou}" +SERVICE="cvm" +HOST="cvm.tencentcloudapi.com" +API_VERSION="2017-03-12" + +# 检查配置 +if [[ -z "$SECRET_ID" || -z "$SECRET_KEY" || -z "$INSTANCE_ID" ]]; then + echo -e "${RED}❌ 缺少环境变量${NC}" + echo "需要: TENCENT_SECRET_ID, TENCENT_SECRET_KEY, CVM_INSTANCE_ID" + echo "请在Forgejo Secrets中配置这些值" + exit 1 +fi + +# ─── 腾讯云API签名 ────────────────────────────────────────────── + +call_api() { + local action=$1 + local payload=$2 + + local timestamp=$(date +%s) + local date=$(date -u -d "@$timestamp" +%Y-%m-%d 2>/dev/null || date -u +%Y-%m-%d) + + # 1. 拼接规范请求串 + local http_method="POST" + local canonical_uri="/" + local canonical_querystring="" + local ct="application/json; charset=utf-8" + local hashed_payload=$(echo -n "$payload" | sha256sum | awk '{print $1}') + local canonical_headers="content-type:${ct}\nhost:${HOST}\nx-tc-action:${action,,}\n" + local signed_headers="content-type;host;x-tc-action" + local canonical_request="${http_method}\n${canonical_uri}\n${canonical_querystring}\n${canonical_headers}\n${signed_headers}\n${hashed_payload}" + + # 2. 拼接待签名字符串 + local algorithm="TC3-HMAC-SHA256" + local credential_scope="${date}/${SERVICE}/tc3_request" + local hashed_canonical=$(echo -n "$canonical_request" | sha256sum | awk '{print $1}') + local string_to_sign="${algorithm}\n${timestamp}\n${credential_scope}\n${hashed_canonical}" + + # 3. 计算签名 + local secret_date=$(echo -n "$date" | openssl dgst -sha256 -hmac "TC3${SECRET_KEY}" -binary | xxd -p -c 256) + local secret_service=$(echo -n "$SERVICE" | openssl dgst -sha256 -mac HMAC -macopt hexkey:"$secret_date" -binary | xxd -p -c 256) + local secret_signing=$(echo -n "tc3_request" | openssl dgst -sha256 -mac HMAC -macopt hexkey:"$secret_service" -binary | xxd -p -c 256) + local signature=$(echo -n "$string_to_sign" | openssl dgst -sha256 -mac HMAC -macopt hexkey:"$secret_signing" | awk '{print $NF}') + + # 4. 构造Authorization + local authorization="${algorithm} Credential=${SECRET_ID}/${credential_scope}, SignedHeaders=${signed_headers}, Signature=${signature}" + + # 5. 发送请求 + curl -s -X POST "https://${HOST}" \ + -H "Authorization: ${authorization}" \ + -H "Content-Type: ${ct}" \ + -H "Host: ${HOST}" \ + -H "X-TC-Action: ${action}" \ + -H "X-TC-Timestamp: ${timestamp}" \ + -H "X-TC-Version: ${API_VERSION}" \ + -H "X-TC-Region: ${REGION}" \ + -d "$payload" +} + +# ─── 操作函数 ─────────────────────────────────────────────────── + +do_start() { + echo -e "${YELLOW}[开机] 正在启动CVM ${INSTANCE_ID}...${NC}" + local payload="{\"InstanceIds\":[\"${INSTANCE_ID}\"]}" + local resp=$(call_api "StartInstances" "$payload") + + if echo "$resp" | python3 -c " +import json,sys +d=json.load(sys.stdin) +if 'Response' in d and 'Error' not in d['Response']: + print('OK') +else: + err=d.get('Response',{}).get('Error',{}).get('Message','未知错误') + print(f'ERROR: {err}') +" 2>/dev/null | grep -q "OK"; then + echo -e "${GREEN}✅ 开机指令已发送${NC}" + echo " 等待实例启动(约1-2分钟)..." + wait_for_ssh + else + echo -e "${RED}❌ 开机失败${NC}" + echo "$resp" | python3 -c "import json,sys;d=json.load(sys.stdin);print(d.get('Response',{}).get('Error',{}).get('Message','未知'))" 2>/dev/null + exit 1 + fi +} + +do_stop() { + echo -e "${YELLOW}[关机] 正在停止CVM ${INSTANCE_ID}...${NC}" + local payload="{\"InstanceIds\":[\"${INSTANCE_ID}\"]}" + local resp=$(call_api "StopInstances" "$payload") + + if echo "$resp" | python3 -c " +import json,sys +d=json.load(sys.stdin) +if 'Response' in d and 'Error' not in d['Response']: + print('OK') +else: + err=d.get('Response',{}).get('Error',{}).get('Message','未知错误') + print(f'ERROR: {err}') +" 2>/dev/null | grep -q "OK"; then + echo -e "${GREEN}✅ 关机指令已发送${NC}" + echo " 实例将在几分钟内完全停止" + echo " 停止后不再收取计算费用(仅收云盘费用)" + else + echo -e "${RED}❌ 关机失败${NC}" + echo "$resp" | python3 -c "import json,sys;d=json.load(sys.stdin);print(d.get('Response',{}).get('Error',{}).get('Message','未知'))" 2>/dev/null + exit 1 + fi +} + +do_status() { + echo -e "${YELLOW}[状态] 查询CVM ${INSTANCE_ID}...${NC}" + local payload="{\"InstanceIds\":[\"${INSTANCE_ID}\"]}" + local resp=$(call_api "DescribeInstancesStatus" "$payload") + + echo "$resp" | python3 -c " +import json,sys +d=json.load(sys.stdin) +r=d.get('Response',{}) +statuses=r.get('InstanceStatusSet',[]) +if not statuses: + print(' 状态: 未找到实例') +else: + state=statuses[0].get('InstanceState','UNKNOWN') + icon={'RUNNING':'🟢','STOPPED':'🔴','STARTING':'🟡','STOPPING':'🟡'}.get(state,'⚪') + print(f' {icon} 实例状态: {state}') +" 2>/dev/null || echo " 查询失败" +} + +wait_for_ssh() { + local max_wait=120 + local waited=0 + local ip=$(get_instance_ip) + + if [[ -z "$ip" ]]; then + echo " ⚠️ 无法获取公网IP,请手动检查" + return + fi + + echo -n " 等待SSH可连 (${ip}:22) " + while [[ $waited -lt $max_wait ]]; do + if timeout 3 bash -c "echo >/dev/tcp/${ip}/22" 2>/dev/null; then + echo "" + echo -e "${GREEN} ✅ CVM已就绪: ${ip}${NC}" + return + fi + echo -n "." + sleep 5 + waited=$((waited + 5)) + done + echo "" + echo " ⚠️ SSH连接超时,实例可能还在启动中" +} + +get_instance_ip() { + local payload="{\"InstanceIds\":[\"${INSTANCE_ID}\"]}" + call_api "DescribeInstances" "$payload" | python3 -c " +import json,sys +d=json.load(sys.stdin) +instances=d.get('Response',{}).get('InstanceSet',[]) +if instances: + ip=instances[0].get('PublicIpAddresses',[])[0] if instances[0].get('PublicIpAddresses') else '' + print(ip) +" 2>/dev/null || echo "" +} + +# ─── 入口 ───────────────────────────────────────────────────── + +case "${1:-}" in + start) do_start ;; + stop) do_stop ;; + status) do_status ;; + *) + echo "用法: bash cvm-power.sh {start|stop|status}" + echo "" + echo " start - 开机(按量计费CVM)" + echo " stop - 关机(停止计费)" + echo " status - 查询状态" + exit 1 + ;; +esac