Files
BlueRoseNote/07-Other/AI/AI Agent/WY/团队服务器.md

22 KiB
Raw Blame History

IP

  • TA.Netease.com
    • SSH10.145.96.67:32200
    • ssh -p 32200 -i C:\Users\loujiajie.ssh\netease_loujiajie_id_rsa loujiajie@10.145.96.67
  • Artlib
    • SSH10.145.96.68:32200
    • ssh -p 32200 -i C:\Users\loujiajie.ssh\netease_loujiajie_id_rsa loujiajie@10.145.96.68
  • Artlib S3
    • NOS 桶信息
      • 名称matrixaita
      • 项目artct
      • 成本项目artct
      • 区域GA
      • Endpointgzdev
      • 桶用户p-artct-matrixaita
      • 域名(内网):nos-gzdev.163nos.com
      • Access Key57SCV9Q4MPLXQ3JCL5K9
      • Secret Key4dzMcakyxW2vEhCGjEfiXHtDgxZUiy57D9NqKOOm

VPS_SSH_KEY

文档

垃圾电脑服务

我现在想在局域网里的另一台Ubuntu电脑上部署Unreal Horde服务存储 缓存以及管理各个worker、节点信息本机性能强劲作为worker进行具体工作。

  • 电脑ip10.219.36.57
  • 用户名netease
  • 密码123

官方文档UE5.5 - https://dev.epicgames.com/documentation/zh-cn/unreal-engine/horde-in-unreal-engine?application_version=5.5

下载地址

大致步骤

  1. 使用docker部署服务。
  2. 服务器配置。
    1. 默认端口Http 13340、Http 2 13342。
      1. 默认情况下Horde配置为使用端口5000通过未加密的HTTP提供数据。在默认情况下代理通过端口5002使用未加密的HTTP/2上的gRPC与Horde服务器通信。这些设置在服务器启动时显示在控制台上。
    2. 相关配置
      • 服务器配置将配置该服务器与其他服务器的通信,定义静态参数等。它由与该服务器一同部署的 Server.json 文件驱动。
      • 全局配置在部署后控制所有面向用户的元素,该配置存于名为 Globals.json 的文件中。设置好部署参数后,大多数配置都在此处完成。
    3. 验证相关配置
      1. #OIDC身份验证
  3. 代理机配置。详见 http://10.219.103.35:13340/docs/Landing.md
    1. 配置文件位置:安装目录\Agent\Defaults\agent.json

部署笔记

部署流程

Perforce服务器

ssl:inner02-commit.perforce.nie.netease.com:1667

OIDC

Netease OIDC信息

client id 4884b03e951711f0ad370242ac120002
client secret f2862b8c5ab24085ab0883d119b631604884b354951711f0ad370242ac120002

OIDC 参考代码

#coding:UTF-8
"""
Requirements:
    1. Flask >= 0.10.1
    2. requests
    3. jwkest >= 1.1.7

Usage:
    python oidc_code_demo.py -H {listen_address} -p {listen_port}

Help:
    python oidc_code_demo.py -h
"""
import os
import uuid
from hashlib import md5
import datetime
from urllib import urlencode
import json
import requests
from flask import Flask, request, jsonify, session, redirect

from jwkest.jwk import SYMKey
from jwkest.jws import JWS
from jwkest.jwk import load_jwks_from_url
from jwkest.jws import NoSuitableSigningKeys

__revision__ = "0.01"
__author__ = "chenxs@corp.netease.com"

OIDC_CLIENT_ID = ""
OIDC_CLIENT_SECRET = ""
OIDC_PROVIDER = "https://login.netease.com/connect"
OIDC_AUTHORIZATION_SERVER = "https://login.netease.com/connect/authorize"
OIDC_TOKEN_ENDPOINT = "https://login.netease.com/connect/token"
OIDC_USERINFO_ENDPOINT = "https://login.netease.com/connect/userinfo"
OIDC_SCOPE = "openid nickname email fullname dep title empno"
OIDC_REDIRECT_URI = "https://127.0.0.1:5000/finish"
OIDC_JWKS_URI = "https://login.netease.com/connect/jwks"
OIDC_ALG = "HS256"

