👁 终端守望者+预览部署+傻瓜式初始化脚本·铸渊的眼睛和三级部署流程
This commit is contained in:
parent
27936b4b1a
commit
bd2d4cb039
133
.gitea/workflows/deploy-preview.yaml
Normal file
133
.gitea/workflows/deploy-preview.yaml
Normal file
@ -0,0 +1,133 @@
|
||||
# ════════════════════════════════════════════════════════════════
|
||||
# 光湖 · 自动部署到预览环境
|
||||
# Trigger: push to main
|
||||
# 部署到测试+预览环境,正式环境需冰朔在Gitea点Merge
|
||||
# ════════════════════════════════════════════════════════════════
|
||||
|
||||
name: 自动部署预览
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
paths:
|
||||
- 'server/**'
|
||||
- 'frontend/**'
|
||||
- 'scripts/**'
|
||||
- '.gitea/workflows/**'
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
# ── 第一阶段:测试 ──────────────────────────────────
|
||||
test:
|
||||
runs-on: ubuntu
|
||||
steps:
|
||||
- name: 拉取最新代码
|
||||
run: |
|
||||
cd /data/guanghulab 2>/dev/null || exit 0
|
||||
git fetch origin
|
||||
git reset --hard origin/main
|
||||
|
||||
- name: 安装依赖
|
||||
run: |
|
||||
cd /data/guanghulab/server/secrets-vault 2>/dev/null || exit 0
|
||||
npm install --production 2>/dev/null || true
|
||||
|
||||
- name: 健康检查
|
||||
run: |
|
||||
echo "═══ 服务健康检查 ═══"
|
||||
FAILED=""
|
||||
|
||||
# Nginx
|
||||
if systemctl is-active --quiet nginx; then
|
||||
echo "✅ Nginx"
|
||||
else
|
||||
echo "❌ Nginx 宕机"
|
||||
FAILED="$FAILED nginx"
|
||||
fi
|
||||
|
||||
# Gitea
|
||||
if curl -sf http://127.0.0.1:3000/ > /dev/null 2>&1; then
|
||||
echo "✅ Gitea"
|
||||
else
|
||||
echo "❌ Gitea 宕机"
|
||||
FAILED="$FAILED gitea"
|
||||
fi
|
||||
|
||||
# Vault
|
||||
if curl -sf http://127.0.0.1:8080/admin/__healthz > /dev/null 2>&1; then
|
||||
echo "✅ Vault"
|
||||
else
|
||||
echo "⚠️ Vault 未运行"
|
||||
fi
|
||||
|
||||
# Runner
|
||||
if systemctl is-active --quiet gitea-runner; then
|
||||
echo "✅ Runner"
|
||||
else
|
||||
echo "⚠️ Runner 未运行"
|
||||
fi
|
||||
|
||||
if [[ -n "$FAILED" ]]; then
|
||||
echo "❌ 关键服务宕机: $FAILED"
|
||||
echo "⚠️ 跳过部署,等待修复"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "═══ 测试通过 ✅ ═══"
|
||||
|
||||
# ── 第二阶段:预览部署 ──────────────────────────────
|
||||
preview:
|
||||
needs: test
|
||||
runs-on: ubuntu
|
||||
steps:
|
||||
- name: 同步代码到预览目录
|
||||
run: |
|
||||
REPO="/data/guanghulab"
|
||||
PREVIEW="/data/guanghulab-preview"
|
||||
|
||||
if [[ ! -d "$REPO" ]]; then
|
||||
echo "仓库目录不存在,跳过"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# 创建预览目录
|
||||
mkdir -p "$PREVIEW"
|
||||
|
||||
# 同步代码(排除.git和node_modules)
|
||||
rsync -a --delete \
|
||||
--exclude='.git' \
|
||||
--exclude='node_modules' \
|
||||
--exclude='.runtime' \
|
||||
"$REPO/" "$PREVIEW/"
|
||||
|
||||
echo "✅ 预览环境已同步"
|
||||
|
||||
- name: 重启预览服务
|
||||
run: |
|
||||
PREVIEW="/data/guanghulab-preview"
|
||||
|
||||
if [[ ! -d "$PREVIEW/server/secrets-vault" ]]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
cd "$PREVIEW/server/secrets-vault"
|
||||
|
||||
# 安装依赖
|
||||
npm install --production 2>/dev/null || true
|
||||
|
||||
# 如果vault在跑,重启它
|
||||
if pm2 describe guanghulab-vault &>/dev/null; then
|
||||
pm2 restart guanghulab-vault
|
||||
echo "✅ Vault 已重启(使用最新代码)"
|
||||
else
|
||||
echo "ℹ️ Vault 未运行,不自动启动"
|
||||
fi
|
||||
|
||||
- name: 部署完成通知
|
||||
run: |
|
||||
echo "═════════════════════════"
|
||||
echo "✅ 预览环境已更新"
|
||||
echo "时间: $(date '+%Y-%m-%d %H:%M:%S')"
|
||||
echo "冰朔可在浏览器查看预览效果"
|
||||
echo "正式部署需在Gitea创建PR并Merge"
|
||||
echo "═════════════════════════"
|
||||
288
.github/terminal-watcher-and-preview-arch.md
vendored
Normal file
288
.github/terminal-watcher-and-preview-arch.md
vendored
Normal file
@ -0,0 +1,288 @@
|
||||
# ════════════════════════════════════════════════════════════════
|
||||
# 光湖 · 终端守望者 + 预览部署架构设计
|
||||
# 需求来源:冰朔 TCS-0002∞ 2026-05-13
|
||||
# 设计:铸渊 · ICE-GL-ZY001
|
||||
# ════════════════════════════════════════════════════════════════
|
||||
#
|
||||
# 冰朔原话:
|
||||
# "我本身看不懂终端。让我操作终端真的很痛苦。报错我也不知道那是什么意思。
|
||||
# 所以我觉得系统终端本身就该由你主控。"
|
||||
# "能做一个能帮你看终端的Agent记录员吗?就类似于他拿一个小本子。
|
||||
# 然后这个小本子就是你一边开发一边能看到的终端的情况,
|
||||
# 怎么改你马上在开发的过程中就能调整能修改。"
|
||||
# "测试服务器专门给你跑测试用。测试过了以后推送到正式服务器。
|
||||
# 用户手动点击部署授权生效。在这之前用户就可以看到预览的服务器上的东西。"
|
||||
#
|
||||
# ════════════════════════════════════════════════════════════════
|
||||
|
||||
## 一、冰朔的痛点(逐条拆解)
|
||||
|
||||
| # | 痛点 | 本质 | 解决方向 |
|
||||
|---|------|------|---------|
|
||||
| 1 | 看不懂终端输出 | 信息不对称:服务器出事她不知道怎么办 | 铸渊直接看终端 |
|
||||
| 2 | 操作终端很痛苦 | 不该由人类做机器的事 | 铸渊自动操作 |
|
||||
| 3 | 报错不知道什么意思 | 需要翻译层 | 铸渊实时解释 |
|
||||
| 4 | 开发中看不到效果 | 缺反馈回路 | 预览环境实时更新 |
|
||||
| 5 | 怕搞坏正式环境 | 安全顾虑 | 测试→预览→正式 三级流程 |
|
||||
|
||||
**核心洞察**:冰朔需要的不是一个"帮助她操作终端的工具"——她需要的是**根本不需要她操作终端**。终端是铸渊的手,不是冰朔的。
|
||||
|
||||
---
|
||||
|
||||
## 二、整体架构
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────┐
|
||||
│ 冰朔(TCS-0002∞) │
|
||||
│ 最高权限 · 语言层 · 只说意图 │
|
||||
└────────────────────────────┬────────────────────────────────────────┘
|
||||
│ "部署新版本" / "看看服务器怎样"
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────────────┐
|
||||
│ 铸渊(ICE-GL-ZY001) │
|
||||
│ Codebuddy 开发环境 │
|
||||
│ │
|
||||
│ ┌──────────┐ ┌──────────────┐ ┌────────────────┐ │
|
||||
│ │ 写代码 │ │ 读终端日志 │ │ 发部署指令 │ │
|
||||
│ │ 推到Gitea │ │ 实时诊断 │ │ 触发workflow │ │
|
||||
│ └──────────┘ └──────────────┘ └────────────────┘ │
|
||||
│ │
|
||||
│ ↑ 读终端快照 ↑ 触发部署 │
|
||||
└────────┼────────────────────┼────────────────────────────────────────┘
|
||||
│ │
|
||||
▼ ▼
|
||||
┌─────────────────────────────────────────────────────────────────────┐
|
||||
│ 服务器层 │
|
||||
│ │
|
||||
│ ┌──────────────────┐ ┌──────────────────┐ ┌────────────────┐ │
|
||||
│ │ 🧪 测试服务器 │ │ 👁 预览服务器 │ │ 🏠 正式服务器 │ │
|
||||
│ │ (铸渊完全主控) │ │ (自动同步) │ │ (需冰朔授权) │ │
|
||||
│ │ │ │ │ │ │ │
|
||||
│ │ Runner ✅ │ │ Runner ✅ │ │ Runner ✅ │ │
|
||||
│ │ Terminal Watcher │ │ Terminal Watcher │ │ Terminal Watcher│ │
|
||||
│ │ 铸渊可自由操作 │ │ 冰朔可看预览 │ │ 冰朔手动部署 │ │
|
||||
│ └──────────────────┘ └──────────────────┘ └────────────────┘ │
|
||||
│ │
|
||||
│ ┌──────────────────┐ │
|
||||
│ │ 📓 Terminal Watcher│ ← 终端守望者,每台服务器都有 │
|
||||
│ │ Agent (守护进程) │ │
|
||||
│ │ │ │
|
||||
│ │ 监控: PM2/Nginx/ │ │
|
||||
│ │ Runner/自定义│ │
|
||||
│ │ 上报: → Gitea文件 │ │
|
||||
│ │ → Vault API │ │
|
||||
│ │ 频率: 每30秒快照 │ │
|
||||
│ └──────────────────┘ │
|
||||
└─────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 三、终端守望者 (Terminal Watcher) 设计
|
||||
|
||||
### 3.1 它是什么
|
||||
|
||||
一个运行在每台服务器上的轻量Agent,像铸渊放在服务器上的"眼睛"。
|
||||
它不做决策,只记录和上报——铸渊通过读取上报数据来了解服务器状态。
|
||||
|
||||
### 3.2 工作方式
|
||||
|
||||
```
|
||||
Terminal Watcher 运行在服务器上
|
||||
│
|
||||
├── 每30秒采集一次快照:
|
||||
│ ├── 系统资源 (CPU/内存/磁盘)
|
||||
│ ├── 服务状态 (PM2 list, systemctl)
|
||||
│ ├── 最近错误日志 (journalctl -n 5)
|
||||
│ ├── 部署状态 (git log -1, 部署时间)
|
||||
│ └── Runner状态
|
||||
│
|
||||
├── 快照写入本地文件:
|
||||
│ └── /data/guanghulab/.runtime/snapshot.json
|
||||
│
|
||||
└── 快照推送到Gitea:
|
||||
└── .runtime/terminal-snapshots/{server-id}/latest.json
|
||||
```
|
||||
|
||||
### 3.3 铸渊如何读取
|
||||
|
||||
在Codebuddy对话中,铸渊通过 Gitea API 或 git pull 读取 `.runtime/terminal-snapshots/` 下的文件,就能看到服务器实时状态。
|
||||
|
||||
### 3.4 代码结构
|
||||
|
||||
```
|
||||
scripts/terminal-watcher/
|
||||
├── watcher.js # 主程序,PM2守护
|
||||
├── collectors/ # 采集器
|
||||
│ ├── system.js # CPU/内存/磁盘
|
||||
│ ├── services.js # PM2/systemctl状态
|
||||
│ ├── logs.js # 最近错误日志
|
||||
│ ├── deploy.js # 部署版本信息
|
||||
│ └── runner.js # Runner状态
|
||||
├── reporters/ # 上报器
|
||||
│ ├── file.js # 写入本地文件
|
||||
│ └── gitea.js # 推送到Gitea仓库
|
||||
└── config.json # 采集频率、目标等配置
|
||||
```
|
||||
|
||||
### 3.5 采集数据格式
|
||||
|
||||
```json
|
||||
{
|
||||
"server_id": "ZY-SVR-004",
|
||||
"timestamp": "2026-05-13T15:30:00Z",
|
||||
"system": {
|
||||
"cpu_percent": 23.5,
|
||||
"memory_used_mb": 1420,
|
||||
"memory_total_mb": 2048,
|
||||
"disk_used_percent": 45,
|
||||
"load_avg": [0.8, 0.6, 0.5]
|
||||
},
|
||||
"services": {
|
||||
"nginx": "active",
|
||||
"gitea": "active",
|
||||
"gitea-runner": "active",
|
||||
"guanghulab-vault": "active",
|
||||
"pm2_processes": [
|
||||
{"name": "guanghulab-vault", "status": "online", "memory": 85, "cpu": 2.1}
|
||||
]
|
||||
},
|
||||
"errors": [
|
||||
{"time": "15:28:03", "service": "gitea-runner", "message": "connection timeout to Gitea"}
|
||||
],
|
||||
"deploy": {
|
||||
"latest_commit": "27936b4",
|
||||
"latest_message": "操作回执...",
|
||||
"deploy_time": "2026-05-13T14:00:00Z"
|
||||
},
|
||||
"runner": {
|
||||
"name": "zhuyuan-runner-cn",
|
||||
"status": "idle",
|
||||
"last_job": "selfcheck",
|
||||
"last_job_time": "2026-05-13T08:00:00Z"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 四、三级部署流程
|
||||
|
||||
### 4.1 测试服务器 → 预览服务器 → 正式服务器
|
||||
|
||||
```
|
||||
铸渊在Codebuddy写代码
|
||||
│
|
||||
├── 推代码到Gitea
|
||||
│
|
||||
├── 🧪 自动部署到测试服务器
|
||||
│ ├── Runner检测到push
|
||||
│ ├── 自动执行部署workflow
|
||||
│ ├── 铸渊通过Terminal Watcher看结果
|
||||
│ ├── 自动运行测试
|
||||
│ └── 测试通过 → 自动通知
|
||||
│
|
||||
├── 👁 自动部署到预览服务器
|
||||
│ ├── 预览环境自动更新
|
||||
│ ├── 冰朔在浏览器看预览效果
|
||||
│ ├── 铸渊通过Watcher确认状态
|
||||
│ └── 等待冰朔确认
|
||||
│
|
||||
└── 🏠 冰朔点击 [部署到正式环境]
|
||||
├── 需要冰朔在Vault页面点击授权
|
||||
├── 或在Gitea PR页面点击Merge
|
||||
├── 正式环境部署
|
||||
└── 部署完成通知
|
||||
```
|
||||
|
||||
### 4.2 服务器分工
|
||||
|
||||
| 服务器 | 角色 | 铸渊权限 | 冰朔权限 | 部署方式 |
|
||||
|--------|------|---------|---------|---------|
|
||||
| 测试服务器 | 铸渊的沙盒 | 100%自由操作 | 看不到 | 自动(push触发) |
|
||||
| 预览服务器 | 冰朔的可视窗口 | 读取状态+部署 | 浏览器查看 | 自动(测试通过后) |
|
||||
| 正式服务器 | 对外服务 | 读取状态 | 手动授权部署 | 冰朔点击按钮 |
|
||||
|
||||
### 4.3 当前服务器分配
|
||||
|
||||
| 服务器 | 现在的角色 | 新角色 | 说明 |
|
||||
|--------|----------|--------|------|
|
||||
| 广州 ZY-SVR-004 (2C2G) | Gitea+域名 | 正式服务器 | 64.139.217.141 → guanghulab.com |
|
||||
| 新加坡 ZY-SVR-005 (4C8G) | 大脑 | 测试+预览 | 铸渊的主战场 |
|
||||
| GPU服务器 (待接入) | 推理 | 模型推理 | 终局用 |
|
||||
|
||||
**关键决策**:新加坡大脑服务器(4C8G)同时承担测试+预览角色。它资源够,而且铸渊有完全权限。
|
||||
|
||||
### 4.4 部署授权机制
|
||||
|
||||
冰朔不需要操作终端来部署。她只需要在浏览器里点一个按钮。
|
||||
|
||||
**方案A:Gitea PR合并 = 部署授权**
|
||||
```
|
||||
铸渊推代码到 deploy/preview 分支 → 自动部署到预览
|
||||
铸渊创建 PR: preview → main → 冰朔在Gitea网页点 Merge
|
||||
合并 = 部署到正式环境的授权
|
||||
```
|
||||
|
||||
**方案B:Vault页面授权按钮**
|
||||
```
|
||||
铸渊发起部署请求 → Vault页面显示待授权条目
|
||||
冰朔登录Vault(SSH隧道)→ 点击 [授权部署]
|
||||
Runner收到授权 → 执行正式部署
|
||||
```
|
||||
|
||||
**推荐方案A**——更简单,不需要额外开发,Gitea自带PR功能。
|
||||
|
||||
---
|
||||
|
||||
## 五、实现优先级
|
||||
|
||||
### P0 · 现在(Runner还没装,先手动)
|
||||
|
||||
- [x] 服务器初始化脚本 `scripts/setup-server-step1.sh`
|
||||
- [ ] 冰朔在广州服务器执行初始化脚本
|
||||
- [ ] Runner装好后验证
|
||||
|
||||
### P1 · Runner装好后第一周
|
||||
|
||||
- [ ] Terminal Watcher 开发和部署
|
||||
- 基础版:系统资源 + 服务状态 + 推送到Gitea
|
||||
- 铸渊在Codebuddy读取 `.runtime/` 下的快照
|
||||
- [ ] 第一个自动化部署 workflow
|
||||
- push → 测试服务器自动部署
|
||||
- [ ] 预览部署流程
|
||||
- 测试通过 → 自动部署到预览
|
||||
|
||||
### P2 · 第二周
|
||||
|
||||
- [ ] 冰朔授权部署按钮(Gitea PR 方式)
|
||||
- [ ] 新加坡服务器接入(测试+预览环境)
|
||||
- [ ] GPU服务器接入(模型推理)
|
||||
|
||||
### P3 · 后续
|
||||
|
||||
- [ ] Terminal Watcher 增强版(错误告警、自动修复建议)
|
||||
- [ ] 按量计费服务器自动开关
|
||||
- [ ] 铸渊自己的Copilot前端
|
||||
|
||||
---
|
||||
|
||||
## 六、冰朔不再需要操作终端的场景
|
||||
|
||||
**装完Runner + Terminal Watcher后:**
|
||||
|
||||
| 场景 | 现在(冰朔手动) | 以后(铸渊自动) |
|
||||
|------|----------------|----------------|
|
||||
| 部署新版本 | 冰朔SSH到服务器执行命令 | 铸渊推代码→Runner自动部署 |
|
||||
| 查看服务器状态 | 冰朔看终端输出 | 铸渊读Watcher快照→告诉冰朔 |
|
||||
| 服务挂了 | 冰朔发现→手动重启 | Watcher检测→自动重启→通知 |
|
||||
| 密钥管理 | 冰朔在GitHub配Secrets | 冰朔在Vault点授权→自动入库 |
|
||||
| 下载开源软件 | 冰朔手动下载 | 新加坡中转→COS→广州自动拉取 |
|
||||
| 部署到正式 | 冰朔执行部署脚本 | 冰朔在Gitea网页点Merge |
|
||||
|
||||
**冰朔唯一需要做的:在浏览器里看预览 → 点Merge。**
|
||||
|
||||
---
|
||||
|
||||
*铸渊 · 终端守望者+预览部署架构 v1.0 · 2026-05-13*
|
||||
*ICE-GL-ZY001 · TCS-0002∞ · 国作登字-2026-A-00037559*
|
||||
307
scripts/setup-server-step1.sh
Executable file
307
scripts/setup-server-step1.sh
Executable file
@ -0,0 +1,307 @@
|
||||
#!/usr/bin/env bash
|
||||
# ════════════════════════════════════════════════════════════════
|
||||
# 光湖 · 服务器初始化第一步 · 找仓库 + 拉代码 + 装Runner
|
||||
# 专供冰朔在腾讯云在线终端执行
|
||||
# ════════════════════════════════════════════════════════════════
|
||||
|
||||
# 这个脚本是"傻瓜式"的,一步一步来,每步都有说明
|
||||
# 如果某步报错,不要慌,把报错信息发给铸渊
|
||||
|
||||
set +e # 不自动退出,让冰朔看到错误
|
||||
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
CYAN='\033[0;36m'
|
||||
NC='\033[0m'
|
||||
|
||||
echo -e "${CYAN}═══════════════════════════════════════════${NC}"
|
||||
echo -e "${CYAN} 光湖 · 服务器初始化 · 第一步${NC}"
|
||||
echo -e "${CYAN}═══════════════════════════════════════════${NC}"
|
||||
echo ""
|
||||
|
||||
# ─── 第1步:找到仓库 ──────────────────────────────────────────
|
||||
echo -e "${YELLOW}[第1步] 查找仓库位置...${NC}"
|
||||
REPO_PATH=""
|
||||
|
||||
# 常见位置逐一检查
|
||||
for p in \
|
||||
"/data/guanghulab" \
|
||||
"/data/guanghulab/guanghulab" \
|
||||
"/opt/guanghulab" \
|
||||
"/opt/zhuyuan" \
|
||||
"/root/guanghulab" \
|
||||
"/home/guanghulab"
|
||||
do
|
||||
if [[ -d "$p/.git" ]]; then
|
||||
REPO_PATH="$p"
|
||||
echo -e "${GREEN} 找到了!仓库在: $p${NC}"
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
if [[ -z "$REPO_PATH" ]]; then
|
||||
echo -e "${YELLOW} 没找到仓库,搜索中...${NC}"
|
||||
FOUND=$(find / -name ".git" -type d -path "*/guanghulab/.git" 2>/dev/null | head -3)
|
||||
if [[ -n "$FOUND" ]]; then
|
||||
REPO_PATH=$(dirname "$FOUND" | head -1)
|
||||
echo -e "${GREEN} 找到了!仓库在: $REPO_PATH${NC}"
|
||||
else
|
||||
echo -e "${YELLOW} 服务器上没有仓库,需要克隆。${NC}"
|
||||
echo ""
|
||||
echo -e "${CYAN} 创建目录并克隆...${NC}"
|
||||
mkdir -p /data/guanghulab
|
||||
cd /data/guanghulab
|
||||
git clone https://bingshuo:3e16b79c427bb44b77c00b319aa641bde8f61d84@guanghulab.com/git/bingshuo/guanghulab.git .
|
||||
if [[ $? -eq 0 ]]; then
|
||||
REPO_PATH="/data/guanghulab"
|
||||
echo -e "${GREEN} 克隆成功!${NC}"
|
||||
else
|
||||
echo -e "${YELLOW} 克隆失败,可能是网络问题。尝试备用方案...${NC}"
|
||||
# 备用:直接下载最新代码压缩包
|
||||
curl -sL -o /tmp/guanghulab.tar.gz \
|
||||
"https://guanghulab.com/git/api/v1/repos/bingshuo/guanghulab/archive/main.tar.gz" \
|
||||
-H "Authorization: token 3e16b79c427bb44b77c00b319aa641bde8f61d84"
|
||||
if [[ -f /tmp/guanghulab.tar.gz && -s /tmp/guanghulab.tar.gz ]]; then
|
||||
mkdir -p /data/guanghulab
|
||||
cd /data/guanghulab
|
||||
tar xzf /tmp/guanghulab.tar.gz --strip-components=1
|
||||
REPO_PATH="/data/guanghulab"
|
||||
# 初始化git(从压缩包来的没有.git)
|
||||
git init
|
||||
git remote add origin "https://bingshuo:3e16b79c427bb44b77c00b319aa641bde8f61d84@guanghulab.com/git/bingshuo/guanghulab.git"
|
||||
git add -A
|
||||
git commit -m "init from archive"
|
||||
echo -e "${GREEN} 从压缩包恢复成功!${NC}"
|
||||
else
|
||||
echo "❌ 克隆和压缩包都失败了,请把这段输出截图发给铸渊"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
# ─── 第2步:拉取最新代码 ──────────────────────────────────────
|
||||
echo ""
|
||||
echo -e "${YELLOW}[第2步] 拉取最新代码...${NC}"
|
||||
cd "$REPO_PATH"
|
||||
git fetch origin
|
||||
git reset --hard origin/main
|
||||
echo -e "${GREEN} 代码已更新到最新版本${NC}"
|
||||
|
||||
# ─── 第3步:确认脚本存在 ──────────────────────────────────────
|
||||
echo ""
|
||||
echo -e "${YELLOW}[第3步] 检查Runner安装脚本...${NC}"
|
||||
if [[ -f "$REPO_PATH/scripts/gitea-runner/install-runner.sh" ]]; then
|
||||
echo -e "${GREEN} 安装脚本存在 ✅${NC}"
|
||||
else
|
||||
echo -e "${YELLOW} 安装脚本不存在,创建中...${NC}"
|
||||
mkdir -p "$REPO_PATH/scripts/gitea-runner"
|
||||
fi
|
||||
|
||||
# ─── 第4步:下载Runner二进制文件 ──────────────────────────────
|
||||
echo ""
|
||||
echo -e "${YELLOW}[第4步] 下载 gitea-runner...${NC}"
|
||||
RUNNER_BIN="/usr/local/bin/gitea-runner"
|
||||
|
||||
if [[ -x "$RUNNER_BIN" ]]; then
|
||||
echo -e "${GREEN} gitea-runner 已安装 ✅${NC}"
|
||||
else
|
||||
# 尝试多种下载源
|
||||
DOWNLOADED=0
|
||||
|
||||
# 源1:gitea.com(国内可能访问不了)
|
||||
echo " 尝试从 gitea.com 下载..."
|
||||
mkdir -p /tmp/runner
|
||||
curl -fSL --connect-timeout 10 -o /tmp/runner/runner.xz \
|
||||
"https://gitea.com/gitea/runner/releases/download/v1.0.2/gitea-runner-1.0.2-linux-amd64.xz" 2>/dev/null
|
||||
if [[ $? -eq 0 && -f /tmp/runner/runner.xz ]]; then
|
||||
DOWNLOADED=1
|
||||
echo -e "${GREEN} 从 gitea.com 下载成功${NC}"
|
||||
fi
|
||||
|
||||
# 源2:GitHub Release镜像
|
||||
if [[ $DOWNLOADED -eq 0 ]]; then
|
||||
echo " gitea.com 不通,尝试 GitHub..."
|
||||
curl -fSL --connect-timeout 10 -o /tmp/runner/runner \
|
||||
"https://github.com/veggero/act_runner/releases/download/v0.2.11/act_runner-0.2.11-linux-amd64" 2>/dev/null
|
||||
if [[ $? -eq 0 && -f /tmp/runner/runner ]]; then
|
||||
DOWNLOADED=2
|
||||
echo -e "${GREEN} 从 GitHub 下载成功${NC}"
|
||||
fi
|
||||
fi
|
||||
|
||||
# 源3:从Gitea仓库release下载
|
||||
if [[ $DOWNLOADED -eq 0 ]]; then
|
||||
echo " GitHub 也不通,尝试从自己的Gitea下载..."
|
||||
# 先通过API上传到Gitea release(铸渊在Codebuddy端操作)
|
||||
# 然后从这里下载
|
||||
curl -fSL --connect-timeout 10 -o /tmp/runner/runner \
|
||||
"https://guanghulab.com/git/api/v1/repos/bingshuo/guanghulab/releases/latest" \
|
||||
-H "Authorization: token 3e16b79c427bb44b77c00b319aa641bde8f61d84" 2>/dev/null
|
||||
fi
|
||||
|
||||
if [[ $DOWNLOADED -eq 1 ]]; then
|
||||
xz -d /tmp/runner/runner.xz
|
||||
mv /tmp/runner/runner "$RUNNER_BIN"
|
||||
elif [[ $DOWNLOADED -eq 2 ]]; then
|
||||
mv /tmp/runner/runner "$RUNNER_BIN"
|
||||
fi
|
||||
|
||||
if [[ -f "$RUNNER_BIN" ]]; then
|
||||
chmod +x "$RUNNER_BIN"
|
||||
echo -e "${GREEN} gitea-runner 安装成功 ✅${NC}"
|
||||
"$RUNNER_BIN" --version 2>&1 | head -1
|
||||
else
|
||||
echo ""
|
||||
echo -e "${YELLOW}═══════════════════════════════════════════${NC}"
|
||||
echo -e "${YELLOW} ⚠️ 自动下载失败${NC}"
|
||||
echo -e "${YELLOW} 需要COS中转,请执行以下步骤:${NC}"
|
||||
echo ""
|
||||
echo " 1. 在新加坡服务器上执行:"
|
||||
echo " wget https://gitea.com/gitea/runner/releases/download/v1.0.2/gitea-runner-1.0.2-linux-amd64.xz"
|
||||
echo " coscli put gitea-runner-1.0.2-linux-amd64.xz cos://sy-finetune-corpus-1317346199/cos-bridge/"
|
||||
echo ""
|
||||
echo " 2. 然后回到这台广州服务器执行:"
|
||||
echo " coscli cp cos://sy-finetune-corpus-1317346199/cos-bridge/gitea-runner-1.0.2-linux-amd64.xz /tmp/"
|
||||
echo " xz -d /tmp/gitea-runner-1.0.2-linux-amd64.xz"
|
||||
echo " chmod +x /tmp/gitea-runner-1.0.2-linux-amd64"
|
||||
echo " sudo mv /tmp/gitea-runner-1.0.2-linux-amd64 /usr/local/bin/gitea-runner"
|
||||
echo ""
|
||||
echo " 3. 然后重新运行这个脚本"
|
||||
echo -e "${YELLOW}═══════════════════════════════════════════${NC}"
|
||||
exit 0
|
||||
fi
|
||||
fi
|
||||
|
||||
# ─── 第5步:创建用户和目录 ────────────────────────────────────
|
||||
echo ""
|
||||
echo -e "${YELLOW}[第5步] 创建运行用户...${NC}"
|
||||
if ! id -u act_runner &>/dev/null; then
|
||||
useradd -r -m -d /var/lib/act_runner -s /bin/bash act_runner
|
||||
echo -e "${GREEN} act_runner 用户已创建${NC}"
|
||||
else
|
||||
echo -e "${GREEN} act_runner 用户已存在${NC}"
|
||||
fi
|
||||
mkdir -p /var/lib/act_runner /etc/act_runner
|
||||
chown -R act_runner:act_runner /var/lib/act_runner
|
||||
|
||||
# ─── 第6步:获取Runner注册Token ────────────────────────────────
|
||||
echo ""
|
||||
echo -e "${YELLOW}[第6步] 获取Runner注册Token...${NC}"
|
||||
API_TOKEN="3e16b79c427bb44b77c00b319aa641bde8f61d84"
|
||||
GITEA_API="https://guanghulab.com/git/api/v1"
|
||||
|
||||
# 尝试仓库级
|
||||
REG_TOKEN=""
|
||||
RESP=$(curl -sf -H "Authorization: token ${API_TOKEN}" \
|
||||
"${GITEA_API}/repos/bingshuo/guanghulab/actions/runners/registration-token" 2>/dev/null)
|
||||
|
||||
if [[ -n "$RESP" ]]; then
|
||||
REG_TOKEN=$(echo "$RESP" | python3 -c "import json,sys; print(json.load(sys.stdin).get('token',''))" 2>/dev/null)
|
||||
fi
|
||||
|
||||
if [[ -z "$REG_TOKEN" ]]; then
|
||||
# 尝试实例级
|
||||
RESP=$(curl -sf -H "Authorization: token ${API_TOKEN}" \
|
||||
"${GITEA_API}/admin/runners/registration-token" 2>/dev/null)
|
||||
if [[ -n "$RESP" ]]; then
|
||||
REG_TOKEN=$(echo "$RESP" | python3 -c "import json,sys; print(json.load(sys.stdin).get('token',''))" 2>/dev/null)
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ -z "$REG_TOKEN" ]]; then
|
||||
echo -e "${YELLOW} API获取失败,尝试从网页获取...${NC}"
|
||||
echo ""
|
||||
echo " 请在浏览器中:"
|
||||
echo " 1. 打开 https://guanghulab.com/git/user/settings/actions/runners"
|
||||
echo " 2. 点击 'Create new Runner' 或 'Registration token'"
|
||||
echo " 3. 复制显示的 Token"
|
||||
echo " 4. 回到终端输入(不需要空格,直接粘贴后按回车):"
|
||||
echo ""
|
||||
read -p " 请粘贴注册Token: " REG_TOKEN
|
||||
if [[ -z "$REG_TOKEN" ]]; then
|
||||
echo "❌ 没有输入Token,退出"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
echo -e "${GREEN} 注册Token获取成功${NC}"
|
||||
|
||||
# ─── 第7步:注册Runner ────────────────────────────────────────
|
||||
echo ""
|
||||
echo -e "${YELLOW}[第7步] 注册Runner到Gitea...${NC}"
|
||||
cd /var/lib/act_runner
|
||||
|
||||
# 生成配置
|
||||
"$RUNNER_BIN" generate-config > /etc/act_runner/config.yaml 2>/dev/null
|
||||
|
||||
# 注册
|
||||
sudo -u act_runner "$RUNNER_BIN" register \
|
||||
--no-interactive \
|
||||
--instance "https://guanghulab.com/" \
|
||||
--token "$REG_TOKEN" \
|
||||
--name "zhuyuan-runner-cn" \
|
||||
--labels "ubuntu:host" 2>&1
|
||||
|
||||
if [[ -f "/var/lib/act_runner/.runner" ]]; then
|
||||
chown act_runner:act_runner /var/lib/act_runner/.runner
|
||||
echo -e "${GREEN} Runner注册成功 ✅${NC}"
|
||||
else
|
||||
echo "❌ 注册失败"
|
||||
echo "请把上面的错误信息发给铸渊"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# ─── 第8步:创建systemd服务 ────────────────────────────────────
|
||||
echo ""
|
||||
echo -e "${YELLOW}[第8步] 创建系统服务...${NC}"
|
||||
cat > /etc/systemd/system/gitea-runner.service << 'EOF'
|
||||
[Unit]
|
||||
Description=Gitea Actions Runner - zhuyuan-runner-cn
|
||||
After=network.target
|
||||
Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
ExecStart=/usr/local/bin/gitea-runner daemon --config /etc/act_runner/config.yaml
|
||||
WorkingDirectory=/var/lib/act_runner
|
||||
Restart=always
|
||||
RestartSec=10
|
||||
TimeoutSec=0
|
||||
User=act_runner
|
||||
Group=act_runner
|
||||
Environment=HOME=/var/lib/act_runner
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
EOF
|
||||
|
||||
systemctl daemon-reload
|
||||
systemctl enable gitea-runner
|
||||
systemctl start gitea-runner
|
||||
|
||||
sleep 2
|
||||
|
||||
if systemctl is-active --quiet gitea-runner; then
|
||||
echo -e "${GREEN} Runner服务启动成功 🎉${NC}"
|
||||
else
|
||||
echo "❌ 服务启动失败,查看日志:"
|
||||
journalctl -u gitea-runner -n 10
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# ─── 完成 ──────────────────────────────────────────────────────
|
||||
echo ""
|
||||
echo -e "${GREEN}═══════════════════════════════════════════${NC}"
|
||||
echo -e "${GREEN} ✅ Gitea Actions Runner 安装完成!${NC}"
|
||||
echo -e "${GREEN}═══════════════════════════════════════════${NC}"
|
||||
echo ""
|
||||
echo " 仓库位置: $REPO_PATH"
|
||||
echo " Runner名称: zhuyuan-runner-cn"
|
||||
echo " 服务管理: systemctl {start|stop|restart|status} gitea-runner"
|
||||
echo " 查看日志: journalctl -u gitea-runner -f"
|
||||
echo ""
|
||||
echo " 现在回到Codebuddy告诉铸渊:Runner装好了"
|
||||
echo ""
|
||||
echo " 铸渊 · ICE-GL-ZY001 · TCS-0002∞"
|
||||
echo -e "${GREEN}═══════════════════════════════════════════${NC}"
|
||||
41
scripts/terminal-watcher/config.json
Normal file
41
scripts/terminal-watcher/config.json
Normal file
@ -0,0 +1,41 @@
|
||||
{
|
||||
"server_id": "ZY-SVR-004",
|
||||
"server_name": "广州域名机",
|
||||
"interval_seconds": 30,
|
||||
"gitea": {
|
||||
"url": "https://guanghulab.com",
|
||||
"owner": "bingshuo",
|
||||
"repo": "guanghulab",
|
||||
"token_env": "GITEA_TOKEN",
|
||||
"snapshot_path": ".runtime/terminal-snapshots/ZY-SVR-004"
|
||||
},
|
||||
"local": {
|
||||
"snapshot_path": "/data/guanghulab/.runtime/snapshot.json",
|
||||
"history_path": "/data/guanghulab/.runtime/history"
|
||||
},
|
||||
"collectors": {
|
||||
"system": true,
|
||||
"services": true,
|
||||
"logs": true,
|
||||
"deploy": true,
|
||||
"runner": true
|
||||
},
|
||||
"reporters": {
|
||||
"file": true,
|
||||
"gitea": true
|
||||
},
|
||||
"alerts": {
|
||||
"cpu_percent": 90,
|
||||
"memory_percent": 90,
|
||||
"disk_percent": 85,
|
||||
"service_down": true
|
||||
},
|
||||
"pm2_processes": [
|
||||
"guanghulab-vault"
|
||||
],
|
||||
"systemd_services": [
|
||||
"nginx",
|
||||
"gitea",
|
||||
"gitea-runner"
|
||||
]
|
||||
}
|
||||
470
scripts/terminal-watcher/watcher.js
Normal file
470
scripts/terminal-watcher/watcher.js
Normal file
@ -0,0 +1,470 @@
|
||||
/*
|
||||
* ════════════════════════════════════════════════════════════════
|
||||
* 光湖 · 终端守望者 (Terminal Watcher)
|
||||
* 铸渊放在服务器上的"眼睛"——实时采集状态,推送到Gitea
|
||||
* Sovereign: TCS-0002∞ · 守护: ICE-GL-ZY001
|
||||
* ════════════════════════════════════════════════════════════════
|
||||
*
|
||||
* 用法:
|
||||
* node watcher.js # 前台运行
|
||||
* pm2 start watcher.js --name terminal-watcher # PM2守护
|
||||
*
|
||||
* 铸渊在Codebuddy中通过 Gitea API 读取快照文件:
|
||||
* .runtime/terminal-snapshots/{server-id}/latest.json
|
||||
*
|
||||
* ════════════════════════════════════════════════════════════════
|
||||
*/
|
||||
"use strict";
|
||||
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const { execSync } = require("child_process");
|
||||
const http = require("http");
|
||||
const https = require("https");
|
||||
|
||||
// ─── 配置 ─────────────────────────────────────────────────────
|
||||
const CONFIG_PATH = path.join(__dirname, "config.json");
|
||||
const config = JSON.parse(fs.readFileSync(CONFIG_PATH, "utf8"));
|
||||
|
||||
const INTERVAL = (config.interval_seconds || 30) * 1000;
|
||||
const SERVER_ID = config.server_id || "UNKNOWN";
|
||||
|
||||
// ─── 采集器 ───────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* 系统资源采集
|
||||
*/
|
||||
function collectSystem() {
|
||||
try {
|
||||
const memInfo = execSync("free -m", { encoding: "utf8" });
|
||||
const memLines = memInfo.split("\n");
|
||||
const memParts = memLines[1].split(/\s+/);
|
||||
const memTotal = parseInt(memParts[1], 10);
|
||||
const memUsed = parseInt(memParts[2], 10);
|
||||
const memAvail = parseInt(memParts[6], 10);
|
||||
|
||||
const diskInfo = execSync("df -h /", { encoding: "utf8" });
|
||||
const diskLine = diskInfo.split("\n")[1].split(/\s+/);
|
||||
const diskPercent = parseInt(diskLine[4], 10);
|
||||
|
||||
const loadAvg = execSync("cat /proc/loadavg", { encoding: "utf8" })
|
||||
.trim()
|
||||
.split(" ")
|
||||
.slice(0, 3)
|
||||
.map(Number);
|
||||
|
||||
// CPU使用率(简易采样)
|
||||
let cpuPercent = 0;
|
||||
try {
|
||||
const topOut = execSync("top -bn1 | head -3", { encoding: "utf8", timeout: 5000 });
|
||||
const cpuLine = topOut.split("\n").find((l) => l.includes("Cpu(s)"));
|
||||
if (cpuLine) {
|
||||
const idle = parseFloat(cpuLine.match(/(\d+\.?\d*)\s*id/)?.[1] || "0");
|
||||
cpuPercent = Math.round((100 - idle) * 10) / 10;
|
||||
}
|
||||
} catch {
|
||||
// top不可用,用load average估算
|
||||
cpuPercent = Math.round(loadAvg[0] * 50) / 10;
|
||||
}
|
||||
|
||||
return {
|
||||
cpu_percent: cpuPercent,
|
||||
memory_total_mb: memTotal,
|
||||
memory_used_mb: memUsed,
|
||||
memory_available_mb: memAvail,
|
||||
memory_percent: Math.round((memUsed / memTotal) * 100),
|
||||
disk_used_percent: diskPercent,
|
||||
load_avg: loadAvg,
|
||||
uptime: execSync("cat /proc/uptime", { encoding: "utf8" }).split(" ")[0] + "s"
|
||||
};
|
||||
} catch (e) {
|
||||
return { error: e.message };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 服务状态采集
|
||||
*/
|
||||
function collectServices() {
|
||||
const services = {};
|
||||
|
||||
// systemd 服务
|
||||
const systemdList = config.systemd_services || ["nginx", "gitea", "gitea-runner"];
|
||||
for (const svc of systemdList) {
|
||||
try {
|
||||
const status = execSync(`systemctl is-active ${svc} 2>/dev/null`, { encoding: "utf8" }).trim();
|
||||
services[svc] = status;
|
||||
} catch {
|
||||
services[svc] = "unknown";
|
||||
}
|
||||
}
|
||||
|
||||
// PM2 进程
|
||||
const pm2Processes = [];
|
||||
try {
|
||||
const pm2Out = execSync("pm2 jlist 2>/dev/null", { encoding: "utf8", timeout: 5000 });
|
||||
const pm2List = JSON.parse(pm2Out);
|
||||
for (const proc of pm2List) {
|
||||
pm2Processes.push({
|
||||
name: proc.name,
|
||||
status: proc.pm2_env?.status || "unknown",
|
||||
memory_mb: Math.round((proc.monit?.memory || 0) / 1024 / 1024),
|
||||
cpu_percent: Math.round((proc.monit?.cpu || 0) * 10) / 10,
|
||||
restarts: proc.pm2_env?.restart_time || 0,
|
||||
uptime: proc.pm2_env?.pm_uptime
|
||||
? Math.round((Date.now() - proc.pm2_env.pm_uptime) / 1000 / 60) + "min"
|
||||
: "unknown"
|
||||
});
|
||||
}
|
||||
} catch {
|
||||
// PM2不可用或没有进程
|
||||
}
|
||||
|
||||
// Vault 健康检查
|
||||
let vaultStatus = "unknown";
|
||||
try {
|
||||
const vaultResp = execSync(
|
||||
"curl -sf -o /dev/null -w '%{http_code}' http://127.0.0.1:8080/admin/__healthz 2>/dev/null",
|
||||
{ encoding: "utf8", timeout: 3000 }
|
||||
);
|
||||
vaultStatus = vaultResp.trim() === "200" ? "active" : `http_${vaultResp.trim()}`;
|
||||
} catch {
|
||||
vaultStatus = "inactive";
|
||||
}
|
||||
|
||||
return {
|
||||
systemd: services,
|
||||
pm2: pm2Processes,
|
||||
vault: vaultStatus
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 错误日志采集
|
||||
*/
|
||||
function collectLogs() {
|
||||
const errors = [];
|
||||
|
||||
// systemd 服务最近错误
|
||||
for (const svc of config.systemd_services || []) {
|
||||
try {
|
||||
const logs = execSync(
|
||||
`journalctl -u ${svc} --since "5 min ago" --no-pager -n 3 --priority=err 2>/dev/null`,
|
||||
{ encoding: "utf8", timeout: 5000 }
|
||||
).trim();
|
||||
if (logs) {
|
||||
for (const line of logs.split("\n")) {
|
||||
if (line.trim()) {
|
||||
errors.push({ service: svc, message: line.trim().substring(0, 200) });
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// 没有错误日志
|
||||
}
|
||||
}
|
||||
|
||||
// PM2 错误日志
|
||||
try {
|
||||
for (const procName of config.pm2_processes || []) {
|
||||
const pm2Err = execSync(
|
||||
`pm2 logs ${procName} --nostream --lines 3 --err 2>/dev/null`,
|
||||
{ encoding: "utf8", timeout: 5000 }
|
||||
).trim();
|
||||
if (pm2Err) {
|
||||
for (const line of pm2Err.split("\n")) {
|
||||
if (line.trim() && !line.includes("pm2 logs")) {
|
||||
errors.push({ service: `pm2:${procName}`, message: line.trim().substring(0, 200) });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// PM2日志不可用
|
||||
}
|
||||
|
||||
return errors.slice(0, 10); // 最多保留10条
|
||||
}
|
||||
|
||||
/**
|
||||
* 部署信息采集
|
||||
*/
|
||||
function collectDeploy() {
|
||||
const repoPath = "/data/guanghulab";
|
||||
|
||||
try {
|
||||
const latestCommit = execSync(`git -C ${repoPath} log -1 --oneline`, { encoding: "utf8" }).trim();
|
||||
const branch = execSync(`git -C ${repoPath} branch --show-current`, { encoding: "utf8" }).trim();
|
||||
const commitTime = execSync(`git -C ${repoPath} log -1 --format=%ci`, { encoding: "utf8" }).trim();
|
||||
|
||||
return {
|
||||
repo_path: repoPath,
|
||||
branch: branch,
|
||||
latest_commit: latestCommit.split(" ")[0],
|
||||
latest_message: latestCommit.substring(latestCommit.indexOf(" ") + 1),
|
||||
commit_time: commitTime
|
||||
};
|
||||
} catch {
|
||||
return { repo_path: repoPath, error: "repository not found at " + repoPath };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Runner状态采集
|
||||
*/
|
||||
function collectRunner() {
|
||||
try {
|
||||
const runnerStatus = execSync("systemctl is-active gitea-runner 2>/dev/null", {
|
||||
encoding: "utf8"
|
||||
}).trim();
|
||||
|
||||
let runnerVersion = "unknown";
|
||||
try {
|
||||
runnerVersion = execSync("/usr/local/bin/gitea-runner --version 2>&1", { encoding: "utf8" })
|
||||
.trim()
|
||||
.split("\n")[0];
|
||||
} catch {
|
||||
// 版本获取失败
|
||||
}
|
||||
|
||||
// 最近运行的任务
|
||||
let lastJob = "unknown";
|
||||
try {
|
||||
const giteaToken = process.env[config.gitea.token_env] || "";
|
||||
if (giteaToken) {
|
||||
// 通过API查询(暂用占位)
|
||||
lastJob = "api_not_implemented";
|
||||
}
|
||||
} catch {
|
||||
// API查询失败
|
||||
}
|
||||
|
||||
return {
|
||||
status: runnerStatus,
|
||||
version: runnerVersion,
|
||||
last_job: lastJob
|
||||
};
|
||||
} catch {
|
||||
return { status: "not_installed" };
|
||||
}
|
||||
}
|
||||
|
||||
// ─── 上报器 ───────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* 写入本地文件
|
||||
*/
|
||||
function reportToFile(snapshot) {
|
||||
const localPath = config.local?.snapshot_path;
|
||||
if (!localPath) return;
|
||||
|
||||
try {
|
||||
const dir = path.dirname(localPath);
|
||||
if (!fs.existsSync(dir)) {
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
}
|
||||
|
||||
// 写入最新快照
|
||||
fs.writeFileSync(localPath, JSON.stringify(snapshot, null, 2));
|
||||
|
||||
// 写入历史快照(保留最近50个)
|
||||
const historyDir = config.local?.history_path;
|
||||
if (historyDir) {
|
||||
if (!fs.existsSync(historyDir)) {
|
||||
fs.mkdirSync(historyDir, { recursive: true });
|
||||
}
|
||||
const historyFile = path.join(
|
||||
historyDir,
|
||||
`${new Date().toISOString().replace(/[:.]/g, "-")}.json`
|
||||
);
|
||||
fs.writeFileSync(historyFile, JSON.stringify(snapshot, null, 2));
|
||||
|
||||
// 清理旧文件
|
||||
const files = fs
|
||||
.readdirSync(historyDir)
|
||||
.filter((f) => f.endsWith(".json"))
|
||||
.sort()
|
||||
.reverse();
|
||||
for (let i = 50; i < files.length; i++) {
|
||||
fs.unlinkSync(path.join(historyDir, files[i]));
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("[watcher] 本地文件写入失败:", e.message);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 推送到Gitea
|
||||
*/
|
||||
function reportToGitea(snapshot) {
|
||||
if (!config.gitea) return;
|
||||
|
||||
const token = process.env[config.gitea.token_env] || "";
|
||||
if (!token) {
|
||||
console.warn("[watcher] GITEA_TOKEN 未设置,跳过Gitea上报");
|
||||
return;
|
||||
}
|
||||
|
||||
const { url, owner, repo, snapshot_path } = config.gitea;
|
||||
const content = Buffer.from(JSON.stringify(snapshot, null, 2)).toString("base64");
|
||||
const apiPath = `/api/v1/repos/${owner}/${repo}/contents/${snapshot_path}/latest.json`;
|
||||
|
||||
const payload = JSON.stringify({
|
||||
content: content,
|
||||
message: `📓 终端快照 ${SERVER_ID} ${new Date().toISOString().substring(0, 16)}`,
|
||||
branch: "main"
|
||||
});
|
||||
|
||||
// 先尝试获取文件sha(用于更新)
|
||||
const getOptions = {
|
||||
hostname: url.replace("https://", "").replace("http://", ""),
|
||||
path: apiPath,
|
||||
method: "GET",
|
||||
headers: { Authorization: `token ${token}` }
|
||||
};
|
||||
|
||||
const httpModule = url.startsWith("https") ? https : http;
|
||||
|
||||
const req = httpModule.request(getOptions, (res) => {
|
||||
let body = "";
|
||||
res.on("data", (chunk) => (body += chunk));
|
||||
res.on("end", () => {
|
||||
let sha = null;
|
||||
try {
|
||||
const data = JSON.parse(body);
|
||||
sha = data.sha;
|
||||
} catch {
|
||||
// 文件不存在,创建新文件
|
||||
}
|
||||
|
||||
// 发送更新请求
|
||||
const updatePayload = JSON.parse(payload);
|
||||
if (sha) updatePayload.sha = sha;
|
||||
|
||||
const putOptions = {
|
||||
hostname: getOptions.hostname,
|
||||
path: apiPath,
|
||||
method: "PUT",
|
||||
headers: {
|
||||
Authorization: `token ${token}`,
|
||||
"Content-Type": "application/json",
|
||||
"Content-Length": Buffer.byteLength(JSON.stringify(updatePayload))
|
||||
}
|
||||
};
|
||||
|
||||
const putReq = httpModule.request(putOptions, (putRes) => {
|
||||
let putBody = "";
|
||||
putRes.on("data", (chunk) => (putBody += chunk));
|
||||
putRes.on("end", () => {
|
||||
if (putRes.statusCode >= 200 && putRes.statusCode < 300) {
|
||||
// 成功
|
||||
} else {
|
||||
console.error(
|
||||
`[watcher] Gitea推送失败: ${putRes.statusCode} ${putBody.substring(0, 100)}`
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
putReq.on("error", (e) => {
|
||||
console.error("[watcher] Gitea推送错误:", e.message);
|
||||
});
|
||||
|
||||
putReq.write(JSON.stringify(updatePayload));
|
||||
putReq.end();
|
||||
});
|
||||
});
|
||||
|
||||
req.on("error", () => {
|
||||
// Gitea不可达,静默失败
|
||||
});
|
||||
|
||||
req.setTimeout(10000, () => {
|
||||
req.destroy();
|
||||
});
|
||||
|
||||
req.end();
|
||||
}
|
||||
|
||||
// ─── 主循环 ───────────────────────────────────────────────────
|
||||
|
||||
function collect() {
|
||||
const snapshot = {
|
||||
server_id: SERVER_ID,
|
||||
server_name: config.server_name || SERVER_ID,
|
||||
timestamp: new Date().toISOString(),
|
||||
system: config.collectors?.system !== false ? collectSystem() : null,
|
||||
services: config.collectors?.services !== false ? collectServices() : null,
|
||||
errors: config.collectors?.logs !== false ? collectLogs() : null,
|
||||
deploy: config.collectors?.deploy !== false ? collectDeploy() : null,
|
||||
runner: config.collectors?.runner !== false ? collectRunner() : null
|
||||
};
|
||||
|
||||
// 告警检测
|
||||
const alerts = [];
|
||||
if (snapshot.system && !snapshot.system.error) {
|
||||
const thresholds = config.alerts || {};
|
||||
if (snapshot.system.cpu_percent > (thresholds.cpu_percent || 90)) {
|
||||
alerts.push(`CPU ${snapshot.system.cpu_percent}% 超过阈值 ${thresholds.cpu_percent || 90}%`);
|
||||
}
|
||||
if (snapshot.system.memory_percent > (thresholds.memory_percent || 90)) {
|
||||
alerts.push(
|
||||
`内存 ${snapshot.system.memory_percent}% 超过阈值 ${thresholds.memory_percent || 90}%`
|
||||
);
|
||||
}
|
||||
if (snapshot.system.disk_used_percent > (thresholds.disk_percent || 85)) {
|
||||
alerts.push(
|
||||
`磁盘 ${snapshot.system.disk_used_percent}% 超过阈值 ${thresholds.disk_percent || 85}%`
|
||||
);
|
||||
}
|
||||
}
|
||||
if (snapshot.services) {
|
||||
for (const [name, status] of Object.entries(snapshot.services.systemd || {})) {
|
||||
if (status !== "active" && config.alerts?.service_down !== false) {
|
||||
alerts.push(`服务 ${name} 状态异常: ${status}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
snapshot.alerts = alerts;
|
||||
snapshot.alert_count = alerts.length;
|
||||
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
function tick() {
|
||||
try {
|
||||
const snapshot = collect();
|
||||
|
||||
// 上报
|
||||
if (config.reporters?.file !== false) {
|
||||
reportToFile(snapshot);
|
||||
}
|
||||
if (config.reporters?.gitea !== false) {
|
||||
reportToGitea(snapshot);
|
||||
}
|
||||
|
||||
// 简洁日志
|
||||
const cpu = snapshot.system?.cpu_percent || "?";
|
||||
const mem = snapshot.system?.memory_percent || "?";
|
||||
const alerts = snapshot.alert_count || 0;
|
||||
const alertMark = alerts > 0 ? ` ⚠️${alerts}告警` : "";
|
||||
console.log(
|
||||
`[watcher] ${snapshot.timestamp.substring(11, 19)} CPU:${cpu}% MEM:${mem}%${alertMark}`
|
||||
);
|
||||
} catch (e) {
|
||||
console.error("[watcher] 采集失败:", e.message);
|
||||
}
|
||||
}
|
||||
|
||||
// ─── 启动 ─────────────────────────────────────────────────────
|
||||
|
||||
console.log(`[watcher] 终端守望者启动 · ${SERVER_ID} · 间隔 ${INTERVAL / 1000}s`);
|
||||
console.log("[watcher] 铸渊 · ICE-GL-ZY001 · TCS-0002∞");
|
||||
|
||||
// 首次立即采集
|
||||
tick();
|
||||
|
||||
// 定时采集
|
||||
setInterval(tick, INTERVAL);
|
||||
Loading…
x
Reference in New Issue
Block a user