使用说明
🚀 MCP矿池系统v2.0
分布式采矿系统mit 2台专用MCP服务器:
- 作业服务器 (端口8082) - 通过HTTP /mcp为n8n AI代理(任务管理)
- 设备服务器 (端口 8083) - 通过 SSE 和 REST 使用 Unity 设备
真正的采矿集成 没有模拟器,没有假数据。默认情况下启用了 Real Mining 。
快速启动
docker compose up -d --build
先决条件:
.env包含您真正的Monero钱包(XMR_WALLET_ADDRESS)并且包含在repo中。REAL_MINING_ENABLED=1(已设定)。
重要:容器直接记录真实的 Stratum 事件;不再有模拟设备。
配置 (.env)
相关密钥:
FASTMCP_HOST/FASTMCP_PORT(作业服务器,HTTP/mcp)DEVICE_SERVER_HOST/DEVICE_SERVER_PORT(团结苏格兰)XMR_WALLET_ADDRESS(实际采矿需要)POOL_HOST/POOL_PORT(设置 Nanopool Defaults)REAL_MINING_ENABLED=1(启用 Stratum Client)ASSIGN_TTL,MAX_INFLIGHT_PER_DEVICE,DEFAULT_CHUNK_SIZE,DEFAULT_NUM_TASKS
作业服务器(n8n/HTTP)
- 端点:
http://<host>:8082/mcp - 协议:流式HTTP(基于HTTP/SSE的JSON-RPC 2.0)
- 所需标题:
Accept: application/json, text/event-stream,mcp-session-id: <unique>
n8n MCP客户端设置
- 网址:
http://<host>:8082/mcp - 传输/协议:HTTP(流式HTTP)
- 工具:
create_mining_job(num_tasks, chunk_size)get_pool_status()get_leaderboard()get_job_details(job_id)list_devices()- (实际采矿)
get_mining_job(device_id, device_type),submit_mining_result(task_id, nonce, result_hash)
示例调用(curl)
curl -X POST http://localhost:8082/mcp \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-H "mcp-session-id: demo-1" \
-d '{"jsonrpc": "2.0", "id": 1, "method": "tools/list", "params": {}}'
设备服务器(Unity/SSE)
- 上海证券交易所
http://<host>:8083/sse?device_id=Unity1 - 注册(REST):
POST /device/register返回钱包/池 - 挖掘提交(REST):
POST /mining/submit→ echte股票/统计数据 - 状态:
GET /status?device_id=...
Unity 通过 SSE 接收任务并发布结果。不再启动假设备。
真实采矿
- Stratum 客户端在下列情况下自动启动:
REAL_MINING_ENABLED=1和XMR_WALLET_ADDRESS已设置。 - 游泳池设置:
xmr-eu1.nanopool.org:10300,工人姓名通过WORKER_NAME. - 日志显示实际的Stratum事件;只要有真正的工作,就不会创建虚拟工作。
操作/故障排除
- 重新启动 :
docker compose restart game-mining-mcp - 记录作业/设备:
docker logs -f game-mining-mcp - Redis:
docker logs -f redis - 如果无法访问池,请检查钱包/池主机/端口和防火墙。
目录结构(已清除)
-
mcp_job_server.pyHTTP /mcp 为 n8n -
mcp_device_server.pySSE/REST 用于 Unity -
shared_state.py,redis_state.py--状态/队列 -
stratum_proxy.py— 然而 Stratum 客户端 -
start_mining.sh— 启动工作和设备服务器 -
docker-compose.yml,Dockerfile,.env -
data/— 空, 用于到期数据头球📊 运行时状态”)] [SerializeField]私有ConnectionStatus状态=ConnectionStatus。断开的; [SerializeField]私有int活动任务=0; [SerializeField]私有int任务已完成=0; [SerializeField]私有int共享提交=0; [SerializeField]私有int sharesAccepted=0; [SerializeField]私有浮点数currentHashrate=0f; [SerializeField]私有字符串lastError=“”;
头球🐛 调试“)] public bool enableDebugLogs=true; public bool showTaskDetails=false;
//私人 私有UnityWebRequest请求; 私有SSEDownloadHandler sseHandler; 专用队列 taskQueue=新队列(); 私人列表 activeTaskCoroutines=新列表(); private bool isReconnecting=false;
//采矿发动机 私人randomxminer; 私人SHA256Miner SHA256Miner;
//绩效跟踪 私有浮点数lastHashrateUpdate; 私有长合计HashesComputed;
public void AddTotalHashesComputed(long count) { totalHashesComputed += count; }公共枚举连接状态 { 断开的, 连接, 有联系的, 重新连接, 错误 }
公共枚举设备类型 { 汽车, 移动电话, 桌面, 浏览器 }
公共枚举挖掘算法 { RandomX,//门罗币(CPU友好型) SHA256, // 比特币(用于测试) 以太坊经典(GPU) }
// ================================================================ //生命周期 // ================================================================
private void Awake() { if(字符串.IsNullOrEmpty(设备ID)) { deviceId=生成设备ID(); }
// Device Type Detection if (deviceType == DeviceType.Auto) { deviceType = DetectDeviceType(); } // Mining Engine initialisieren InitializeMiningEngine();}
private void开始() { if(自动连接) { 启动子程序(ConnectToMCPServer()); } }
私有void更新() { // 计算哈希率 如果(Time.Time-lastHashrateUpdate>5f) { currentHashrate=计算的总哈希数/5f; totalHashesComputed=0; lastHashrateUpdate=Time.Time; } }
private void OnDestroy() { 断开SSE(); 清洁采矿机(); }
私有字符串GenerateDeviceId() { string platform=应用程序平台。ToString(); string unique=SystemInfo.deviceUniqueIdentifier。子环(0,8); return$“Unity_{platform}_{unique}“; }
私有设备类型检测设备类型() { if(Application.platform==运行时平台.WIGLPlayer) 返回设备类型。浏览器; 否则if(Application.isMobilePlatform) 返回设备类型。移动; 否则 返回设备类型。桌面; }
// ================================================================ //采矿发动机设置 // ================================================================
private void初始化挖掘引擎() { 开关(算法) { 案例挖掘算法。RandomX: randomXMiner=新的randomXMiner(this); 日志(“🔧 RandomX Miner已初始化”); 断裂;
case MiningAlgorithm.SHA256: sha256Miner = new SHA256Miner(); Log("🔧 SHA256 Miner initialized"); break; case MiningAlgorithm.Ethash: LogError("Ethash not yet implemented"); break; }}
私有空间清理采矿引擎() { RandomXMiner?.dispose(); sha256Miner?.Dispose(); }
// ================================================================ //MCP服务器连接-SSE流 // ================================================================
公共IE分子连接到MCPServer() { if(状态==连接状态.正在连接||状态==已连接状态.已连接) { 日志(“已连接/正在连接”); 产量突破; }
status = ConnectionStatus.Connecting; lastError = ""; Log($"🔌 Connecting to MCP Server: {mcpServerUrl}..."); // 1. Health Check yield return HealthCheck(); if (status == ConnectionStatus.Error) { yield return ReconnectCoroutine(); yield break; } // 2. Device Registration yield return RegisterDevice(); // 3. SSE Stream öffnen (kompatibel mit Device Server Port 8083) // Nutzt /sse?device_id= Format (auch /sse/device?device_id= wird unterstützt) string sseUrl = $"{mcpServerUrl}/sse?device_id={deviceId}"; sseRequest = UnityWebRequest.Get(sseUrl); sseHandler = new SSEDownloadHandler(this); sseRequest.downloadHandler = sseHandler; sseRequest.SetRequestHeader("Accept", "text/event-stream"); sseRequest.SetRequestHeader("Cache-Control", "no-cache"); sseRequest.SetRequestHeader("X-Device-Type", deviceType.ToString()); sseRequest.SetRequestHeader("X-Algorithm", algorithm.ToString()); sseRequest.SendWebRequest(); // Warte auf Hello Event float timeout = 10f; while (!sseHandler.isConnected && timeout > 0) { yield return new WaitForSeconds(0.1f); timeout -= 0.1f; } if (sseHandler.isConnected) { status = ConnectionStatus.Connected; Log($"✅ Connected as {deviceId} ({deviceType})"); // Task Processing starten StartCoroutine(ProcessTaskQueue()); } else { status = ConnectionStatus.Error; lastError = "SSE connection timeout"; LogError("Connection timeout"); yield return ReconnectCoroutine(); }}
私有IEnumerator健康检查() { 使用(var req=UnityWebRequest.Get(mcpServerUrl+“/health”)) { 请求超时=5; 收益回报要求。SendWebRequest();
if (req.result == UnityWebRequest.Result.Success) { Log("✅ MCP Server health check OK"); } else { status = ConnectionStatus.Error; lastError = "MCP Server unreachable: " + req.error; LogError("Health check failed: " + req.error); } }}
私有IE分子注册设备() { var设备信息=新设备注册 { device_id=设备id, device_type=设备类型。ToString(), 算法=算法。ToString(), max_countrent_tasks=maxConcurrentTasks, platform=应用程序平台。ToString(), system_info=新系统信息2 { processor_type=系统信息处理器类型, processor_count=系统信息处理器计数, system_memory_size=系统信息系统内存大小, graphics_device_name=系统信息图形设备名称 } };
string json = JsonUtility.ToJson(deviceInfo); using (var req = new UnityWebRequest(mcpServerUrl + "/device/register", "POST")) { req.uploadHandler = new UploadHandlerRaw(Encoding.UTF8.GetBytes(json)); req.downloadHandler = new DownloadHandlerBuffer(); req.SetRequestHeader("Content-Type", "application/json"); req.timeout = 10; yield return req.SendWebRequest(); if (req.result == UnityWebRequest.Result.Success) { Log("✅ Device registered with MCP Server"); try { var response = JsonUtility.FromJson<DeviceRegistrationResponse>(req.downloadHandler.text); // Server-verwaltete Wallet verwenden (zentral verwaltet) currentWallet = response.wallet_address; currentPoolAddress = response.pool_address; if (string.IsNullOrEmpty(currentWallet)) { LogError("⚠️ Keine Wallet vom Server erhalten!"); } else { Log($"💰 Wallet assigned: {currentWallet}"); } if (!string.IsNullOrEmpty(currentPoolAddress)) { Log($"🏊 Pool: {currentPoolAddress}"); } } catch (Exception e) { LogError($"Registration response parse error: {e.Message}"); } } else { LogError($"Device registration failed: {req.error}"); } }}
private void DisconnectSSE() { if(sseRequest!=空) { sseRequest。中止(); sseRequest。Dispose(); sseRequest=空; }
status = ConnectionStatus.Disconnected; Log("🔌 Disconnected from MCP Server");}
私有IEnumerator重新连接协程() { 如果(正在重新连接)屈服断裂;
isReconnecting = true; status = ConnectionStatus.Reconnecting; Log($"🔄 Reconnecting in {reconnectDelay}s..."); DisconnectSSE(); yield return new WaitForSeconds(reconnectDelay); isReconnecting = false; yield return ConnectToMCPServer();}
// ================================================================ //SSE事件处理程序 // ================================================================
公共无效OnSSEConnected() { 日志(“📡 SSE流已打开”); }
public void OnSSEMessage(字符串事件类型,字符串数据) { 尝试 { if(事件类型==“你好”) { 日志($“👋 MCP服务器:{data}“); } 否则if(事件类型==“mining_job”) { //真实采矿作业vom设备服务器(端口8083) OnMiningJobReceived(数据); } else if(事件类型==“赋值”) { // 支持基于分配的模拟 收到分配(数据); } 否则if(事件类型==“pool_update”) { OnPoolUpdate(数据); } 否则,如果(事件类型==“钱包更新”) { OnWalletUpdate(数据); } 否则 { if(showTaskDetails) 日志($)($)📨 SSE:{eventtype}={data}“); } } catch(例外情况e) { LogError($“SSE消息错误:{e.message}”); } }
public void OnSSEError(字符串错误) { LogError($“SSE错误:{Error}”); status=连接状态。错误; lastError=错误; StartCoroutine(ReconnectCoroutine); }
private void OnMiningJobReceived(字符串json) { 尝试 { RealMiningTask任务=JsonUtility。FromJson(json);
if (task == null || string.IsNullOrEmpty(task.job_id)) { LogError("Invalid mining job data"); return; } taskQueue.Enqueue(task); Log($"📥 Mining Job queued: {task.job_id}"); if (showTaskDetails) { Log($" Pool: {task.pool_job.pool_address}"); Log($" Difficulty: {task.pool_job.difficulty}"); Log($" Algorithm: {task.pool_job.algorithm}"); } } catch (Exception e) { LogError($"Mining job parse error: {e.Message}"); }}
private void OnAssignmentReceived(字符串json) { // 传统支持:设备服务器也可以发送“分配”事件 // 如果只有 assignment_id + job_id 尝试 { Log()📨 接收分配(使用mining_job格式以获得最佳兼容性)”; } catch(例外情况e) { LogError($“分配解析错误:{e.Message}”); } }
private void OnPoolUpdate(字符串json) { 尝试 { var更新=JsonUtility。FromJson(json); 日志($“🏊 池更新:难度={Update.current_difficity},哈希率={Update.Pool_Hashrate}“); } catch(例外情况e) { LogError($“池更新解析错误:{e.Message}”); } }
private void OnWalletUpdate(字符串json) { 尝试 { var更新=JsonUtility。FromJson(json); currentWallet=update.wallet_address; 日志($“💰 钱包已更新:{currentWallet}“); } catch(例外情况e) { LogError($“钱包更新解析错误:{e.Message}”); } }
// ================================================================ //基于队列的任务处理 // ================================================================
私有IE分子进程任务队列() { while(状态==连接状态。已连接) { //清理已完成的任务 activeTaskCoroutines。全部移除(c=>c==null); activeTasks=activeTask-Coroutines。计数;
// Start new tasks if available while (taskQueue.Count > 0 && activeTasks < maxConcurrentTasks) { RealMiningTask task = taskQueue.Dequeue(); var coroutine = StartCoroutine(MineRealTask(task)); activeTaskCoroutines.Add(coroutine); activeTasks++; } yield return new WaitForSeconds(0.1f); }}
// ================================================================ //真实挖掘算法调度 // ================================================================
私有IE分子MineRealTask(RealMiningTask任务) { Log($"⛏️ 挖掘已开始:{task.job_id}({task.pool_job.algority})“);
float startTime = Time.time; // Algorithm-spezifisches Mining MiningResult result = null; switch (task.pool_job.algorithm.ToLower()) { case "randomx": yield return randomXMiner.Mine(task, r => result = r); break; case "sha256": case "sha256d": yield return sha256Miner.Mine(task, r => result = r); break; default: LogError($"Unsupported algorithm: {task.pool_job.algorithm}"); yield break; } float duration = Time.time - startTime; if (result != null && showTaskDetails) { Log($"⛏️ Mining completed in {duration:F2}s"); Log($" Hashes computed: {result.hashes_computed}"); Log($" Hashrate: {result.hashes_computed / duration:F0} H/s"); } // Result submitten if (result != null) { yield return SubmitMiningResult(task, result); } tasksCompleted++; activeTasks--;}
// ================================================================ //结果提交到MCP服务器 // ================================================================
私有IEnumerator提交挖掘结果(RealMiningTask任务,挖掘结果) { // Mining Submit Payload - 与 8083 端口上的设备服务器兼容 // 文档中的结构: UNITY_CLIENT_COMPAT.md var payload=新的MiningResultPayload { job_id=task.job_id, device_id=设备id, nonce=结果.nonce, hash=result.hash, hashes_computed=result.hashes_computed, 持续时间秒=结果持续时间秒, 算法=task.pool_job算法 };
string json = JsonUtility.ToJson(payload); // POST /mining/submit an Device Server (Port 8083) using (var req = new UnityWebRequest(mcpServerUrl + "/mining/submit", "POST")) { req.uploadHandler = new UploadHandlerRaw(Encoding.UTF8.GetBytes(json)); req.downloadHandler = new DownloadHandlerBuffer(); req.SetRequestHeader("Content-Type", "application/json"); req.timeout = 10; yield return req.SendWebRequest(); if (req.result == UnityWebRequest.Result.Success) { try { var response = JsonUtility.FromJson<MiningResultResponse>(req.downloadHandler.text); sharesSubmitted++; if (response.share_accepted) { sharesAccepted++; Log($"✅ Share ACCEPTED! ({sharesAccepted}/{sharesSubmitted})"); if (response.block_found) { Log($"🎉🎉🎉 BLOCK FOUND! 🎉🎉🎉"); Log($"💰 Reward: {response.reward} XMR"); } } else { Log($"❌ Share rejected: {response.reason}"); } } catch (Exception e) { LogError($"Submit response parse error: {e.Message}"); } } else { LogError($"Submit failed: {req.error}"); } }}
// ================================================================ //公共API // ================================================================
public void连接() { if(状态!=连接状态。已连接) { 启动子程序(ConnectToMCPServer()); } }
public void断开连接() { StopAllCoroutines(); 断开SSE(); }
public void Reconnect() { 断开连接(); 启动子程序(ConnectToMCPServer()); }
公共字典<string,object>GetStats() { 返回新字典<string,object> { {“status”,state.ToString()}, {“device_id”,deviceId}, {“device_type”,deviceTypeToString()}, {“算法”,algorithm.ToString()}, {“tasks_completed”,tasksCollected}, {“shares_submitted”,shares_submited}, {“shares_accepted”,sharesAccepted}, {“acceptance_rate”,提交的共享>0?(已接受的共享*100f/已提交的共享):0f}, {“current_hashrate”,currentHashrate}, {“active_tasks”,activeTasks}, {“queued_tasks”,taskQueue.Count}, {“钱包”,currentWallet}, {“池”,当前池地址} }; }
// ================================================================ //调试 // ================================================================
public void日志(字符串msg) { if(启用DebugLogs) 调试。日志($“[MCP矿业]{msg}”); }
public void日志错误(字符串msg) { 调试。日志错误($“[MCP挖掘]{msg}”); }
[上下文菜单(“🔌 连接“)] void MenuConnect()=>Connect();
[上下文菜单(“🔌 断开连接”)] void菜单断开连接()=>断开();
[上下文菜单(“🔄 重新连接“)] void菜单重新连接()=>重新连接(;
[上下文菜单(“📊 显示统计数据”)] void菜单显示统计() { var stats = GetStats(); 调试。日志(“===MCP采矿统计===”); foreach(统计中的var kv) { 调试。日志($“{kv.Key}:{kv.Value}”); } } }
// ================================================================ //矿用发动机 // ================================================================
公共类RandomXMiner { 私人UnityMCPClient客户端;
#如果UNITY_WEBGL&&!UNITY_EDITOR [DllImport(“__内部”)] 私有静态外部void InitRandomX();
[DllImport("__Internal")]
private static extern string ComputeRandomX(string input);
#endif
public RandomXMiner(UnityMCPClient client)
{
this.client = client;
#如果UNITY_WEBGL&&!UNITY_EDITOR Initrandomxtools); #endif }
public IEnumerator Mine(RealMiningTask task, System.Action<MiningResult> callback)
{
float startTime = Time.time;
long hashCount = 0;
string blob = task.pool_job.blob;
long target = task.pool_job.target;
// Nonce Range aus Task
uint nonceStart = task.nonce_start;
uint nonceEnd = task.nonce_end;
for (uint nonce = nonceStart; nonce < nonceEnd; nonce++)
{
// Nonce in Blob einfügen (Bytes 39-42 bei Monero)
string blobWithNonce = InsertNonce(blob, nonce);
// RandomX Hash berechnen
string hash = ComputeRandomXHash(blobWithNonce);
hashCount++;
// Target check
if (CheckTarget(hash, target))
{
// SHARE GEFUNDEN!
var result = new MiningResult
{
nonce = (int)nonce,
hash = hash,
hashes_computed = hashCount,
duration_seconds = Time.time - startTime
};
callback?.Invoke(result);
yield break;
}
// Frame Budget
if (hashCount % client.hashesPerFrame == 0)
{
client.AddTotalHashesComputed(client.hashesPerFrame);
yield return null;
}
}
// Kein Share gefunden, trotzdem Statistik zurückgeben
callback?.Invoke(new MiningResult
{
nonce = 0,
hash = "",
hashes_computed = hashCount,
duration_seconds = Time.time - startTime
});
}
private string ComputeRandomXHash(string blob)
{
#如果UNITY_WEBGL&&!UNITY_EDITOR 返回ComputeRandomX(blob); #其他 // Native C# RandomX (如果可用) 或 Fallback // TODO: 包含 RandomX 库 返回“00000000000000000000000000000000000000000000000000000000000”; #endif }
private string InsertNonce(string blob, uint nonce)
{
// Monero: Nonce an Position 39 (Byte 78-85 in Hex)
byte[] bytes = HexToBytes(blob);
byte[] nonceBytes = BitConverter.GetBytes(nonce);
Array.Copy(nonceBytes, 0, bytes, 39, 4);
return BytesToHex(bytes);
}
private bool CheckTarget(string hash, long target)
{
// Hash < Target prüfen
// TODO: Proper BigInteger comparison
return hash.StartsWith("0000");
}
private byte[] HexToBytes(string hex)
{
byte[] bytes = new byte[hex.Length / 2];
for (int i = 0; i < bytes.Length; i++)
{
bytes[i] = Convert.ToByte(hex.Substring(i * 2, 2), 16);
}
return bytes;
}
private string BytesToHex(byte[] bytes)
{
StringBuilder sb = new StringBuilder(bytes.Length * 2);
foreach (byte b in bytes)
{
sb.Append(b.ToString("x2"));
}
return sb.ToString();
}
public void Dispose()
{
// Cleanup
}
}
公共类SHA256Miner { 私人系统。安全。密码学。SHA256 SHA256;
public SHA256Miner()
{
sha256 = System.Security.Cryptography.SHA256.Create();
}
public IEnumerator Mine(RealMiningTask task, System.Action<MiningResult> callback)
{
float startTime = Time.time;
long hashCount = 0;
string blockHeader = task.pool_job.blob;
int difficulty = task.pool_job.difficulty;
string targetPrefix = new string('0', difficulty);
for (int nonce = (int)task.nonce_start; nonce < (int)task.nonce_end; nonce++)
{
string input = $"{blockHeader}:{nonce}";
string hash = ComputeSHA256(input);
hashCount++;
if (hash.StartsWith(targetPrefix))
{
var result = new MiningResult
{
nonce = nonce,
hash = hash,
hashes_computed = hashCount,
duration_seconds = Time.time - startTime
};
callback?.Invoke(result);
yield break;
}
if (hashCount % 10000 == 0)
{
yield return null;
}
}
callback?.Invoke(new MiningResult
{
nonce = 0,
hash = "",
hashes_computed = hashCount,
duration_seconds = Time.time - startTime
});
}
private string ComputeSHA256(string input)
{
byte[] inputBytes = Encoding.UTF8.GetBytes(input);
byte[] hashBytes = sha256.ComputeHash(inputBytes);
StringBuilder sb = new StringBuilder(64);
foreach (byte b in hashBytes)
{
sb.Append(b.ToString("x2"));
}
return sb.ToString();
}
public void Dispose()
{
sha256?.Dispose();
}
}
// ================================================================ //数据模型-真实挖掘 // ================================================================
[可序列化] 公共类RealMiningTask { 公共字符串job_id; 公共PoolJob pool_job; public uint nonce-start; public uint noncend; 公共字符串wallet_address; }
[可序列化] 公共类PoolJob { 公共字符串pool_address; 公共字符串blob; 公共长期目标; 公共信息困难; 公共字符串算法; 公共点高度; }
[可序列化] 公共类MiningResult { public int nonce; 公共字符串哈希; 公共长哈希计算; 公众持股持续时间_秒; }
[可序列化] 公共类设备注册 { 公共字符串device_id; 公共字符串device_type; 公共字符串算法; 公共int max_countrent_tasks; 公共字符串平台; 公共系统信息2系统信息; }
[可序列化] 公共类SystemInfo2 { 公共字符串处理器_ type; 公共int处理器计数; public int system_memory_size; 公共字符串graphics_device_name; }
[可序列化] 公共类设备注册响应 { 公共布尔成功; 公共字符串wallet_address; 公共字符串pool_address; 公共字符串消息; }
[可序列化] 公共类MiningResultPayload { 公共字符串job_id; 公共字符串device_id; public int nonce; 公共字符串哈希; 公共长哈希计算; 公众持股持续时间_秒; 公共字符串算法; }
[可序列化] 公共类MiningResultResponse { 公共bool共享接受; 公共bool block_found; 公众浮动奖励; 公共字符串原因; 公众持股流动平衡; }
[可序列化] 公共类PoolUpdateEvent { public int当前困难; 公共字符串pool_hashrate; 公共点连通矿工; }
[可序列化] 公共类WalletUpdateEvent { 公共字符串wallet_address; 公众持股余额; }
// ================================================================ // SSE DOWNLOAD HANDLER (未更改,可用) // ================================================================
公共类SSEDownloadHandler:DownloadHandlerScript { 私人UnityMCPClient客户端; 私有StringBuilder缓冲区=new StringBuilder(); public bool isConnected=false;
public SSEDownloadHandler(UnityMCPClient client) : base()
{
this.client = client;
}
protected override bool ReceiveData(byte[] data, int dataLength)
{
if (data == null || dataLength == 0)
return true;
string text = Encoding.UTF8.GetString(data, 0, dataLength);
buffer.Append(text);
string bufferStr = buffer.ToString();
string[] parts = bufferStr.Split(new[] { "\n\n" }, StringSplitOptions.None);
for (int i = 0; i < parts.Length - 1; i++)
{
ParseSSEEvent(parts[i]);
}
buffer.Clear();
buffer.Append(parts[parts.Length - 1]);
return true;
}
private void ParseSSEEvent(string eventData)
{
if (string.IsNullOrEmpty(eventData))
return;
string[] lines = eventData.Split('\n');
string eventType = "";
string data = "";
foreach (string line in lines)
{
if (line.StartsWith("event: "))
{
eventType = line.Substring(7).Trim();
}
else if (line.StartsWith("data: "))
{
data = line.Substring(6).Trim();
}
}
if (!string.IsNullOrEmpty(data))
{
if (!isConnected && data.Contains("hello"))
{
isConnected = true;
client.OnSSEConnected();
}
try
{
// Neue Parsing-Logik für Device Server (Port 8083)
// Der Server sendet: data: {json-object mit "type" Feld}
if (data.StartsWith("{"))
{
// Versuche, den "type" aus dem JSON zu extrahieren
int typeStart = data.IndexOf("\"type\":\"") + 8;
int typeEnd = data.IndexOf("\"", typeStart);
if (typeStart > 7 && typeEnd > typeStart)
{
string type = data.Substring(typeStart, typeEnd - typeStart);
// Nutze extrahierten Type als eventType
client.OnSSEMessage(type, data);
}
else
{
// Fallback: Nutze expliziten event Type falls vorhanden
if (!string.IsNullOrEmpty(eventType))
{
client.OnSSEMessage(eventType, data);
}
}
}
}
catch (Exception e)
{
Debug.LogWarning($"SSE parse error: {e.Message}");
}
}
}
protected override void CompleteContent()
{
client.OnSSEError("Stream closed");
}
}
### REST API (Alternative zu SSE)
Falls SSE nicht funktioniert, kann Unity auch pollen:
```bash
# Status abrufen
curl "http://192.168.178.12:8083/status?device_id=Unity1"
🔄 工作流程
1.n8n人工智能代理销售任务
AI Agent: "Erstelle einen Mining Job mit 20 Tasks"
↓
Tool: create_mining_job(num_tasks=20)
↓
Server: Block #5 wird generiert, 20 Tasks erstellt
↓
Tasks landen in Queue
2.Unity设备完成任务(通过SSE!)
Unity1 verbindet zu SSE
↓
Bekommt sofort Task 1 gepusht
↓
Beginnt Mining (nonce 0-1000000)
↓
Während dem Mining bekommt Unity1 Task 2 gepusht
(wenn max_inflight=2)
3.结果提交
Unity1 findet Hash "0000abc..."
↓
POST /result {nonce: 123456, hash: "0000..."}
↓
Server prüft Hash
↓
WINNER! 50 Coins für Unity1
↓
Block #5 zur Blockchain hinzugefügt
↓
Job als "completed" markiert
↓
Alle anderen Tasks cancelled
4.超时并重试
Unity2 bekommt Task 5
↓
120 Sekunden vergehen
↓
Unity2 antwortet nicht
↓
Task 5 wird automatisch neu vergeben
↓
Unity3 bekommt Task 5
🎯 这种架构的优势
✅ 推动而不是投票
- Unity不等待,立即获取任务
- 无
/nextPolling 更多 - 更有效率!
✅ 清洁分离
- 作业管理≠任务分配
- 不同的安全可能
- 独立可扩展
✅ 自动重试
- 超时会自动处理
- 没有丢失的任务
- 就像示例代码一样。
✅ 内存队列
asyncio.Queue对于设备- 快速简单
- 完美用于原型设计
📊 监控
作业服务器运行状况
curl http://192.168.178.12:8082/health
设备服务器运行状况
curl http://192.168.178.12:8083/health
设备状态
curl "http://192.168.178.12:8083/status?device_id=Unity1"
🔐 可选:HMAC签名
对于生产环境,您可以启用 HMAC:
.env创建 :
HMAC_SECRET=your_secret_key_here
- 然后,Unity 必须发送签名:
string sig = ComputeHMAC(assignmentId, secret);
🐛 调试
日志
两个服务器都会详细记录:
📨 MCP: tools/call
🔧 Tool: create_mining_job({'num_tasks': 10})
📋 Job job_abc123: Block #5, 10 tasks, diff=4
🎮 Device connected: Unity1
📤 Unity1: Assignment sent
🎉 POTENTIAL WINNER: Unity1 found hash!
🏆 WINNER CONFIRMED: Unity1 - Block #5 - Reward: 50.0 coins
测试没有Unity
# SSE verbinden (Terminal)
curl -N "http://192.168.178.12:8083/sse?device_id=test1"
# In anderem Terminal: Result senden
curl -X POST http://192.168.178.12:8083/result \
-H "Content-Type: application/json" \
-d '{
"assignment_id": "asg_xxx",
"job_id": "job_xxx",
"device_id": "test1",
"nonce": 0,
"hash": "",
"conf": 1.0
}'
🚀 后续步骤
-
启动服务器:
./start_mining.sh -
n8n MCP工具:
- SSE端点:
http://192.168.178.12:8082/sse - 测试:
create_mining_job
- SSE端点:
-
Unity SSE:
- 复制C#代码
- 连接到 :
http://192.168.178.12:8083/sse?device_id=Unity1
-
AI代理提示:
Erstelle einen Mining Job mit 10 Tasks. Wie ist der Pool Status? Zeige mir das Leaderboard.
📝 数据概述
mining-pool/
├── shared_state.py # Gemeinsamer State (Blockchain, Jobs, Queue)
├── mcp_job_server.py # Job Management für n8n (Port 8082)
├── mcp_device_server.py # Task Distribution für Unity (Port 8083)
├── requirements.txt # Python Dependencies
├── start_mining.sh # Startup Script
└── mining.env # Config (optional)
💡 提示
- 调整难度: 在
shared_state.py→base_difficulty - 更多设备: 使用多个Unity客户端
device_id - 生产: Redis 而不是内存中的多服务器设置
- 监控: 添加 Prometheus Metrics
祝矿业成功! (2)⛏️