PYTHON_OIDC_DEMO = Flask(__name__)

@PYTHON_OIDC_DEMO.route("/", methods=['GET'])
def index():
    """index"""
    if 'username' in session:
        body = (
            u"</br><h2>OpenID Connect 鐧诲綍鎴愬姛銆<E5A79B></h2></br>"
            u"鎮ㄧ殑鐢ㄦ埛鍚嶆槸锛<E6A7B8>%s</br>"
            u"鎮ㄧ殑鍏ㄥ悕鏄細%s</br>"
            u"鎮ㄧ殑閭鏄細%s</br>"
            u"鎮ㄧ殑鑱屼綅鏄細%s</br>"
            u"鎮ㄧ殑閮ㄩ棬鏄細%s</br>"
            u"鎮ㄧ殑宸ュ彿鏄細%s</br>") % (
                session['username'], session.get('fullname', ''),
                session.get('email', ''), session.get('title', ''),
                session.get('dep', ''), session.get('empno', ''))
        body += u"<a href='/login'>鎴虫垜閲嶆柊鐧诲綍</a>"
        return body
    else:
        return u"<a href='/login'>鎴虫垜鐧诲綍</a>"

@PYTHON_OIDC_DEMO.route("/login", methods=['GET'])
def login():
    """AuthN Request"""
    session.clear()
    now = datetime.datetime.now().strftime("%s")
    session['uid'] = uuid.uuid4().hex
    session['state'] = session['uid']
    session['nonce'] = md5(session['uid'] + now).hexdigest()

    authn_request_params = {
        'response_type': 'code',
        'client_id': OIDC_CLIENT_ID,
        'state': session['state'],
        'nonce': session['nonce'],
        'scope': OIDC_SCOPE,
        'redirect_uri': OIDC_REDIRECT_URI,
        #'prompt': 'login',
        'display': 'touch',
    }

    redirect_url = "?".join([
        OIDC_AUTHORIZATION_SERVER, urlencode(authn_request_params)])

    return redirect(redirect_url)


def token_request(code):
    """2. Token Request"""
    params = {
        'grant_type': 'authorization_code',
        'code': code,
        'redirect_uri': OIDC_REDIRECT_URI,
        'client_id': OIDC_CLIENT_ID,
        'client_secret': OIDC_CLIENT_SECRET,
    }
    _resp = requests.post(OIDC_TOKEN_ENDPOINT, data=params)
    return json.loads(_resp.text)

def id_token_verify(id_token, nonce=None):
    """3. id token verify"""

    now = int(datetime.datetime.now().strftime("%s"))
    if OIDC_ALG == "HS256":
        signed_keys = [SYMKey(key=OIDC_CLIENT_SECRET)]
    else:
        signed_keys = load_jwks_from_url(OIDC_JWKS_URI)

    try:
        plain_id_token = JWS().verify_compact(id_token, signed_keys)
    except NoSuitableSigningKeys:
        # logger the id_token please
        return {'error': 'can not verify the id token'}

    print "idtoken: %s" % plain_id_token
    if nonce:
        if (not plain_id_token.has_key('nonce')) or (
                plain_id_token['nonce'] != nonce):
            return {'error': 'id token nonce not correct'}
    if plain_id_token['iss'] != OIDC_PROVIDER:
        return {'error': 'id token iss not correct'}
    if plain_id_token['aud'] != OIDC_CLIENT_ID:
        return {'error': 'id token aud not correct'}
    if now >= int(plain_id_token['exp']):
        return {'error': 'id token expired'}

    return {'id_token': plain_id_token}


