feat: 发布智能体版

This commit is contained in:
augushong
2026-03-26 20:22:34 +08:00
parent 7ee9e102a5
commit 8cc08bcb8c
138 changed files with 7964 additions and 660 deletions

View File

@@ -0,0 +1,445 @@
<?php
namespace base\common\command\tools\http;
use app\admin\model\SystemAdmin;
use app\common\console\Command;
use app\common\constants\AdminConstant;
use GuzzleHttp\Client;
use GuzzleHttp\Exception\GuzzleException;
use think\console\Input;
use think\console\input\Argument;
use think\console\input\Option;
use think\console\Output;
use think\facade\Cache;
/**
* tools:http:call 命令基类
* 类似 curl 的 HTTP 调用功能,支持框架特性参数
*/
class ToolsHttpCallBase extends Command
{
/**
* Guzzle HTTP 客户端
* @var Client
*/
protected $httpClient;
/**
* 执行开始时间(毫秒)
* @var float
*/
protected $startTime;
/**
* 配置命令
*/
protected function configure()
{
parent::configure();
$this->setName('tools:http:call')
->setDescription('HTTP 调用工具,类似 curl支持框架特性参数')
->addArgument('url', Argument::OPTIONAL, '请求 URL 或控制器路径(如 /admin/user/index')
->addOption('url', null, Option::VALUE_OPTIONAL, '请求 URL')
->addOption('method', 'm', Option::VALUE_OPTIONAL, 'HTTP 方法 (GET, POST, PUT, DELETE)', 'GET')
->addOption('data', 'd', Option::VALUE_OPTIONAL, '请求数据JSON 或 key=value')
->addOption('body', null, Option::VALUE_OPTIONAL, '请求体(原始数据)')
->addOption('headers', 'H', Option::VALUE_OPTIONAL, '请求头JSON 格式,如 {"Content-Type":"application/json"}')
->addOption('app', null, Option::VALUE_OPTIONAL, '应用名称(框架特性)')
->addOption('controller', null, Option::VALUE_OPTIONAL, '控制器名称(框架特性)')
->addOption('action', null, Option::VALUE_OPTIONAL, '动作名称(框架特性)')
->addOption('page-data', null, Option::VALUE_NONE, '返回页面 assign 数据(追加 get_page_data=1')
->addOption('super-token', null, Option::VALUE_OPTIONAL, '超级 Token框架特性', 'true')
->addOption('user-id', null, Option::VALUE_OPTIONAL, '用户 ID框架特性');
}
/**
* 执行命令
*/
protected function execute(Input $input, Output $output)
{
$this->startTime = microtime(true);
// 检查环境
$this->checkEnvironment($input);
// 初始化 HTTP 客户端
$this->httpClient = new Client([
'base_uri' => $this->getBaseUrl(),
'timeout' => 30,
'verify' => false, // 开发环境跳过 SSL 验证
]);
try {
// 构建请求
$request = $this->buildRequest($input);
// 执行请求
$response = $this->executeRequest($request, $output);
// 输出结果
$this->outputResult($response, $output);
} catch (\Exception $e) {
$executionTime = round((microtime(true) - $this->startTime) * 1000, 2);
$this->outputError($e, $executionTime, $output);
}
}
/**
* 检查运行环境
*/
protected function checkEnvironment(Input $input): void
{
// 检查是否使用框架特性参数
$usingFrameworkParams = !empty($input->getOption('app')) ||
!empty($input->getOption('controller')) ||
!empty($input->getOption('action'));
if ($usingFrameworkParams) {
// 检查应用是否运行
$baseUrl = $this->getBaseUrl();
$isLocalhost = (strpos($baseUrl, 'localhost') !== false || strpos($baseUrl, '127.0.0.1') !== false);
if ($isLocalhost) {
// 尝试检测应用是否运行
$client = new Client([
'timeout' => 20,
'verify' => false,
'http_errors' => false,
]);
try {
$client->get($baseUrl);
} catch (\GuzzleHttp\Exception\ConnectException $e) {
throw new \Exception(
"无法连接到应用服务器 ($baseUrl)。请确保应用正在运行(执行 'php think run'" .
" 或者设置 'app.app_host' 环境变量。"
);
}
}
}
}
/**
* 获取基础 URL
*/
protected function getBaseUrl(): string
{
$appUrl = env('app.app_host', '');
if (empty($appUrl)) {
$appUrl = 'http://127.0.0.1:8000';
}
return rtrim($appUrl, '/');
}
/**
* 构建请求
*/
protected function buildRequest(Input $input): array
{
// 获取 URL优先使用 --url 参数,其次使用位置参数)
$url = $input->getOption('url') ?: $input->getArgument('url');
// 如果使用框架特性参数,构建 URL
if (empty($url)) {
$url = $this->buildUrlFromFrameworkParams($input);
}
if (empty($url)) {
throw new \Exception('请提供 URL 或使用框架特性参数(--app, --controller, --action');
}
if ($input->getOption('page-data') && strpos($url, 'get_page_data=') === false) {
$url .= (strpos($url, '?') === false ? '?' : '&') . 'get_page_data=1';
}
$request = [
'url' => $url,
'method' => strtoupper($input->getOption('method')),
'headers' => [],
'body' => null,
];
// 解析请求头
$headers = $input->getOption('headers');
if (!empty($headers)) {
$request['headers'] = json_decode($headers, true);
if (json_last_error() !== JSON_ERROR_NONE) {
throw new \Exception('请求头格式错误:' . json_last_error_msg());
}
}
// 设置 Accept 头为 JSON
$request['headers']['Accept'] = 'application/json';
// 解析请求数据
$data = $input->getOption('data');
$body = $input->getOption('body');
if (!empty($body)) {
$request['body'] = $body;
} elseif (!empty($data)) {
$request['headers']['Content-Type'] = 'application/json';
$request['body'] = $this->normalizeDataToJson($data);
}
$superToken = $this->resolveSuperToken($input);
if (!is_null($superToken)) {
$userId = $input->getOption('user-id') ?: AdminConstant::SUPER_ADMIN_ID;
$admin = SystemAdmin::find($userId);
if (empty($admin)) {
throw new \Exception('管理员不存在:' . $userId);
}
$adminData = $admin->toArray();
unset($adminData['password']);
$adminData['expire_time'] = time() + 7200;
Cache::store('login')->set($superToken, $adminData, 7200);
$request['headers']['Authorization'] = 'Bearer ' . $superToken;
}
return $request;
}
protected function normalizeDataToJson(string $raw): string
{
$raw = trim($raw);
if ($raw === '') {
return $raw;
}
while (strlen($raw) >= 2) {
$first = $raw[0];
$last = $raw[strlen($raw) - 1];
if (!($first === $last && ($first === '"' || $first === "'"))) {
break;
}
$raw = substr($raw, 1, -1);
$raw = trim($raw);
}
json_decode($raw, true);
if (json_last_error() === JSON_ERROR_NONE) {
return $raw;
}
if (strpos($raw, '=') === false) {
throw new \Exception('数据格式错误:必须是 JSON 字符串,或 key=value 格式');
}
$data = $this->parseKeyValueData($raw);
return json_encode($data, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
}
protected function parseKeyValueData(string $raw): array
{
$parts = preg_split('/[&,\s]+/', trim($raw)) ?: [];
$result = [];
foreach ($parts as $part) {
$part = trim((string) $part);
if ($part === '') {
continue;
}
$pos = strpos($part, '=');
if ($pos === false) {
throw new \Exception('数据格式错误key=value 格式中存在无效片段:' . $part);
}
$key = urldecode(trim(substr($part, 0, $pos)));
$value = urldecode(substr($part, $pos + 1));
if ($key === '') {
throw new \Exception('数据格式错误key 不能为空');
}
$result[$key] = $this->castStringValue($value);
}
return $result;
}
protected function castStringValue(string $value)
{
$value = trim($value);
$lower = strtolower($value);
if ($lower === 'true') {
return true;
}
if ($lower === 'false') {
return false;
}
if ($lower === 'null') {
return null;
}
if ($value !== '' && ($value[0] === '{' || $value[0] === '[')) {
$decoded = json_decode($value, true);
if (json_last_error() === JSON_ERROR_NONE) {
return $decoded;
}
}
if (preg_match('/^-?\d+$/', $value)) {
return (int) $value;
}
if (is_numeric($value)) {
return (float) $value;
}
return $value;
}
/**
* 从框架特性参数构建 URL
*/
protected function buildUrlFromFrameworkParams(Input $input): string
{
$app = $input->getOption('app');
$controller = $input->getOption('controller');
$action = $input->getOption('action');
if (empty($app) || empty($controller) || empty($action)) {
return '';
}
// 构建路由路径
$path = '/' . $app . '/' . $controller . '/' . $action;
return $path;
}
protected function resolveSuperToken(Input $input): ?string
{
$raw = $input->getOption('super-token');
$value = is_bool($raw) ? ($raw ? 'true' : 'false') : trim((string) $raw);
$lower = strtolower($value);
if (in_array($lower, ['false', '0', 'off', 'no'], true)) {
return null;
}
if ($value === '' || in_array($lower, ['true', '1', 'on', 'yes'], true)) {
return $this->generateToken();
}
return $value;
}
protected function generateToken(): string
{
return bin2hex(random_bytes(16));
}
/**
* 执行 HTTP 请求
*/
protected function executeRequest(array $request, Output $output): array
{
$guzzleOptions = [
'headers' => $request['headers'],
];
// 如果有请求体,添加到选项中
if ($request['body'] !== null) {
$guzzleOptions['body'] = $request['body'];
}
// 执行请求
$guzzleResponse = $this->httpClient->request($request['method'], $request['url'], $guzzleOptions);
// 解析响应
$response = [
'status' => $guzzleResponse->getStatusCode(),
'headers' => $guzzleResponse->getHeaders(),
'data' => null,
];
// 解析响应体
$body = $guzzleResponse->getBody()->getContents();
$jsonData = json_decode($body, true);
if (json_last_error() === JSON_ERROR_NONE) {
$response['data'] = $jsonData;
} else {
$response['data'] = $body;
}
return $response;
}
/**
* 输出结果
*/
protected function outputResult(array $response, Output $output): void
{
$executionTime = round((microtime(true) - $this->startTime) * 1000, 2);
// 构建输出数据
$outputData = [
'success' => $response['status'] >= 200 && $response['status'] < 300,
'response' => [
'status' => $response['status'],
'data' => $response['data'],
'headers' => $response['headers'],
],
'execution_time' => $executionTime,
'exception' => null,
];
// 文本模式:输出可读格式
$this->outputTextResult($outputData, $output);
}
/**
* 输出文本格式结果
*/
protected function outputTextResult(array $data, Output $output): void
{
$response = $data['response'];
$output->newLine();
$output->info('HTTP 状态: ' . $response['status']);
$output->info('执行时间: ' . $data['execution_time'] . 'ms');
$output->newLine();
// 输出响应头
$output->comment('响应头:');
foreach ($response['headers'] as $key => $values) {
$output->writeln(' ' . $key . ': ' . implode(', ', $values));
}
$output->newLine();
// 输出响应数据
$output->comment('响应数据:');
if (is_string($response['data'])) {
$output->writeln(' ' . $response['data']);
} else {
$output->writeln(' ' . json_encode($response['data'], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT));
}
$output->newLine();
}
/**
* 输出错误信息
*/
protected function outputError(\Exception $e, float $executionTime, Output $output): void
{
$output->error('错误: ' . $e->getMessage());
$output->writeln('执行时间: ' . $executionTime . 'ms');
if (env('app_debug', false)) {
$output->writeln('文件: ' . $e->getFile() . ':' . $e->getLine());
// 如果是 Guzzle 连接错误,提供提示
if (strpos($e->getMessage(), 'Failed to connect') !== false) {
$baseUrl = $this->getBaseUrl();
$output->comment('提示: 请确保应用正在运行(执行 "php think run")或设置 "app.app_host" 环境变量');
}
}
}
}