connection/status 不再因 workphone.quwanzhi.com 502 报 500;补 20260530 部署进度。 Co-authored-by: Cursor <cursoragent@cursor.com>
467 lines
15 KiB
PHP
467 lines
15 KiB
PHP
<?php
|
||
|
||
namespace app\cunkebao\controller\workphone;
|
||
|
||
use app\common\util\WorkPhoneSDK;
|
||
use app\cunkebao\controller\BaseController;
|
||
use library\ResponseHelper;
|
||
|
||
/**
|
||
* 工作手机SDK状态控制器
|
||
*
|
||
* 聚合 SDK 服务端的健康状态、连接设备列表、连接协议信息,
|
||
* 供存客宝前端「工作手机管理」页面使用。
|
||
*/
|
||
class GetWorkPhoneStatusController extends BaseController
|
||
{
|
||
/**
|
||
* 获取 SDK 总览(健康 + 设备列表 + 连接状态)
|
||
*/
|
||
public function index()
|
||
{
|
||
try {
|
||
$sdk = WorkPhoneSDK::getInstance();
|
||
|
||
$overview = $this->fetchWorkbenchOverview($sdk);
|
||
if ($overview) {
|
||
return ResponseHelper::success($overview);
|
||
}
|
||
|
||
$health = $sdk->healthCheck();
|
||
$devices = $sdk->getDevices();
|
||
$connStatus = $this->fetchConnectionStatus($sdk);
|
||
|
||
$deviceList = $devices['data'] ?? [];
|
||
if (!is_array($deviceList)) {
|
||
$deviceList = [];
|
||
}
|
||
$onlineCount = 0;
|
||
$offlineCount = 0;
|
||
|
||
foreach ($deviceList as &$d) {
|
||
$status = $d['status'] ?? 'unknown';
|
||
if ($status === 'online') {
|
||
$onlineCount++;
|
||
} else {
|
||
$offlineCount++;
|
||
}
|
||
}
|
||
unset($d);
|
||
|
||
return ResponseHelper::success([
|
||
'sdk_online' => ($health['status'] ?? '') === 'healthy',
|
||
'sdk_version' => $health['version'] ?? '-',
|
||
'ws_online' => $health['devices_online'] ?? 0,
|
||
'adb_count' => $health['adb_devices'] ?? 0,
|
||
'device_total' => count($deviceList),
|
||
'device_online' => $onlineCount,
|
||
'device_offline' => $offlineCount,
|
||
'devices' => $deviceList,
|
||
'connection' => $connStatus,
|
||
'summary' => [
|
||
'sdk_online' => ($health['status'] ?? '') === 'healthy',
|
||
'sdk_version' => $health['version'] ?? '-',
|
||
'ws_online' => $health['devices_online'] ?? 0,
|
||
'adb_count' => $health['adb_devices'] ?? 0,
|
||
'device_total' => count($deviceList),
|
||
'device_online' => $onlineCount,
|
||
'device_offline' => $offlineCount,
|
||
],
|
||
'docs' => [],
|
||
'architecture_assets' => [],
|
||
]);
|
||
} catch (\Exception $e) {
|
||
return ResponseHelper::success([
|
||
'sdk_online' => false,
|
||
'sdk_version' => '-',
|
||
'ws_online' => 0,
|
||
'adb_count' => 0,
|
||
'device_total' => 0,
|
||
'device_online' => 0,
|
||
'device_offline' => 0,
|
||
'devices' => [],
|
||
'connection' => null,
|
||
'summary' => null,
|
||
'docs' => [],
|
||
'architecture_assets' => [],
|
||
'error' => $e->getMessage(),
|
||
]);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 获取单台设备详情
|
||
*/
|
||
public function detail()
|
||
{
|
||
try {
|
||
$deviceId = $this->request->param('device_id');
|
||
if (!$deviceId) {
|
||
return ResponseHelper::error('缺少 device_id');
|
||
}
|
||
|
||
$sdk = WorkPhoneSDK::getInstance();
|
||
$result = $sdk->getDevice($deviceId);
|
||
|
||
return ResponseHelper::success($result['data'] ?? $result);
|
||
} catch (\Exception $e) {
|
||
return ResponseHelper::error($e->getMessage());
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 向设备发送截屏指令
|
||
*/
|
||
public function screenshot()
|
||
{
|
||
try {
|
||
$deviceId = $this->request->param('device_id');
|
||
if (!$deviceId) {
|
||
return ResponseHelper::error('缺少 device_id');
|
||
}
|
||
|
||
$sdk = WorkPhoneSDK::getInstance();
|
||
$result = $sdk->screenshot($deviceId);
|
||
|
||
return ResponseHelper::success($result['data'] ?? $result);
|
||
} catch (\Exception $e) {
|
||
return ResponseHelper::error($e->getMessage());
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 获取设备 AI Brain 状态
|
||
*/
|
||
public function aiStatus()
|
||
{
|
||
try {
|
||
$deviceId = $this->request->param('device_id');
|
||
if (!$deviceId) {
|
||
return ResponseHelper::error('缺少 device_id');
|
||
}
|
||
|
||
$sdk = WorkPhoneSDK::getInstance();
|
||
$result = $sdk->getAIBrainStatus($deviceId);
|
||
|
||
return ResponseHelper::success($result['data'] ?? $result);
|
||
} catch (\Exception $e) {
|
||
return ResponseHelper::error($e->getMessage());
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 推送 AI 任务到设备队列
|
||
*/
|
||
public function aiTask()
|
||
{
|
||
try {
|
||
$deviceId = $this->request->param('device_id');
|
||
$instruction = $this->request->param('instruction', '');
|
||
$priority = (int) $this->request->param('priority', 5);
|
||
|
||
if (!$deviceId) {
|
||
return ResponseHelper::error('缺少 device_id');
|
||
}
|
||
if ($instruction === '') {
|
||
return ResponseHelper::error('缺少 instruction');
|
||
}
|
||
|
||
$sdk = WorkPhoneSDK::getInstance();
|
||
$result = $sdk->pushAITask($deviceId, $instruction, $priority);
|
||
|
||
return ResponseHelper::success($result['data'] ?? $result);
|
||
} catch (\Exception $e) {
|
||
return ResponseHelper::error($e->getMessage());
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 推送 AI 常驻指令
|
||
*/
|
||
public function aiStandingOrder()
|
||
{
|
||
try {
|
||
$deviceId = $this->request->param('device_id');
|
||
$order = $this->request->param('order', '');
|
||
|
||
if (!$deviceId) {
|
||
return ResponseHelper::error('缺少 device_id');
|
||
}
|
||
if ($order === '') {
|
||
return ResponseHelper::error('缺少 order');
|
||
}
|
||
|
||
$sdk = WorkPhoneSDK::getInstance();
|
||
$result = $sdk->pushAIStandingOrder($deviceId, $order);
|
||
|
||
return ResponseHelper::success($result['data'] ?? $result);
|
||
} catch (\Exception $e) {
|
||
return ResponseHelper::error($e->getMessage());
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 同步执行 AI 自然语言任务
|
||
*/
|
||
public function aiExecute()
|
||
{
|
||
try {
|
||
$deviceId = $this->request->param('device_id');
|
||
$task = $this->request->param('task', '');
|
||
$timeout = (int) $this->request->param('timeout', 60);
|
||
|
||
if (!$deviceId) {
|
||
return ResponseHelper::error('缺少 device_id');
|
||
}
|
||
if ($task === '') {
|
||
return ResponseHelper::error('缺少 task');
|
||
}
|
||
|
||
$sdk = WorkPhoneSDK::getInstance();
|
||
$result = $sdk->executeAITask($deviceId, $task, $timeout);
|
||
|
||
return ResponseHelper::success($result['data'] ?? $result);
|
||
} catch (\Exception $e) {
|
||
return ResponseHelper::error($e->getMessage());
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 通过 SDK 拉取连接协议状态
|
||
*/
|
||
private function fetchConnectionStatus(WorkPhoneSDK $sdk): ?array
|
||
{
|
||
try {
|
||
$json = $this->fetchJson($sdk, '/api/v3/connection/status');
|
||
return $json['data'] ?? null;
|
||
} catch (\Exception $e) {
|
||
return null;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 工作台统一概览 DTO
|
||
*/
|
||
private function fetchWorkbenchOverview(WorkPhoneSDK $sdk): ?array
|
||
{
|
||
try {
|
||
$json = $this->fetchJson($sdk, '/api/v3/workbench/overview');
|
||
$data = $json['data'] ?? null;
|
||
if (!$data) {
|
||
return null;
|
||
}
|
||
return $this->normalizeWorkbenchPayload($sdk, $data);
|
||
} catch (\Exception $e) {
|
||
return null;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 拉取 SDK JSON
|
||
*/
|
||
private function fetchJson(WorkPhoneSDK $sdk, string $path): ?array
|
||
{
|
||
$baseUrl = $this->getBaseUrl($sdk);
|
||
$url = rtrim($baseUrl, '/') . $path;
|
||
|
||
$ch = curl_init($url);
|
||
curl_setopt_array($ch, [
|
||
CURLOPT_RETURNTRANSFER => true,
|
||
CURLOPT_TIMEOUT => 5,
|
||
CURLOPT_HTTPHEADER => ['Accept: application/json'],
|
||
]);
|
||
$raw = curl_exec($ch);
|
||
$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||
curl_close($ch);
|
||
|
||
if ($code === 200 && $raw) {
|
||
return json_decode($raw, true);
|
||
}
|
||
return null;
|
||
}
|
||
|
||
/**
|
||
* 统一把工作手机 SDK 返回的相对链接补全为绝对链接
|
||
*/
|
||
private function normalizeWorkbenchPayload(WorkPhoneSDK $sdk, array $data): array
|
||
{
|
||
$baseUrl = rtrim($this->getBaseUrl($sdk), '/');
|
||
|
||
if (!empty($data['architecture_assets']) && is_array($data['architecture_assets'])) {
|
||
foreach ($data['architecture_assets'] as &$asset) {
|
||
if (!empty($asset['url']) && strpos($asset['url'], 'http') !== 0) {
|
||
$asset['url'] = $baseUrl . $asset['url'];
|
||
}
|
||
}
|
||
unset($asset);
|
||
}
|
||
|
||
if (!empty($data['docs']) && is_array($data['docs'])) {
|
||
foreach ($data['docs'] as &$doc) {
|
||
if (!empty($doc['raw_url']) && strpos($doc['raw_url'], 'http') !== 0) {
|
||
$doc['raw_url'] = $baseUrl . $doc['raw_url'];
|
||
}
|
||
}
|
||
unset($doc);
|
||
}
|
||
|
||
$data['sdk_base_url'] = $baseUrl;
|
||
return $data;
|
||
}
|
||
|
||
/**
|
||
* 反射获取 SDK baseUrl(SDK 构造函数从 env/config 读取)
|
||
*/
|
||
private function getBaseUrl(WorkPhoneSDK $sdk): string
|
||
{
|
||
$ref = new \ReflectionClass($sdk);
|
||
$prop = $ref->getProperty('baseUrl');
|
||
$prop->setAccessible(true);
|
||
return $prop->getValue($sdk);
|
||
}
|
||
|
||
/**
|
||
* GET /v1/workphone/hook/events — 代理 SDK Hook 事件列表
|
||
*/
|
||
public function hookEvents()
|
||
{
|
||
try {
|
||
$deviceId = $this->request->param('device_id', null);
|
||
$eventType = $this->request->param('event_type', null);
|
||
$limit = (int) $this->request->param('limit/d', 50);
|
||
$sdk = WorkPhoneSDK::getInstance();
|
||
$resp = $sdk->listHookEvents($deviceId, $eventType, $limit);
|
||
if (($resp['code'] ?? 500) !== 200) {
|
||
return ResponseHelper::error($resp['message'] ?? '拉取失败', (int) ($resp['code'] ?? 500));
|
||
}
|
||
return ResponseHelper::success($resp['data'] ?? []);
|
||
} catch (\Exception $e) {
|
||
return ResponseHelper::error($e->getMessage(), 500);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* POST /v1/workphone/hook/events/sync — 拉取并处理 Hook 事件
|
||
*/
|
||
public function hookEventsSync()
|
||
{
|
||
try {
|
||
$deviceId = $this->request->param('device_id', null);
|
||
$eventType = $this->request->param('event_type', null);
|
||
$limit = (int) $this->request->param('limit/d', 50);
|
||
$result = \app\common\util\WorkPhoneEventBridge::syncFromSdk($deviceId, $eventType, $limit);
|
||
if (($result['code'] ?? 500) !== 200) {
|
||
return ResponseHelper::error($result['message'] ?? '同步失败', (int) ($result['code'] ?? 500));
|
||
}
|
||
return ResponseHelper::success($result['data'] ?? []);
|
||
} catch (\Exception $e) {
|
||
return ResponseHelper::error($e->getMessage(), 500);
|
||
}
|
||
}
|
||
|
||
/** GET /v1/workphone/connection/status */
|
||
public function connectionStatus()
|
||
{
|
||
try {
|
||
$resp = WorkPhoneSDK::getInstance()->getConnectionStatus();
|
||
if (($resp['code'] ?? 500) !== 200) {
|
||
return ResponseHelper::success([
|
||
'connected' => false,
|
||
'websocket' => false,
|
||
'frida' => false,
|
||
'sdk_online' => false,
|
||
'error' => $resp['message'] ?? 'SDK 不可用',
|
||
]);
|
||
}
|
||
return ResponseHelper::success($resp['data'] ?? $resp);
|
||
} catch (\Exception $e) {
|
||
return ResponseHelper::success([
|
||
'connected' => false,
|
||
'sdk_online' => false,
|
||
'error' => $e->getMessage(),
|
||
]);
|
||
}
|
||
}
|
||
|
||
/** GET /v1/workphone/hook/probe?device_id= */
|
||
public function hookProbe()
|
||
{
|
||
try {
|
||
$deviceId = (string) $this->request->param('device_id', '');
|
||
if ($deviceId === '') {
|
||
return ResponseHelper::error('device_id 不能为空', 400);
|
||
}
|
||
$resp = WorkPhoneSDK::getInstance()->hookProbe($deviceId);
|
||
return ResponseHelper::success($resp);
|
||
} catch (\Exception $e) {
|
||
return ResponseHelper::error($e->getMessage(), 500);
|
||
}
|
||
}
|
||
|
||
/** POST /v1/workphone/message/send */
|
||
public function messageSend()
|
||
{
|
||
try {
|
||
$deviceId = (string) $this->request->param('device_id', '');
|
||
$platform = (string) $this->request->param('platform', 'wechat');
|
||
$toId = (string) $this->request->param('to_id', '');
|
||
$content = (string) $this->request->param('content', '');
|
||
$msgType = (string) $this->request->param('msg_type', 'text');
|
||
$channel = $this->request->param('channel', null);
|
||
|
||
if ($deviceId === '' || $toId === '' || $content === '') {
|
||
return ResponseHelper::error('device_id / to_id / content 不能为空', 400);
|
||
}
|
||
|
||
$resp = WorkPhoneSDK::getInstance()->sendMessage(
|
||
$deviceId,
|
||
$platform,
|
||
$toId,
|
||
$content,
|
||
$msgType,
|
||
$channel !== null ? (string) $channel : null
|
||
);
|
||
if (empty($resp['success']) && ($resp['code'] ?? 200) !== 200) {
|
||
return ResponseHelper::error($resp['error'] ?? $resp['message'] ?? '发送失败', 502);
|
||
}
|
||
return ResponseHelper::success($resp);
|
||
} catch (\Exception $e) {
|
||
return ResponseHelper::error($e->getMessage(), 500);
|
||
}
|
||
}
|
||
|
||
/** POST /v1/workphone/hook/execute */
|
||
public function hookExecute()
|
||
{
|
||
try {
|
||
$deviceId = (string) $this->request->param('device_id', '');
|
||
$platform = (string) $this->request->param('platform', 'wechat');
|
||
$action = (string) $this->request->param('action', '');
|
||
$params = $this->request->param('params/a', []);
|
||
if (!is_array($params)) {
|
||
$params = [];
|
||
}
|
||
// SDK 要求 params 为 object;空数组在 JSON 会变成 [] 导致校验失败
|
||
if ($params === []) {
|
||
$params = new \stdClass();
|
||
}
|
||
$hookOnly = (bool) $this->request->param('hook_only/d', 1);
|
||
|
||
if ($deviceId === '' || $action === '') {
|
||
return ResponseHelper::error('device_id / action 不能为空', 400);
|
||
}
|
||
|
||
$resp = WorkPhoneSDK::getInstance()->hookExecute(
|
||
$deviceId,
|
||
$platform,
|
||
$action,
|
||
$params,
|
||
$hookOnly
|
||
);
|
||
return ResponseHelper::success($resp);
|
||
} catch (\Exception $e) {
|
||
return ResponseHelper::error($e->getMessage(), 500);
|
||
}
|
||
}
|
||
}
|