@PYTHON_OIDC_DEMO.route("/finish", methods=['GET'])
def finish():
    """
    1. AuthN Response
    2. Token Request
    3. id token verify
    4. userinfo request
    5. login user
    """

    # 1. AuthN Response
    try:
        code = request.args.get('code')
        if session['state']:
            state = request.args.get('state')
            if state != session['state']:
                return u"闈炴硶璇锋眰"
    except ValueError:
        return u"闈炴硶璇锋眰"
    # 2. Token Request
    token = token_request(code)
    print "token: %s" % token
    if token.has_key('error'):
        return u"鍑洪敊浜嗭細%s" % str(token)
    # 3. id token verify
    id_token_verified = id_token_verify(token['id_token'])
    if id_token_verified.has_key('error'):
        return id_token_verified['error']
    else:
        id_token = id_token_verified['id_token']

    print "id_token: %s" % id_token

    # 4. userinfo request
    _req_session = requests.Session()
    _req_session.headers.update({
        "Authorization": "Bearer %s" % token['access_token']})
    userinfo_req = _req_session.get(OIDC_USERINFO_ENDPOINT)
    userinfo = json.loads(userinfo_req.text)
    # login the user
    session['username'] = userinfo['nickname']
    session['email'] = userinfo['email']
    session['title'] = userinfo.get('title', '')
    session['empno'] = userinfo.get('empno', '')
    session['dep'] = userinfo.get('dep', '')
    session['fullname'] = userinfo.get('fullname', '')
    return redirect("/")

if __name__ == "__main__":
    import sys
    import argparse
    parser = argparse.ArgumentParser(
        usage='%(prog)s [options]', version='%(prog)s ' + str(__revision__))
    parser.add_argument(
        '-H', '--host', dest='host', type=str,
        help="Specify listening adress, default is 127.0.0.1")
    parser.add_argument(
        '-p', '--port', dest='port', type=int,
        help="Specify listening port, default is 5000")
    parser.add_argument(
        '-c', '--client_id', dest='client_id', type=str,
        help="oidc client_id is required.")
    parser.add_argument(
        '-s', '--client_secret', dest='client_secret', type=str,
        help="oidc client_secret is required.")

    args = parser.parse_args()

    host = args.host or '127.0.0.1'
    port = args.port or 5000
    OIDC_REDIRECT_URI = "http://%s:%s/finish" % (host, port)
    OIDC_CLIENT_ID = args.client_id
    OIDC_CLIENT_SECRET = args.client_secret
    if not OIDC_CLIENT_ID or not OIDC_CLIENT_SECRET:
        parser.print_help()
        parser.exit()

    PYTHON_OIDC_DEMO.secret_key = "this is a random secret"
    PYTHON_OIDC_DEMO.debug = True
    PYTHON_OIDC_DEMO.run(host=host, port=port)

本机部分

根据搜索结果,你通过 Claude 部署 Unreal Horde 的工作主要在以下目录进行:

主要工作目录

┌──────────────────────────────────────┬────────────────────────────────────────────────────────────────────┐ │ 目录 │ 内容 │ ├──────────────────────────────────────┼────────────────────────────────────────────────────────────────────┤ │ C:\Users\loujiajie\horde-deploy\ │ 部署脚本主目录 — 26 个 Python 脚本 + docker-compose.yml + 配置文件 │ ├──────────────────────────────────────┼────────────────────────────────────────────────────────────────────┤ │ C:\Users\loujiajie\docs\superpowers\ │ 设计文档和实施计划2026-04-17 │ └──────────────────────────────────────┴────────────────────────────────────────────────────────────────────┘

部署目标

  • 远程 Ubuntu 服务器 10.219.36.57Docker Compose 运行 Horde Server + MongoDB + Redis远程路径 /home/netease/docker/UnrealHorde/
  • 本地 Windows Agent注册在 C:\Users\loujiajie\AppData\Local\Epic Games\Horde\Agent\Agent ID HIH-D-X41334

相关 Claude 会话

  • Claude 计划文件 C:\Users\loujiajie.claude\plans\vivid-jingling-elephant.md 记录了 UBA 远程编译加速配置
  • Claude 会话记录在 C:\Users\loujiajie.claude\projects\C--Users-loujiajie\ 下

D:\UnrealHorde\ 目录虽然存在但是空的(已被回收站中有删除痕迹),实际部署工作都在 C:\Users\loujiajie\horde-deploy
完成。

当前状态以及后续部署

更新日期2026-04-22

架构概览

┌─────────────────────────────────────────────────────────────────────┐
│                    Horde Server (Linux Docker)                      │
│                    10.219.36.57                                     │
│  ┌───────────┐  ┌──────────────────┐  ┌───────┐  ┌──────────────┐ │
│  │  MongoDB   │  │  Horde Server    │  │ Redis │  │    Nginx     │ │
│  │  :27017    │  │  :5001→5000      │  │ :6379 │  │  :5000(未用) │ │
│  │  (7.0.5)   │  │  :5002 (gRPC)    │  │(6.2)  │  │             │ │
│  └───────────┘  └──────────────────┘  └───────┘  └──────────────┘ │
│  Docker Compose: /home/netease/docker/UnrealHorde/                 │
│  数据目录: ./data/server.json, ./data/globals.json                  │
└─────────────────────────────────────────────────────────────────────┘
        │ :5001 HTTP API              │ :5002 gRPC
        ▼                             ▼
┌──────────────────┐     ┌──────────────────────┐
│  Agent 1 (本机)   │     │  Agent 2 (远程)       │
│  HIH-D-X41334    │     │  HIH-D-X13864        │
│  10.219.103.35   │     │  10.219.32.45        │
│  Ryzen 9 9900X   │     │  Ryzen 9 9900X       │
│  64GB RAM        │     │  62GB RAM            │
│  Windows 11      │     │  Windows 11          │
│  UBA 发起方       │     │  UBA 远程 Worker      │
│  端口: 1345(入站) │     │  端口: 7000-7002(入站)│
└──────────────────┘     └──────────────────────┘

当前已完成配置

Horde Server (v5.7.0-46863086)

  • 部署方式: Docker Compose (horde-server + mongodb + redis + nginx)
  • 访问地址: http://10.219.36.57:5001 (Dashboard + API)
  • gRPC 端口: 5002 (Agent 通信)
  • 认证方式: Anonymous免登录已移除 OIDC
  • 管理员: loujiajie
  • 遥测: MongoDB 存储1天保留

Horde Agent

属性 Agent 1 (本机) Agent 2 (远程)
ID HIH-D-X41334 HIH-D-X13864
IP 10.219.103.35 10.219.32.45
CPU AMD Ryzen 9 9900X (12C/24T) AMD Ryzen 9 9900X (12C/24T)
RAM 64GB 62GB
GPU RTX 5070 RTX 5070
安装路径 C:\Program Files\Epic Games\Horde\Agent\ 同左
Server URL 注册表配置,非 JSON 同左
Pool Win-UE5 Win-UE5
注册方式 enrollment token 同左

UBA 远程分布式编译

  • 状态: 已验证可用
  • 效果: 91 个任务分发到远程机器,编译耗时 58.94s vs 本机 70.74s(提升约 17%
  • 防火墙端口要求(双向):
    • 发起方 (本机): 1345-1400 入站 — UBA SessionServer 监听,远程 UbaClient 连回
    • 远程 Worker: 7000 入站 (Compute)、7001 入站 (UbaAgent Listen)、7002 入站 (Proxy)
  • 远程机器依赖: 已安装 MSVC Build Tools (14.44.35207) + Windows SDK

已加载的 Server 插件

插件 状态 说明
tools 已加载 工具分发Agent、UGS 安装包等)
build 已加载 CI/CD 构建(未配置 project/stream
storage 已加载 内容存储(默认文件系统后端)
compute 已加载 远程计算/UBA已在使用
symbols 已加载 符号服务器(未配置)
secrets 已加载 密钥管理(未配置)
ddc 已加载 Derived Data Cache未配置
analytics 已加载 编辑器遥测分析(未配置)

配置文件位置

服务器端 (10.219.36.57)

  • Docker Compose: /home/netease/docker/UnrealHorde/docker-compose.yml
  • Server 配置: /home/netease/docker/UnrealHorde/data/server.json(重启生效)
  • Global 配置: /home/netease/docker/UnrealHorde/data/globals.json(热重载)
  • Nginx 配置: /home/netease/docker/UnrealHorde/nginx/nginx.conf(当前未做反向代理)

Agent 端 (Windows)

  • 安装目录: C:\Program Files\Epic Games\Horde\Agent\
  • Server URL: 通过注册表配置,非 appsettings.json
    • 路径: HKLM\SOFTWARE\Epic Games\Horde\AgentSettingsserver
  • Agent 日志: %ProgramData%\Epic Games\Horde\Agent\Logs\

后续可部署功能

优先级 1: Perforce CI/CD 集成(如果需要自动化构建)

价值: 每次 P4 提交自动触发编译+测试、Preflight 预提交检测、UGS 预编译 Editor 分发

前置条件: 需要 Perforce Server公司已有 ssl:inner02-commit.perforce.nie.netease.com:1667

配置方式: 修改 globals.json

{
  "plugins": {
    "build": {
      "perforceClusters": [{
        "name": "Default",
        "servers": [{ "serverAndPort": "ssl:inner02-commit.perforce.nie.netease.com:1667" }],
        "credentials": [{ "userName": "horde-svc", "password": "xxx" }]
      }],
      "projects": [{
        "id": "farming-game",
        "path": "farming-game.project.json"
      }]
    }
  }
}

还需要编写 project.jsonstream.json 定义构建任务(可参考 /app/Defaults/ue5.project.json 模板)。

注意: Horde 仅支持 Perforce Streams不支持传统分支。

优先级 2: Editor Telemetry / Analytics零外部依赖

价值: 可视化团队编译时间、Shader 编译、Editor 启动时间等指标

UE 项目端Config/DefaultEngine.ini 添加:

[StudioTelemetry.Provider.HordeAnalytics]
ProviderModule=AnalyticsET
APIKeyET=HordeAnalytics.Dev
APIServerET=http://10.219.36.57:5001/
APIEndpointET=api/v1/telemetry/engine
UsageType=EditorAndClient

Horde 端globals.json 添加:

{
  "plugins": {
    "analytics": {
      "stores": [{
        "id": "engine",
        "include": ["$(HordeDir)/Defaults/default-metrics.telemetry.json"]
      }]
    }
  }
}

优先级 3: DDC 共享缓存(多人协作时有价值)

价值: 避免每台机器重复编译 Shader、重复 Cook Asset

两种方案:

  1. 使用 Horde 内置 DDC 端点 — Horde Server DDC 插件已加载,配置 UE 指向即可
  2. 部署独立 Zen DDC Server — 更适合大规模团队,独立服务

评估: 当前 2 台机器做 C++ 编译为主DDC 主要优化 Shader/Asset 场景。团队扩大后优先考虑。

优先级 4: UGS (UnrealGameSync)

价值: 美术无需本地编译 Editor直接从 Horde 下载预编译二进制

前置条件: 必须先配好 Perforce CI/CD 集成(需要 Incremental Build 任务产出 PCB

安装: Horde Dashboard → Tools → Downloads 下载 UGS 安装包,安装时选 "Horde" 并填写 Server URL

优先级 5: 其他(按需)

功能 何时需要 配置复杂度
Symbol Server 需要分析 crash dump 时 低 — globals.json 加 symbols store
Secrets 管理 启用 CI/CD 需要存凭据时 低 — globals.json 加 secrets 条目
Storage 外部后端 构建产物/日志量大时 中 — 可配 S3/Azure/NFS 替代本地磁盘
Compute 网络优化 多网段 Agent 时 低 — globals.json 按 CIDR 配置网络
HTTPS + OIDC 需要安全认证时 高 — 需配 nginx 反向代理 + SSL + OIDC redirect_uri 注册

已知问题

  1. Nginx 未配置: 当前 nginx 容器使用默认配置,未做反向代理。如需 HTTPS 需重新配置。
  2. MapViewOfFile 分页错误: 本机编译时偶发 9-38 次 paging error可能需增大 page file。
  3. Agent HIH-D-X13864 (远程) 经常 Offline: 该 Agent 状态不稳定,可能需要检查 HordeAgent 服务自动重启。
  4. OIDC 已移除: 当前 Anonymous 模式无访问控制,内网使用可接受,外网暴露需重新配置认证。