4020 lines
138 KiB
PHP
4020 lines
138 KiB
PHP
<?php
|
||
namespace app\mcp;
|
||
|
||
use PhpMcp\Server\Server;
|
||
use PhpMcp\Server\ServerBuilder;
|
||
use PhpMcp\Server\Transports\StdioServerTransport;
|
||
use PhpMcp\Server\Defaults\BasicContainer;
|
||
use PhpMcp\Schema\Tool;
|
||
use PhpMcp\Schema\Resource;
|
||
use PhpMcp\Schema\ToolAnnotations;
|
||
use PhpMcp\Schema\Annotations;
|
||
use think\facade\Db;
|
||
use support\Log;
|
||
|
||
/**
|
||
* MCP(Model Context Protocol)服务类
|
||
* 提供与AI模型交互的上下文协议服务
|
||
*/
|
||
class McpService
|
||
{
|
||
/**
|
||
* MCP服务器实例
|
||
* @var Server|null
|
||
*/
|
||
protected $server = null;
|
||
|
||
/**
|
||
* 日志记录器
|
||
*/
|
||
protected $logger;
|
||
|
||
/**
|
||
* 超时配置(毫秒)
|
||
* @var int
|
||
*/
|
||
protected $timeout = 600000;
|
||
|
||
/**
|
||
* 连接超时配置(毫秒)
|
||
* @var int
|
||
*/
|
||
protected $connectTimeout = 30000;
|
||
|
||
/**
|
||
* 读取超时配置(毫秒)
|
||
* @var int
|
||
*/
|
||
protected $readTimeout = 30000;
|
||
|
||
/**
|
||
* 重试次数
|
||
* @var int
|
||
*/
|
||
protected $retryAttempts = 3;
|
||
|
||
/**
|
||
* 重试延迟(毫秒)
|
||
* @var int
|
||
*/
|
||
protected $retryDelay = 1000;
|
||
|
||
/**
|
||
* 调试模式
|
||
* @var bool
|
||
*/
|
||
protected $debug = false;
|
||
|
||
/**
|
||
* 服务名称
|
||
* @var string
|
||
*/
|
||
protected $name = 'mcp';
|
||
|
||
/**
|
||
* 服务版本
|
||
* @var string
|
||
*/
|
||
protected $version = '1.0.0';
|
||
|
||
/**
|
||
* 内存限制
|
||
* @var string
|
||
*/
|
||
protected $memoryLimit;
|
||
|
||
/**
|
||
* 缓冲区大小
|
||
* @var int
|
||
*/
|
||
protected $bufferSize;
|
||
|
||
/**
|
||
* 心跳机制启用
|
||
* @var bool
|
||
*/
|
||
protected $heartbeatEnabled;
|
||
|
||
/**
|
||
* 心跳间隔(秒)
|
||
* @var int
|
||
*/
|
||
protected $heartbeatInterval;
|
||
|
||
/**
|
||
* 构造函数
|
||
*/
|
||
public function __construct()
|
||
{
|
||
$this->initialize();
|
||
}
|
||
|
||
/**
|
||
* 初始化MCP服务
|
||
*/
|
||
protected function initialize()
|
||
{
|
||
// 确保 logger 不为 null
|
||
try {
|
||
$this->logger = Log::channel('mcp');
|
||
if (!$this->logger) {
|
||
// 如果 mcp 通道不存在,使用默认通道
|
||
$this->logger = Log::channel('default');
|
||
}
|
||
} catch (\Throwable $e) {
|
||
// 如果所有通道都失败,创建一个简单的 logger
|
||
$this->logger = Log::channel('default');
|
||
}
|
||
|
||
// 读取MCP配置文件
|
||
$this->loadMcpConfig();
|
||
|
||
return $this;
|
||
}
|
||
|
||
/**
|
||
* 加载MCP配置
|
||
*/
|
||
protected function loadMcpConfig()
|
||
{
|
||
try {
|
||
// 对于长时间运行的服务器,设置为无限制
|
||
ini_set('max_execution_time', 0);
|
||
set_time_limit(0);
|
||
|
||
// 忽略用户中断,保持服务器运行
|
||
ignore_user_abort(true);
|
||
|
||
$mcpConfig = config('mcp', []);
|
||
|
||
// 设置超时配置
|
||
if (isset($mcpConfig['timeout']) && $mcpConfig['timeout'] > 0) {
|
||
$this->timeout = $mcpConfig['timeout'];
|
||
}
|
||
|
||
if (isset($mcpConfig['connect_timeout']) && $mcpConfig['connect_timeout'] > 0) {
|
||
$this->connectTimeout = $mcpConfig['connect_timeout'];
|
||
}
|
||
|
||
if (isset($mcpConfig['read_timeout']) && $mcpConfig['read_timeout'] > 0) {
|
||
$this->readTimeout = $mcpConfig['read_timeout'];
|
||
}
|
||
|
||
// 设置重试配置
|
||
if (isset($mcpConfig['retry_attempts']) && $mcpConfig['retry_attempts'] > 0) {
|
||
$this->retryAttempts = $mcpConfig['retry_attempts'];
|
||
}
|
||
|
||
if (isset($mcpConfig['retry_delay']) && $mcpConfig['retry_delay'] > 0) {
|
||
$this->retryDelay = $mcpConfig['retry_delay'];
|
||
}
|
||
|
||
// 设置调试模式
|
||
if (isset($mcpConfig['debug'])) {
|
||
$this->debug = $mcpConfig['debug'];
|
||
}
|
||
|
||
// 设置内存限制
|
||
if (isset($mcpConfig['memory_limit'])) {
|
||
ini_set('memory_limit', $mcpConfig['memory_limit']);
|
||
}
|
||
|
||
// 设置缓冲区大小
|
||
if (isset($mcpConfig['buffer_size'])) {
|
||
$this->bufferSize = $mcpConfig['buffer_size'];
|
||
}
|
||
|
||
// 设置心跳配置
|
||
if (isset($mcpConfig['heartbeat_enabled'])) {
|
||
$this->heartbeatEnabled = $mcpConfig['heartbeat_enabled'];
|
||
}
|
||
|
||
if (isset($mcpConfig['heartbeat_interval'])) {
|
||
$this->heartbeatInterval = $mcpConfig['heartbeat_interval'];
|
||
}
|
||
|
||
Log::channel('mcp')->info('MCP配置加载成功', [
|
||
'timeout' => $this->timeout,
|
||
'connect_timeout' => $this->connectTimeout,
|
||
'read_timeout' => $this->readTimeout,
|
||
'retry_attempts' => $this->retryAttempts,
|
||
'retry_delay' => $this->retryDelay,
|
||
'heartbeat_enabled' => $this->heartbeatEnabled ?? false,
|
||
'heartbeat_interval' => $this->heartbeatInterval ?? 30
|
||
]);
|
||
|
||
} catch (\Exception $e) {
|
||
Log::warning('MCP配置加载失败,使用默认配置: ' . $e->getMessage());
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 启动心跳机制
|
||
*/
|
||
protected function startHeartbeat()
|
||
{
|
||
if (!$this->heartbeatEnabled) {
|
||
return;
|
||
}
|
||
|
||
// 在后台启动心跳线程
|
||
if (function_exists('pcntl_fork')) {
|
||
$pid = pcntl_fork();
|
||
if ($pid == 0) {
|
||
// 子进程执行心跳
|
||
$this->heartbeatLoop();
|
||
exit(0);
|
||
}
|
||
} else {
|
||
// Windows系统使用定时器
|
||
$this->scheduleHeartbeat();
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 心跳循环
|
||
*/
|
||
protected function heartbeatLoop()
|
||
{
|
||
while (true) {
|
||
try {
|
||
// 发送心跳信号
|
||
$this->sendHeartbeat();
|
||
|
||
// 等待下次心跳
|
||
sleep($this->heartbeatInterval);
|
||
|
||
} catch (\Exception $e) {
|
||
Log::channel('mcp')->error('心跳发送失败: ' . $e->getMessage());
|
||
sleep(5); // 失败后等待5秒再重试
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 发送心跳信号
|
||
*/
|
||
protected function sendHeartbeat()
|
||
{
|
||
// 记录心跳日志
|
||
if ($this->debug) {
|
||
Log::debug('发送心跳信号', [
|
||
'timestamp' => time(),
|
||
'memory_usage' => memory_get_usage(true)
|
||
]);
|
||
}
|
||
|
||
// 这里可以添加实际的心跳逻辑
|
||
// 比如向客户端发送ping消息
|
||
}
|
||
|
||
/**
|
||
* 调度心跳(Windows系统)
|
||
*/
|
||
protected function scheduleHeartbeat()
|
||
{
|
||
// Windows系统下的心跳调度
|
||
if (function_exists('register_tick_function')) {
|
||
register_tick_function([$this, 'sendHeartbeat']);
|
||
declare(ticks=1);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 带重试机制的操作执行
|
||
*/
|
||
protected function executeWithRetry(callable $operation, string $operationName = 'operation')
|
||
{
|
||
$attempts = 0;
|
||
$lastException = null;
|
||
|
||
while ($attempts < $this->retryAttempts) {
|
||
try {
|
||
$attempts++;
|
||
Log::channel('mcp')->info("执行{$operationName},第{$attempts}次尝试");
|
||
|
||
$result = $operation();
|
||
|
||
if ($attempts > 1) {
|
||
Log::channel('mcp')->info("{$operationName}在第{$attempts}次尝试后成功");
|
||
}
|
||
|
||
return $result;
|
||
|
||
} catch (\Exception $e) {
|
||
$lastException = $e;
|
||
Log::warning("{$operationName}第{$attempts}次尝试失败: " . $e->getMessage());
|
||
|
||
if ($attempts < $this->retryAttempts) {
|
||
$delay = $this->retryDelay * pow(1.5, $attempts - 1); // 指数退避
|
||
Log::channel('mcp')->info("等待{$delay}ms后重试");
|
||
usleep($delay * 1000);
|
||
}
|
||
}
|
||
}
|
||
|
||
Log::channel('mcp')->error("{$operationName}在{$this->retryAttempts}次尝试后仍然失败");
|
||
throw $lastException;
|
||
}
|
||
|
||
/**
|
||
* 构建MCP服务器
|
||
*/
|
||
protected function buildServer()
|
||
{
|
||
if ($this->server !== null) {
|
||
return $this->server;
|
||
}
|
||
|
||
// 创建容器并注册服务实例
|
||
$container = new BasicContainer();
|
||
|
||
// 确保 logger 有效后再注册
|
||
if ($this->logger) {
|
||
$container->set(\Psr\Log\LoggerInterface::class, $this->logger);
|
||
}
|
||
$container->set(self::class, $this);
|
||
|
||
$builder = Server::make()
|
||
->withServerInfo($this->name, $this->version);
|
||
// 只在 logger 有效时设置
|
||
if ($this->logger) {
|
||
$builder->withLogger($this->logger);
|
||
}
|
||
|
||
$this->server = $builder->withContainer($container)
|
||
->withTool([self::class, 'handleDbQuery'], 'db-query', '执行数据库查询操作(仅支持SELECT语句)')
|
||
->withTool([self::class, 'handleSysConfig'], 'sys-config', '获取系统配置信息')
|
||
->withTool([self::class, 'handleWriteLog'], 'write-log', '写入系统日志')
|
||
->withTool([self::class, 'handleFileOperation'], 'file-operation', '文件读写操作')
|
||
->withTool([self::class, 'handleUserManagement'], 'user-management', '用户管理相关操作')
|
||
->withTool([self::class, 'handleSystemInfo'], 'system-info', '获取系统运行信息')
|
||
->withTool([self::class, 'handleCreateController'], 'controller', '生成webman控制器文件')
|
||
->withTool([self::class, 'handleCreateModel'], 'model', '生成webman模型文件')
|
||
->withTool([self::class, 'handleCreateView'], 'view', '生成webman视图文件')
|
||
->withTool([self::class, 'handleCreateJs'], 'js', '生成webman JS文件')
|
||
->withTool([self::class, 'handleCreateApi'], 'api', '生成webman API接口文件')
|
||
->withTool([self::class, 'handleCurd'], 'curd', '生成webman CURD模块')
|
||
->withTool([self::class, 'handleAddon'], 'addon', '生成webman 插件模块')
|
||
->withTool([self::class, 'handleMenu'], 'menu', '生成webman 菜单模块')
|
||
->withTool([self::class, 'handleCreateTable'], 'table', '创建数据库表格, 支持字段信息、类型、注释等')
|
||
->withTool([self::class, 'handleThinkCommand'], 'think-command', '执行webman框架命令')
|
||
->withTool([self::class, 'handleMcpCommand'], 'mcp-command', '执行MCP专用命令')
|
||
->withTool([self::class, 'handleWebmanCommand'], 'webman-command', '执行webman框架命令')
|
||
->withPrompt([self::class, 'handleWithPrompt'], 'with-prompt', '通过自然语言描述生成数据库表、控制器、模型等')
|
||
->withResource([self::class, 'handleConfigResource'], 'config://system', 'config-system', '系统配置信息资源', 'application/json')
|
||
->withResource([self::class, 'handleSchemaResource'], 'schema://database', 'schema-database', '数据库表结构信息资源', 'application/json')
|
||
->build();
|
||
return $this->server;
|
||
}
|
||
|
||
|
||
|
||
/**
|
||
* 处理数据库查询
|
||
* @param string $query SQL查询语句
|
||
* @param array $params 查询参数
|
||
* @return array
|
||
*/
|
||
public function handleDbQuery(string $query, array $params = []): array
|
||
{
|
||
try {
|
||
if (empty($query)) {
|
||
throw new \Exception('SQL查询语句不能为空');
|
||
}
|
||
|
||
$trimmedQuery = trim($query);
|
||
|
||
// 安全检查:允许SELECT查询和表结构查看语句
|
||
if (!preg_match('/^\s*(select|show|describe|desc)\s+/i', $trimmedQuery)) {
|
||
throw new \Exception('出于安全考虑,只允许执行SELECT、SHOW、DESCRIBE等查询语句');
|
||
}
|
||
|
||
$result = Db::query($query, $params);
|
||
|
||
return [
|
||
'success' => true,
|
||
'data' => $result,
|
||
'count' => count($result),
|
||
'message' => '查询执行成功'
|
||
];
|
||
|
||
} catch (\Exception $e) {
|
||
Log::channel('mcp')->error('MCP数据库查询错误: ' . $e->getMessage());
|
||
return [
|
||
'success' => false,
|
||
'error' => $e->getMessage()
|
||
];
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 处理配置获取
|
||
* @param string $key 配置键名(可选)
|
||
* @return array
|
||
*/
|
||
public function handleSysConfig(string $key = ''): array
|
||
{
|
||
try {
|
||
if (empty($key)) {
|
||
// 返回常用配置的概览
|
||
return [
|
||
'app' => [
|
||
'debug' => config('app.debug', false),
|
||
'default_timezone' => config('app.default_timezone', 'Asia/Shanghai'),
|
||
'default_lang' => config('app.default_lang', 'zh-cn'),
|
||
],
|
||
'database' => [
|
||
'type' => config('thinkorm.connections.mysql.type', 'mysql'),
|
||
'hostname' => config('thinkorm.connections.mysql.hostname', '127.0.0.1'),
|
||
'database' => config('thinkorm.connections.mysql.database', ''),
|
||
],
|
||
'cache' => [
|
||
'default' => config('plugin.bilulanlv.think-cache.app.default', 'redis'),
|
||
'stores' => array_keys(config('cache.stores', [])),
|
||
]
|
||
];
|
||
} else {
|
||
return ['value' => config($key), 'key' => $key];
|
||
}
|
||
|
||
} catch (\Exception $e) {
|
||
Log::channel('mcp')->error('MCP配置获取错误: ' . $e->getMessage());
|
||
throw $e;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 处理日志写入
|
||
* @param string $message 日志消息
|
||
* @param string $level 日志级别
|
||
* @param array $context 上下文数据
|
||
* @return string
|
||
*/
|
||
public function handleWriteLog(string $message, string $level = 'info', array $context = []): string
|
||
{
|
||
try {
|
||
if (empty($message)) {
|
||
throw new \Exception('日志消息不能为空');
|
||
}
|
||
|
||
// 支持的日志级别
|
||
$allowedLevels = ['debug', 'info', 'notice', 'warning', 'error', 'critical', 'alert', 'emergency'];
|
||
|
||
if (!in_array($level, $allowedLevels)) {
|
||
$level = 'info';
|
||
}
|
||
Log::channel('mcp')->addRecord($level, $message, $context);
|
||
|
||
return "日志记录成功 [级别: {$level}]";
|
||
|
||
} catch (\Exception $e) {
|
||
Log::channel('mcp')->error('MCP日志写入错误: ' . $e->getMessage());
|
||
throw $e;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 处理文件操作
|
||
* @param string $operation 操作类型
|
||
* @param string $filepath 文件路径
|
||
* @return array
|
||
*/
|
||
public function handleFileOperation(string $operation, string $filepath): array
|
||
{
|
||
try {
|
||
if (empty($operation) || empty($filepath)) {
|
||
throw new \Exception('操作类型和文件路径不能为空');
|
||
}
|
||
// 安全检查:限制文件路径范围
|
||
$allowedPaths = [
|
||
runtime_path(),
|
||
public_path(),
|
||
config_path(),
|
||
];
|
||
|
||
$isAllowed = false;
|
||
$realFilePath = realpath($filepath);
|
||
if ($realFilePath) {
|
||
foreach ($allowedPaths as $allowedPath) {
|
||
$realAllowedPath = realpath($allowedPath);
|
||
if ($realAllowedPath && strpos($realFilePath, $realAllowedPath) === 0) {
|
||
$isAllowed = true;
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
|
||
if (!$isAllowed) {
|
||
throw new \Exception('文件路径不在允许的范围内');
|
||
}
|
||
|
||
switch ($operation) {
|
||
case 'read':
|
||
if (!file_exists($filepath)) {
|
||
throw new \Exception('文件不存在');
|
||
}
|
||
return [
|
||
'content' => file_get_contents($filepath),
|
||
'size' => filesize($filepath),
|
||
'modified' => date('Y-m-d H:i:s', filemtime($filepath))
|
||
];
|
||
|
||
case 'exists':
|
||
return ['exists' => file_exists($filepath)];
|
||
|
||
case 'info':
|
||
if (!file_exists($filepath)) {
|
||
throw new \Exception('文件不存在');
|
||
}
|
||
return [
|
||
'size' => filesize($filepath),
|
||
'modified' => date('Y-m-d H:i:s', filemtime($filepath)),
|
||
'is_file' => is_file($filepath),
|
||
'is_dir' => is_dir($filepath),
|
||
'is_readable' => is_readable($filepath),
|
||
'is_writable' => is_writable($filepath)
|
||
];
|
||
|
||
default:
|
||
throw new \Exception('不支持的操作类型: ' . $operation);
|
||
}
|
||
|
||
} catch (\Exception $e) {
|
||
Log::channel('mcp')->error('MCP文件操作错误: ' . $e->getMessage());
|
||
throw $e;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 处理用户管理
|
||
* @param string $action 操作类型
|
||
* @param int $userId 用户ID(可选)
|
||
* @param int $limit 返回数量限制(可选)
|
||
* @return array
|
||
*/
|
||
public function handleUserManagement(string $action, int $userId = 0, int $limit = 10): array
|
||
{
|
||
try {
|
||
switch ($action) {
|
||
case 'list':
|
||
$users = Db::name('admin')
|
||
->field('id,username,email,mobile,created_at,status')
|
||
->limit($limit)
|
||
->select();
|
||
|
||
return [
|
||
'users' => $users->toArray(),
|
||
'count' => count($users)
|
||
];
|
||
|
||
case 'info':
|
||
if (!$userId) {
|
||
throw new \Exception('用户ID不能为空');
|
||
}
|
||
|
||
$user = Db::name('admin')
|
||
->field('id,username,nickname,email,mobile,created_at,status')
|
||
->where('id', $userId)
|
||
->find();
|
||
|
||
if (!$user) {
|
||
throw new \Exception('用户不存在');
|
||
}
|
||
|
||
return $user;
|
||
|
||
case 'count':
|
||
$total = Db::name('admin')->count();
|
||
$active = Db::name('admin')->where('status', 1)->count();
|
||
|
||
return [
|
||
'total' => $total,
|
||
'active' => $active,
|
||
'inactive' => $total - $active
|
||
];
|
||
|
||
default:
|
||
throw new \Exception('不支持的操作类型: ' . $action);
|
||
}
|
||
|
||
} catch (\Exception $e) {
|
||
Log::channel('mcp')->error('MCP用户管理错误: ' . $e->getMessage());
|
||
throw $e;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 处理系统信息
|
||
* @param string $type 信息类型
|
||
* @return array
|
||
*/
|
||
public function handleSystemInfo(string $type = 'general'): array
|
||
{
|
||
try {
|
||
switch ($type) {
|
||
case 'general':
|
||
return [
|
||
'php_version' => PHP_VERSION,
|
||
'framework' => 'Webman',
|
||
'framework_version' => '1.0.0',
|
||
'server_software' => $_SERVER['SERVER_SOFTWARE'] ?? 'Unknown',
|
||
'memory_limit' => ini_get('memory_limit'),
|
||
'max_execution_time' => ini_get('max_execution_time'),
|
||
'upload_max_filesize' => ini_get('upload_max_filesize'),
|
||
'post_max_size' => ini_get('post_max_size'),
|
||
];
|
||
|
||
case 'database':
|
||
$version = 'Unknown';
|
||
try {
|
||
$versionResult = Db::query('SELECT VERSION() as version');
|
||
$version = $versionResult[0]['version'] ?? 'Unknown';
|
||
} catch (\Exception $e) {
|
||
// 数据库连接失败时使用默认值
|
||
}
|
||
|
||
return [
|
||
'type' => config('thinkorm.connections.mysql.type', 'mysql'),
|
||
'version' => $version,
|
||
'charset' => config('thinkorm.connections.mysql.charset', 'utf8mb4'),
|
||
'collation' => config('thinkorm.connections.mysql.collation', 'utf8mb4_general_ci'),
|
||
];
|
||
|
||
case 'performance':
|
||
return [
|
||
'memory_usage' => $this->formatBytes(memory_get_usage(true)),
|
||
'memory_peak' => $this->formatBytes(memory_get_peak_usage(true)),
|
||
'included_files' => count(get_included_files()),
|
||
];
|
||
|
||
case 'cache':
|
||
return [
|
||
'default_driver' => config('plugin.bilulanlv.think-cache.app.default', 'redis'),
|
||
'opcache_enabled' => function_exists('opcache_get_status') && opcache_get_status() !== false,
|
||
'redis_available' => extension_loaded('redis'),
|
||
'memcached_available' => extension_loaded('memcached'),
|
||
];
|
||
|
||
default:
|
||
throw new \Exception('不支持的系统信息类型: ' . $type);
|
||
}
|
||
|
||
} catch (\Exception $e) {
|
||
Log::channel('mcp')->error('MCP系统信息错误: ' . $e->getMessage());
|
||
throw $e;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 格式化字节数
|
||
* @param int $bytes
|
||
* @return string
|
||
*/
|
||
private function formatBytes(int $bytes): string
|
||
{
|
||
$units = ['B', 'KB', 'MB', 'GB'];
|
||
$index = 0;
|
||
|
||
while ($bytes >= 1024 && $index < count($units) - 1) {
|
||
$bytes /= 1024;
|
||
$index++;
|
||
}
|
||
|
||
return round($bytes, 2) . ' ' . $units[$index];
|
||
}
|
||
|
||
/**
|
||
* 处理配置资源
|
||
* @return string
|
||
*/
|
||
public function handleConfigResource(): string
|
||
{
|
||
$configs = [
|
||
'app' => config('app', []),
|
||
'database' => [
|
||
'type' => config('thinkorm.connections.mysql.type', 'mysql'),
|
||
'charset' => config('thinkorm.connections.mysql.charset', 'utf8mb4'),
|
||
'debug' => config('thinkorm.connections.mysql.trigger_sql', true),
|
||
],
|
||
'cache' => config('plugin.bilulanlv.think-cache.app', []),
|
||
'session' => config('session', []),
|
||
'log' => config('log', []),
|
||
];
|
||
|
||
return json_encode($configs, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
|
||
}
|
||
|
||
/**
|
||
* 处理数据库模式资源
|
||
* @return string
|
||
*/
|
||
public function handleSchemaResource(): string
|
||
{
|
||
try {
|
||
// 获取所有表名
|
||
$tables = Db::query('SHOW TABLES');
|
||
$schema = [];
|
||
|
||
foreach ($tables as $table) {
|
||
$tableName = array_values($table)[0];
|
||
|
||
// 获取表结构
|
||
$columns = Db::query("SHOW COLUMNS FROM `{$tableName}`");
|
||
$schema[$tableName] = $columns;
|
||
}
|
||
|
||
return json_encode($schema, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
|
||
|
||
} catch (\Exception $e) {
|
||
Log::channel('mcp')->error('MCP数据库模式获取错误: ' . $e->getMessage());
|
||
return json_encode(['error' => $e->getMessage()], JSON_UNESCAPED_UNICODE);
|
||
}
|
||
}
|
||
|
||
|
||
/**
|
||
* 设置日志记录器
|
||
* @param $logger
|
||
* @return $this
|
||
*/
|
||
public function setLogger($logger)
|
||
{
|
||
$this->logger = $logger;
|
||
return $this;
|
||
}
|
||
|
||
/**
|
||
* 获取当前配置信息
|
||
* @return array
|
||
*/
|
||
public function getConfig()
|
||
{
|
||
return [
|
||
'timeout' => $this->timeout,
|
||
'connect_timeout' => $this->connectTimeout,
|
||
'read_timeout' => $this->readTimeout,
|
||
'retry_attempts' => $this->retryAttempts,
|
||
'retry_delay' => $this->retryDelay,
|
||
'debug' => $this->debug
|
||
];
|
||
}
|
||
|
||
/**
|
||
* 启动MCP服务器(STDIO传输)
|
||
*/
|
||
public function startWithStdio()
|
||
{
|
||
try {
|
||
// 启动心跳机制
|
||
$this->startHeartbeat();
|
||
|
||
$server = $this->buildServer();
|
||
$transport = new StdioServerTransport();
|
||
|
||
Log::channel('mcp')->info('MCP STDIO服务器启动成功');
|
||
$server->listen($transport);
|
||
|
||
} catch (\Exception $e) {
|
||
Log::channel('mcp')->error('MCP STDIO服务器启动失败: ' . $e->getMessage());
|
||
throw $e;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 使用指定传输启动MCP服务器
|
||
*/
|
||
public function startWithTransport($transport)
|
||
{
|
||
try {
|
||
// 启动心跳机制
|
||
$this->startHeartbeat();
|
||
|
||
$server = $this->buildServer();
|
||
$server->listen($transport);
|
||
|
||
Log::channel('mcp')->info('MCP服务器启动成功');
|
||
|
||
} catch (\Exception $e) {
|
||
Log::channel('mcp')->error('MCP服务器启动失败: ' . $e->getMessage());
|
||
throw $e;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 使用SSE传输启动MCP服务器
|
||
*/
|
||
public function startWithSse(string $host = '127.0.0.1', int $port = 8080, string $mcpPath = 'mcp')
|
||
{
|
||
try {
|
||
// 启动心跳机制
|
||
$this->startHeartbeat();
|
||
|
||
$server = $this->buildServer();
|
||
$transport = new \PhpMcp\Server\Transports\StreamableHttpServerTransport($host, $port, $mcpPath);
|
||
|
||
Log::channel('mcp')->info("MCP SSE服务器启动成功,监听地址: http://{$host}:{$port}/{$mcpPath}");
|
||
$server->listen($transport);
|
||
|
||
} catch (\Exception $e) {
|
||
Log::channel('mcp')->error('MCP SSE服务器启动失败: ' . $e->getMessage());
|
||
throw $e;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 使用HTTP传输启动MCP服务器
|
||
*/
|
||
public function startWithHttp(string $host = '127.0.0.1', int $port = 8080, string $mcpPath = 'mcp')
|
||
{
|
||
try {
|
||
// 启动心跳机制
|
||
$this->startHeartbeat();
|
||
|
||
$server = $this->buildServer();
|
||
$transport = new \PhpMcp\Server\Transports\HttpServerTransport($host, $port, $mcpPath);
|
||
|
||
Log::channel('mcp')->info("MCP HTTP服务器启动成功,监听地址: http://{$host}:{$port}/{$mcpPath}");
|
||
$server->listen($transport);
|
||
|
||
} catch (\Exception $e) {
|
||
Log::channel('mcp')->error('MCP HTTP服务器启动失败: ' . $e->getMessage());
|
||
throw $e;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 获取服务器实例
|
||
* @return Server|null
|
||
*/
|
||
public function getServer()
|
||
{
|
||
return $this->buildServer();
|
||
}
|
||
|
||
/**
|
||
* 获取服务信息
|
||
* @return array
|
||
*/
|
||
public function getServiceInfo(): array
|
||
{
|
||
return [
|
||
'name' => $this->name,
|
||
'version' => $this->version,
|
||
'tools' => 16, // 16个工具(新增ThinkPHP命令工具)
|
||
'resources' => 3, // 3个资源
|
||
'prompt' => 1, // 1个提示词
|
||
'status' => 'ready',
|
||
'config' => $this->getConfig()
|
||
];
|
||
}
|
||
|
||
/**
|
||
* 生成webman控制器文件
|
||
* @param string $module 模块名称 (admin/api/frontend等)
|
||
* @param string $controller 控制器名称
|
||
* @param array $fields 字段信息 (可选)
|
||
* @param string $description 控制器描述 (可选)
|
||
* @return array
|
||
*/
|
||
public function handleCreateController(string $module, string $controller, array $fields = [], string $description = ''): array
|
||
{
|
||
try {
|
||
// 生成控制器类名
|
||
$controllerClass = ucfirst($controller);
|
||
$controllerPath = "app/{$module}/controller/{$controllerClass}.php";
|
||
if ($module == 'admin') {
|
||
$controllerPath = "plugin/admin/app/controller/{$controllerClass}.php";
|
||
}
|
||
|
||
// 检查文件是否已存在
|
||
if (file_exists($controllerPath)) {
|
||
return [
|
||
'success' => false,
|
||
'error' => "控制器文件 {$controllerPath} 已存在"
|
||
];
|
||
}
|
||
|
||
// 生成控制器内容
|
||
$controllerContent = $this->CreateControllerContent($module, $controllerClass, $fields, $description);
|
||
|
||
// 确保目录存在
|
||
$dir = dirname($controllerPath);
|
||
if (!is_dir($dir)) {
|
||
mkdir($dir, 0755, true);
|
||
}
|
||
|
||
// 写入文件
|
||
if (file_put_contents($controllerPath, $controllerContent)) {
|
||
Log::channel('mcp')->info("webman控制器生成成功: {$controllerPath}");
|
||
return [
|
||
'success' => true,
|
||
'message' => '控制器生成成功',
|
||
'file_path' => $controllerPath,
|
||
'content' => $controllerContent
|
||
];
|
||
} else {
|
||
throw new \Exception('文件写入失败');
|
||
}
|
||
|
||
} catch (\Exception $e) {
|
||
Log::channel('mcp')->error('webman控制器生成错误: ' . $e->getMessage());
|
||
return [
|
||
'success' => false,
|
||
'error' => $e->getMessage()
|
||
];
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 生成模型文件
|
||
* @param string $modelName 模型名称
|
||
* @param array $fields 字段信息 (可选)
|
||
* @param string $tableName 表名 (可选,默认使用模型名)
|
||
* @param string $description 模型描述 (可选)
|
||
* @return array
|
||
*/
|
||
public function handleCreateModel(string $modelName, array $fields = [], string $tableName = '', string $description = ''): array
|
||
{
|
||
try {
|
||
// 生成模型类名
|
||
$modelClass = ucfirst($modelName);
|
||
$modelPath = "app/model/{$modelClass}.php";
|
||
|
||
// 检查文件是否已存在
|
||
if (file_exists($modelPath)) {
|
||
return [
|
||
'success' => false,
|
||
'error' => "模型文件 {$modelPath} 已存在"
|
||
];
|
||
}
|
||
|
||
// 如果没有指定表名,使用模型名
|
||
if (empty($tableName)) {
|
||
$tableName = strtolower($modelName);
|
||
}
|
||
|
||
// 生成模型内容
|
||
$modelContent = $this->CreateModelContent($modelClass, $tableName, $fields, $description);
|
||
|
||
// 确保目录存在
|
||
$dir = dirname($modelPath);
|
||
if (!is_dir($dir)) {
|
||
mkdir($dir, 0755, true);
|
||
}
|
||
|
||
// 写入文件
|
||
if (file_put_contents($modelPath, $modelContent)) {
|
||
Log::channel('mcp')->info("模型生成成功: {$modelPath}");
|
||
return [
|
||
'success' => true,
|
||
'message' => '模型生成成功',
|
||
'file_path' => $modelPath,
|
||
'content' => $modelContent
|
||
];
|
||
} else {
|
||
throw new \Exception('文件写入失败');
|
||
}
|
||
|
||
} catch (\Exception $e) {
|
||
Log::channel('mcp')->error('模型生成错误: ' . $e->getMessage());
|
||
return [
|
||
'success' => false,
|
||
'error' => $e->getMessage()
|
||
];
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 生成控制器内容
|
||
* @param string $module 模块名称
|
||
* @param string $controllerClass 控制器类名
|
||
* @param array $fields 字段信息
|
||
* @param string $description 描述
|
||
* @return string
|
||
*/
|
||
private function CreateControllerContent(string $module, string $controllerClass, array $fields = [], string $description = ''): string
|
||
{
|
||
|
||
$description = $description ?: $controllerClass;
|
||
$tpl = 'app/mcp/tpl/controller/' . $module . '.tpl';
|
||
if (!file_exists($tpl)) {
|
||
$tpl = 'app/mcp/tpl/controller/default.tpl';
|
||
}
|
||
$content = view($tpl, [
|
||
'description' => $description,
|
||
'controllerClass' => $controllerClass,
|
||
]);
|
||
|
||
return $content;
|
||
}
|
||
|
||
/**
|
||
* 生成模型内容
|
||
* @param string $modelClass 模型类名
|
||
* @param string $tableName 表名
|
||
* @param array $fields 字段信息
|
||
* @param string $description 描述
|
||
* @return string
|
||
*/
|
||
private function CreateModelContent(string $modelClass, string $tableName, array $fields = [], string $description = ''): string
|
||
{
|
||
$description = $description ?: $modelClass;
|
||
|
||
// 生成字段定义
|
||
$fieldDefinitions = '';
|
||
if (!empty($fields)) {
|
||
$fieldDefinitions = " // 字段定义\n";
|
||
foreach ($fields as $field) {
|
||
$fieldName = $field['name'] ?? '';
|
||
$fieldType = $field['type'] ?? 'string';
|
||
$fieldComment = $field['comment'] ?? '';
|
||
if ($fieldName) {
|
||
$fieldDefinitions .= " protected \$" . $fieldName . " = ''; // {$fieldComment}\n";
|
||
}
|
||
}
|
||
}
|
||
|
||
$content = "<?php
|
||
namespace app\\model;
|
||
|
||
// SoftDelete 为 ThinkPHP 专属,这里注释以避免耦合
|
||
// use traits\\model\\SoftDelete;
|
||
|
||
/**
|
||
* {$description}模型
|
||
* Class {$modelClass}
|
||
* @package app\\model
|
||
*/
|
||
class {$modelClass} extends Base
|
||
{
|
||
// use SoftDelete; // 如需软删除,请实现项目内等价方案
|
||
|
||
/**
|
||
* 数据表名
|
||
* @var string
|
||
*/
|
||
protected \$table = '{$tableName}';
|
||
|
||
/**
|
||
* 软删除字段
|
||
* @var string
|
||
*/
|
||
protected \$deleteTime = 'deleted_at';
|
||
|
||
{$fieldDefinitions}
|
||
|
||
}";
|
||
|
||
return $content;
|
||
}
|
||
|
||
/**
|
||
* 创建数据库表格
|
||
* @param string $tableName 表名
|
||
* @param array $fields 字段信息数组
|
||
* @param string $tableComment 表注释
|
||
* @param string $engine 存储引擎 (默认 InnoDB)
|
||
* @param string $charset 字符集 (默认 utf8mb4)
|
||
* @return array
|
||
*/
|
||
public function handleCreateTable(string $tableName, array $fields, string $tableComment = '', string $engine = 'InnoDB', string $charset = 'utf8mb4'): array
|
||
{
|
||
try {
|
||
// 验证表名
|
||
if (empty($tableName)) {
|
||
throw new \Exception('表名不能为空');
|
||
}
|
||
|
||
// 验证字段信息
|
||
if (empty($fields) || !is_array($fields)) {
|
||
throw new \Exception('字段信息不能为空且必须是数组');
|
||
}
|
||
|
||
// 检查表是否已存在
|
||
$existingTables = Db::query("SHOW TABLES LIKE '{$tableName}'");
|
||
if (!empty($existingTables)) {
|
||
return [
|
||
'success' => false,
|
||
'error' => "表 {$tableName} 已存在"
|
||
];
|
||
}
|
||
|
||
// 生成建表SQL
|
||
$createSql = $this->CreateCreateTableSql($tableName, $fields, $tableComment, $engine, $charset);
|
||
|
||
// 执行建表SQL
|
||
$result = Db::execute($createSql);
|
||
|
||
if ($result !== false) {
|
||
Log::channel('mcp')->info("数据库表创建成功: {$tableName}");
|
||
return [
|
||
'success' => true,
|
||
'message' => '表创建成功',
|
||
'table_name' => $tableName,
|
||
'sql' => $createSql,
|
||
'fields_count' => count($fields)
|
||
];
|
||
} else {
|
||
throw new \Exception('建表SQL执行失败');
|
||
}
|
||
|
||
} catch (\Exception $e) {
|
||
Log::channel('mcp')->error('创建数据库表出现错误: ' . $e->getMessage());
|
||
return [
|
||
'success' => false,
|
||
'error' => $e->getMessage()
|
||
];
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 生成建表SQL
|
||
* @param string $tableName 表名
|
||
* @param array $fields 字段信息
|
||
* @param string $tableComment 表注释
|
||
* @param string $engine 存储引擎
|
||
* @param string $charset 字符集
|
||
* @return string
|
||
*/
|
||
private function CreateCreateTableSql(string $tableName, array $fields, string $tableComment = '', string $engine = 'InnoDB', string $charset = 'utf8mb4'): string
|
||
{
|
||
$sql = "CREATE TABLE `{$tableName}` (\n";
|
||
|
||
$fieldDefinitions = [];
|
||
$primaryKey = null;
|
||
|
||
foreach ($fields as $field) {
|
||
$fieldName = $field['name'] ?? '';
|
||
$fieldType = $field['type'] ?? 'varchar(255)';
|
||
$fieldLength = $field['length'] ?? '';
|
||
$fieldDefault = $field['default'] ?? '';
|
||
$fieldComment = $field['comment'] ?? '';
|
||
$fieldNull = isset($field['null']) && $field['null'] ? 'NULL' : 'NOT NULL';
|
||
$fieldAutoIncrement = isset($field['auto_increment']) && $field['auto_increment'] ? 'AUTO_INCREMENT' : '';
|
||
$fieldPrimary = isset($field['primary']) && $field['primary'] ? 'PRIMARY KEY' : '';
|
||
|
||
// 构建字段定义
|
||
$fieldDef = " `{$fieldName}` {$fieldType}";
|
||
|
||
// 添加长度
|
||
if (!empty($fieldLength) && !in_array(strtolower($fieldType), ['text', 'longtext', 'mediumtext', 'tinytext', 'blob', 'longblob', 'mediumblob', 'tinyblob'])) {
|
||
$fieldDef .= "({$fieldLength})";
|
||
}
|
||
|
||
// 添加默认值
|
||
if ($fieldDefault !== '') {
|
||
if (is_string($fieldDefault)) {
|
||
$fieldDef .= " DEFAULT '{$fieldDefault}'";
|
||
} else {
|
||
$fieldDef .= " DEFAULT {$fieldDefault}";
|
||
}
|
||
}
|
||
|
||
// 添加NULL/NOT NULL
|
||
$fieldDef .= " {$fieldNull}";
|
||
|
||
// 添加自增
|
||
if (!empty($fieldAutoIncrement)) {
|
||
$fieldDef .= " {$fieldAutoIncrement}";
|
||
}
|
||
|
||
// 添加注释
|
||
if (!empty($fieldComment)) {
|
||
$fieldDef .= " COMMENT '{$fieldComment}'";
|
||
}
|
||
|
||
// 添加主键
|
||
if (!empty($fieldPrimary)) {
|
||
$fieldDef .= " {$fieldPrimary}";
|
||
$primaryKey = $fieldName;
|
||
}
|
||
|
||
$fieldDefinitions[] = $fieldDef;
|
||
}
|
||
|
||
// 添加默认字段(如果不存在)
|
||
$hasId = false;
|
||
$hasCreateTime = false;
|
||
$hasUpdateTime = false;
|
||
$hasDeleteTime = false;
|
||
|
||
foreach ($fields as $field) {
|
||
if ($field['name'] === 'id')
|
||
$hasId = true;
|
||
if ($field['name'] === 'created_at')
|
||
$hasCreateTime = true;
|
||
if ($field['name'] === 'updated_at')
|
||
$hasUpdateTime = true;
|
||
if ($field['name'] === 'deleted_at')
|
||
$hasDeleteTime = true;
|
||
}
|
||
|
||
// 如果没有ID字段,添加默认ID字段
|
||
if (!$hasId) {
|
||
array_unshift($fieldDefinitions, " `id` int(11) NOT NULL AUTO_INCREMENT PRIMARY KEY COMMENT '主键ID'");
|
||
}
|
||
|
||
// 添加默认时间字段
|
||
if (!$hasCreateTime) {
|
||
$fieldDefinitions[] = " `created_at` int(11) DEFAULT NULL COMMENT '创建时间'";
|
||
}
|
||
if (!$hasUpdateTime) {
|
||
$fieldDefinitions[] = " `updated_at` int(11) DEFAULT NULL COMMENT '更新时间'";
|
||
}
|
||
if (!$hasDeleteTime) {
|
||
$fieldDefinitions[] = " `deleted_at` int(11) DEFAULT NULL COMMENT '删除时间'";
|
||
}
|
||
|
||
$sql .= implode(",\n", $fieldDefinitions);
|
||
$sql .= "\n)";
|
||
|
||
// 添加表注释
|
||
if (!empty($tableComment)) {
|
||
$sql .= " COMMENT='{$tableComment}'";
|
||
}
|
||
|
||
// 添加存储引擎和字符集
|
||
$sql .= " ENGINE={$engine} DEFAULT CHARSET={$charset}";
|
||
|
||
return $sql;
|
||
}
|
||
|
||
/**
|
||
* 获取支持的字段类型
|
||
* @return array
|
||
*/
|
||
public function getSupportedFieldTypes(): array
|
||
{
|
||
return [
|
||
'整数类型' => [
|
||
'int(11)' => '整数类型,11位长度',
|
||
'bigint(20)' => '大整数类型,20位长度',
|
||
'tinyint(1)' => '小整数类型,1位长度',
|
||
'smallint(6)' => '小整数类型,6位长度',
|
||
'mediumint(9)' => '中等整数类型,9位长度'
|
||
],
|
||
'字符串类型' => [
|
||
'varchar(255)' => '可变长度字符串,最大255字符',
|
||
'char(50)' => '固定长度字符串,50字符',
|
||
'text' => '长文本类型',
|
||
'longtext' => '超长文本类型',
|
||
'mediumtext' => '中等长度文本类型',
|
||
'tinytext' => '短文本类型'
|
||
],
|
||
'浮点数类型' => [
|
||
'decimal(10,2)' => '定点数类型,10位总长度,2位小数',
|
||
'float' => '单精度浮点数',
|
||
'double' => '双精度浮点数'
|
||
],
|
||
'日期时间类型' => [
|
||
'datetime' => '日期时间类型',
|
||
'timestamp' => '时间戳类型',
|
||
'date' => '日期类型',
|
||
'time' => '时间类型',
|
||
'year' => '年份类型'
|
||
],
|
||
'其他类型' => [
|
||
'json' => 'JSON数据类型',
|
||
'blob' => '二进制大对象',
|
||
'longblob' => '长二进制大对象',
|
||
'mediumblob' => '中等二进制大对象',
|
||
'tinyblob' => '小二进制大对象'
|
||
]
|
||
];
|
||
}
|
||
|
||
/**
|
||
* 处理CRUD生成,基于fun/curd/Curd.php功能
|
||
* @param string $tableName 表名
|
||
* @param string $module 模块名(admin/frontend/api)
|
||
* @param array $fields 字段信息
|
||
* @param string $description 描述
|
||
* @param array $options 其他选项
|
||
* @return array
|
||
*/
|
||
public function handleCurd(string $tableName, string $module = 'admin', array $fields = [], string $description = '', array $options = []): array
|
||
{
|
||
try {
|
||
// 构建命令行参数
|
||
$parameters = [
|
||
'--table=' . $tableName,
|
||
'--app=' . $module,
|
||
'--controller=' . $this->convertTableNameToControllerName($tableName),
|
||
'--model=' . $this->convertTableNameToModelName($tableName),
|
||
'--validate=' . $this->convertTableNameToModelName($tableName),
|
||
];
|
||
|
||
// 添加可选参数
|
||
if (!empty($options['force'])) {
|
||
$parameters[] = '--force=1';
|
||
}
|
||
if (!empty($options['menu'])) {
|
||
$parameters[] = '--menu=1';
|
||
}
|
||
if (!empty($options['menuname'])) {
|
||
$parameters[] = '--menuname=' . $options['menuname'];
|
||
}
|
||
if (!empty($options['common'])) {
|
||
$parameters[] = '--common=1';
|
||
}
|
||
|
||
// webman 不支持 ThinkPHP Console,使用文件生成方式
|
||
$content = $this->generateCurdFiles($tableName, $module, $fields, $description, $options);
|
||
|
||
// 检查执行结果
|
||
if (strpos($content, 'success') !== false || strpos($content, 'make success') !== false) {
|
||
return [
|
||
'success' => true,
|
||
'message' => 'CRUD模块生成成功',
|
||
'data' => [
|
||
'table' => $tableName,
|
||
'module' => $module,
|
||
'controller' => $this->convertTableNameToControllerName($tableName),
|
||
'model' => $this->convertTableNameToModelName($tableName),
|
||
'output' => $content
|
||
]
|
||
];
|
||
} else {
|
||
return [
|
||
'success' => false,
|
||
'error' => 'CRUD模块生成失败',
|
||
'output' => $content
|
||
];
|
||
}
|
||
|
||
} catch (\Exception $e) {
|
||
Log::channel('mcp')->error('CRUD生成错误: ' . $e->getMessage());
|
||
return [
|
||
'success' => false,
|
||
'error' => $e->getMessage()
|
||
];
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 处理插件管理,基于fun/curd/Addon.php功能
|
||
* @param string $action 操作类型(create/install/uninstall/enable/disable)
|
||
* @param string $addonName 插件名称
|
||
* @param array $options 其他选项
|
||
* @return array
|
||
*/
|
||
public function handleAddon(string $action, string $addonName = '', array $options = []): array
|
||
{
|
||
try {
|
||
// 构建命令行参数
|
||
$parameters = [];
|
||
|
||
switch ($action) {
|
||
case 'create':
|
||
if (empty($addonName)) {
|
||
throw new \Exception('插件名称不能为空');
|
||
}
|
||
$parameters = [
|
||
'--app=' . $addonName,
|
||
'--title=' . ($options['title'] ?? $addonName),
|
||
'--description=' . ($options['description'] ?? $addonName),
|
||
'--author=' . ($options['author'] ?? 'webman'),
|
||
'--ver=' . ($options['version'] ?? '1.0.0'),
|
||
'--requires=' . ($options['requires'] ?? '1.0.0'),
|
||
];
|
||
if (!empty($options['force'])) {
|
||
$parameters[] = '--force=1';
|
||
}
|
||
break;
|
||
|
||
case 'install':
|
||
if (empty($addonName)) {
|
||
throw new \Exception('插件名称不能为空');
|
||
}
|
||
$parameters = [
|
||
'--install=1',
|
||
'--app=' . $addonName
|
||
];
|
||
break;
|
||
|
||
case 'uninstall':
|
||
if (empty($addonName)) {
|
||
throw new \Exception('插件名称不能为空');
|
||
}
|
||
$parameters = [
|
||
'--delete=1',
|
||
'--force=1',
|
||
'--app=' . $addonName,
|
||
];
|
||
break;
|
||
|
||
case 'enable':
|
||
$parameters = [
|
||
'--app=' . $addonName,
|
||
'--enable=1',
|
||
];
|
||
break;
|
||
|
||
case 'disable':
|
||
$parameters = [
|
||
'--app=' . $addonName,
|
||
'--disable=1',
|
||
];
|
||
break;
|
||
default:
|
||
throw new \Exception('不支持的操作类型: ' . $action);
|
||
}
|
||
|
||
// webman 不支持 ThinkPHP Console,使用文件生成方式
|
||
$content = $this->generateAddonFiles($addonName, $action, $options);
|
||
|
||
// 检查执行结果
|
||
if (strpos($content, 'success') !== false || strpos($content, 'make success') !== false) {
|
||
return [
|
||
'success' => true,
|
||
'message' => "插件{$action}操作成功",
|
||
'data' => [
|
||
'action' => $action,
|
||
'addon_name' => $addonName,
|
||
'output' => $content
|
||
]
|
||
];
|
||
} else {
|
||
return [
|
||
'success' => false,
|
||
'error' => "插件{$action}操作失败",
|
||
'output' => $content
|
||
];
|
||
}
|
||
|
||
} catch (\Exception $e) {
|
||
Log::channel('mcp')->error('插件管理错误: ' . $e->getMessage());
|
||
return [
|
||
'success' => false,
|
||
'error' => $e->getMessage()
|
||
];
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 处理菜单管理,基于fun/curd/Menu.php功能
|
||
* @param string $action 操作类型(create/delete)
|
||
* @param array $menuData 菜单数据
|
||
* @param array $options 其他选项
|
||
* @return array
|
||
*/
|
||
public function handleMenu(string $action, array $menuData = [], array $options = []): array
|
||
{
|
||
try {
|
||
// 构建命令行参数
|
||
$parameters = [];
|
||
|
||
switch ($action) {
|
||
case 'create':
|
||
if (empty($menuData['controller'])) {
|
||
throw new \Exception('控制器名称不能为空');
|
||
}
|
||
$parameters = [
|
||
'--controller=' . $menuData['controller'],
|
||
'--app=' . ($menuData['app'] ?? 'admin'),
|
||
];
|
||
if (!empty($menuData['menuname'])) {
|
||
$parameters[] = '--menuname=' . $menuData['menuname'];
|
||
}
|
||
if (!empty($options['force'])) {
|
||
$parameters[] = '--force=1';
|
||
}
|
||
break;
|
||
case 'delete':
|
||
if (empty($menuData['controller'])) {
|
||
throw new \Exception('控制器名称不能为空');
|
||
}
|
||
$parameters = [
|
||
'--controller=' . $menuData['controller'],
|
||
'--app=' . ($menuData['app'] ?? 'admin'),
|
||
'--delete=1',
|
||
'--force=1'
|
||
];
|
||
break;
|
||
default:
|
||
throw new \Exception('不支持的操作类型: ' . $action);
|
||
}
|
||
|
||
// webman 不支持 ThinkPHP Console,使用文件生成方式
|
||
$content = $this->generateMenuFiles($menuData, $action, $options);
|
||
|
||
// 检查执行结果
|
||
if (strpos($content, 'success') !== false || strpos($content, 'make success') !== false) {
|
||
return [
|
||
'success' => true,
|
||
'message' => "菜单{$action}操作成功",
|
||
'data' => [
|
||
'action' => $action,
|
||
'menu_data' => $menuData,
|
||
'output' => $content
|
||
]
|
||
];
|
||
} else {
|
||
return [
|
||
'success' => false,
|
||
'error' => "菜单{$action}操作失败",
|
||
'output' => $content
|
||
];
|
||
}
|
||
|
||
} catch (\Exception $e) {
|
||
Log::channel('mcp')->error('菜单管理错误: ' . $e->getMessage());
|
||
return [
|
||
'success' => false,
|
||
'error' => $e->getMessage()
|
||
];
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 通过自然语言描述生成数据库表、控制器、模型等
|
||
* @param string $prompt 自然语言描述
|
||
* @param string $type 生成类型 (table/controller/model/js/api/view/all)
|
||
* @return array
|
||
*/
|
||
public function handleWithPrompt(string $prompt, string $type = 'all'): array
|
||
{
|
||
try {
|
||
if (empty($prompt)) {
|
||
throw new \Exception('描述不能为空');
|
||
}
|
||
|
||
// 解析提示词
|
||
$parsedData = $this->parsePrompt($prompt);
|
||
|
||
$results = [];
|
||
|
||
// 根据类型生成相应的内容
|
||
if (in_array($type, ['table', 'all'])) {
|
||
if (!empty($parsedData['table'])) {
|
||
$tableResult = $this->handleCreateTable(
|
||
$parsedData['table']['name'],
|
||
$parsedData['table']['fields'],
|
||
$parsedData['table']['comment'] ?? '',
|
||
'InnoDB',
|
||
'utf8mb4'
|
||
);
|
||
$results['table'] = $tableResult;
|
||
}
|
||
}
|
||
|
||
if (in_array($type, ['controller', 'all'])) {
|
||
if (!empty($parsedData['controller'])) {
|
||
$controllerResult = $this->handleCreateController(
|
||
$parsedData['controller']['module'] ?? 'admin',
|
||
$parsedData['controller']['name'],
|
||
$parsedData['controller']['fields'] ?? [],
|
||
$parsedData['controller']['description'] ?? ''
|
||
);
|
||
$results['controller'] = $controllerResult;
|
||
}
|
||
}
|
||
|
||
if (in_array($type, ['model', 'all'])) {
|
||
if (!empty($parsedData['model'])) {
|
||
$modelResult = $this->handleCreateModel(
|
||
$parsedData['model']['name'],
|
||
$parsedData['model']['fields'] ?? [],
|
||
$parsedData['model']['table'] ?? '',
|
||
$parsedData['model']['description'] ?? ''
|
||
);
|
||
$results['model'] = $modelResult;
|
||
}
|
||
}
|
||
|
||
if (in_array($type, ['js', 'all'])) {
|
||
if (!empty($parsedData['js'])) {
|
||
$jsResult = $this->handleCreateJs(
|
||
$parsedData['js']['module'] ?? 'admin',
|
||
$parsedData['js']['name'],
|
||
$parsedData['js']['fields'] ?? [],
|
||
$parsedData['js']['description'] ?? ''
|
||
);
|
||
$results['js'] = $jsResult;
|
||
}
|
||
}
|
||
|
||
if (in_array($type, ['api', 'all'])) {
|
||
if (!empty($parsedData['api'])) {
|
||
$apiResult = $this->handleCreateApi(
|
||
$parsedData['api']['module'] ?? 'api',
|
||
$parsedData['api']['name'],
|
||
$parsedData['api']['fields'] ?? [],
|
||
$parsedData['api']['description'] ?? ''
|
||
);
|
||
$results['api'] = $apiResult;
|
||
}
|
||
}
|
||
|
||
if (in_array($type, ['view', 'all'])) {
|
||
if (!empty($parsedData['view'])) {
|
||
$viewResult = $this->handleCreateView(
|
||
$parsedData['view']['module'] ?? 'admin',
|
||
$parsedData['view']['name'],
|
||
$parsedData['view']['fields'] ?? [],
|
||
$parsedData['view']['description'] ?? ''
|
||
);
|
||
$results['view'] = $viewResult;
|
||
}
|
||
}
|
||
|
||
if (in_array($type, ['addon', 'all'])) {
|
||
if (!empty($parsedData['addon'])) {
|
||
$addonResult = $this->handleAddon(
|
||
'create',
|
||
$parsedData['addon']['name'],
|
||
$parsedData['addon']['options'] ?? []
|
||
);
|
||
$results['addon'] = $addonResult;
|
||
}
|
||
}
|
||
|
||
if (in_array($type, ['curd', 'all'])) {
|
||
if (!empty($parsedData['curd'])) {
|
||
$curdResult = $this->handleCurd(
|
||
$parsedData['curd']['name'],
|
||
$parsedData['curd']['module'] ?? 'admin',
|
||
$parsedData['curd']['fields'] ?? [],
|
||
$parsedData['curd']['description'] ?? '',
|
||
$parsedData['curd']['options'] ?? []
|
||
);
|
||
$results['curd'] = $curdResult;
|
||
}
|
||
}
|
||
|
||
if (in_array($type, ['menu', 'all'])) {
|
||
if (!empty($parsedData['menu'])) {
|
||
$menuResult = $this->handleMenu(
|
||
'create',
|
||
$parsedData['menu']['data'] ?? [],
|
||
$parsedData['menu']['options'] ?? []
|
||
);
|
||
$results['menu'] = $menuResult;
|
||
}
|
||
}
|
||
//如果这里面都没有 那么执行其他操作
|
||
if (empty($results)) {
|
||
$results = $this->handleOtherOperation($prompt);
|
||
}
|
||
|
||
return [
|
||
'success' => true,
|
||
'message' => '通过提示词生成成功',
|
||
'parsed_data' => $parsedData,
|
||
'results' => $results
|
||
];
|
||
|
||
} catch (\Exception $e) {
|
||
Log::channel('mcp')->error('webman withPrompt错误: ' . $e->getMessage());
|
||
return [
|
||
'success' => false,
|
||
'error' => $e->getMessage()
|
||
];
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 解析自然语言提示词
|
||
* @param string $prompt
|
||
* @return array
|
||
*/
|
||
private function parsePrompt(string $prompt): array
|
||
{
|
||
$parsedData = [
|
||
'table' => null,
|
||
'controller' => null,
|
||
'model' => null,
|
||
'js' => null,
|
||
'api' => null,
|
||
'view' => null,
|
||
'addon' => null,
|
||
'curd' => null,
|
||
'menu' => null
|
||
];
|
||
|
||
// 转换为小写便于匹配
|
||
$lowerPrompt = strtolower($prompt);
|
||
|
||
// 提取表名
|
||
$tableName = null;
|
||
if (preg_match('/(?:创建|生成|建立).*?(?:表|table).*?[名为|叫|是]\s*([a-zA-Z_][a-zA-Z0-9_]*)/', $lowerPrompt, $matches)) {
|
||
$tableName = $matches[1];
|
||
} elseif (preg_match('/([a-zA-Z_][a-zA-Z0-9_]*)\s*(?:表|table)/', $lowerPrompt, $matches)) {
|
||
$tableName = $matches[1];
|
||
} elseif (preg_match('/(?:创建|生成|建立).*?([a-zA-Z_][a-zA-Z0-9_]*)/', $lowerPrompt, $matches)) {
|
||
$tableName = $matches[1];
|
||
}
|
||
|
||
// 提取字段信息
|
||
$fields = $this->extractFieldsFromPrompt($prompt);
|
||
|
||
// 提取表注释
|
||
$tableComment = $this->extractTableComment($prompt);
|
||
|
||
// 构建表数据
|
||
if ($tableName) {
|
||
$parsedData['table'] = [
|
||
'name' => $tableName,
|
||
'fields' => $fields,
|
||
'comment' => $tableComment
|
||
];
|
||
|
||
// 构建控制器数据
|
||
$controllerName = ucfirst($tableName) . 'Controller';
|
||
$parsedData['controller'] = [
|
||
'module' => 'admin',
|
||
'name' => $controllerName,
|
||
'fields' => $fields,
|
||
'description' => $tableComment ?: $controllerName
|
||
];
|
||
|
||
// 构建模型数据
|
||
$modelName = ucfirst($tableName);
|
||
$parsedData['model'] = [
|
||
'name' => $modelName,
|
||
'fields' => $fields,
|
||
'table' => $tableName,
|
||
'description' => $tableComment ?: $modelName
|
||
];
|
||
|
||
// 构建JS数据
|
||
$parsedData['js'] = [
|
||
'module' => 'admin',
|
||
'name' => $controllerName,
|
||
'fields' => $fields,
|
||
'description' => $tableComment ?: $controllerName
|
||
];
|
||
|
||
// 构建API数据
|
||
$parsedData['api'] = [
|
||
'module' => 'api',
|
||
'name' => $controllerName,
|
||
'fields' => $fields,
|
||
'description' => $tableComment ?: $controllerName
|
||
];
|
||
|
||
// 构建视图数据
|
||
$parsedData['view'] = [
|
||
'module' => 'admin',
|
||
'name' => $controllerName,
|
||
'fields' => $fields,
|
||
'description' => $tableComment ?: $controllerName
|
||
];
|
||
|
||
// 构建CRUD数据
|
||
$parsedData['curd'] = [
|
||
'name' => $tableName,
|
||
'module' => 'admin',
|
||
'fields' => $fields,
|
||
'description' => $tableComment ?: $tableName,
|
||
'options' => []
|
||
];
|
||
|
||
// 构建菜单数据
|
||
$parsedData['menu'] = [
|
||
'data' => [
|
||
'controller' => $controllerName,
|
||
'app' => 'admin',
|
||
'menuname' => $tableComment ?: $tableName
|
||
],
|
||
'options' => []
|
||
];
|
||
}
|
||
|
||
// 解析特殊操作
|
||
$this->parseSpecialOperations($lowerPrompt, $parsedData);
|
||
|
||
return $parsedData;
|
||
}
|
||
|
||
/**
|
||
* 解析特殊操作
|
||
* @param string $lowerPrompt
|
||
* @param array &$parsedData
|
||
*/
|
||
private function parseSpecialOperations(string $lowerPrompt, array &$parsedData): void
|
||
{
|
||
// 解析JS相关操作
|
||
if (strpos($lowerPrompt, 'js') !== false || strpos($lowerPrompt, 'javascript') !== false || strpos($lowerPrompt, '前端') !== false) {
|
||
if (!empty($parsedData['js'])) {
|
||
$parsedData['js']['description'] = '前端JS文件';
|
||
}
|
||
}
|
||
|
||
// 解析API相关操作
|
||
if (strpos($lowerPrompt, 'api') !== false || strpos($lowerPrompt, '接口') !== false || strpos($lowerPrompt, '接口文件') !== false) {
|
||
if (!empty($parsedData['api'])) {
|
||
$parsedData['api']['description'] = 'API接口文件';
|
||
}
|
||
}
|
||
|
||
// 解析视图相关操作
|
||
if (strpos($lowerPrompt, 'view') !== false || strpos($lowerPrompt, '视图') !== false || strpos($lowerPrompt, '页面') !== false) {
|
||
if (!empty($parsedData['view'])) {
|
||
$parsedData['view']['description'] = '视图文件';
|
||
}
|
||
}
|
||
|
||
// 解析插件相关操作
|
||
if (strpos($lowerPrompt, 'addon') !== false || strpos($lowerPrompt, '插件') !== false) {
|
||
if (preg_match('/(?:创建|生成|建立).*?(?:插件|addon).*?[名为|叫|是]\s*([a-zA-Z_][a-zA-Z0-9_]*)/', $lowerPrompt, $matches)) {
|
||
$addonName = $matches[1];
|
||
$parsedData['addon'] = [
|
||
'name' => $addonName,
|
||
'options' => [
|
||
'title' => $addonName,
|
||
'description' => $addonName . '插件',
|
||
'author' => 'webman',
|
||
'version' => '1.0.0',
|
||
'requires' => '1.0.0'
|
||
]
|
||
];
|
||
}
|
||
}
|
||
|
||
// 解析菜单相关操作
|
||
if (strpos($lowerPrompt, 'menu') !== false || strpos($lowerPrompt, '菜单') !== false) {
|
||
if (!empty($parsedData['menu'])) {
|
||
$parsedData['menu']['description'] = '菜单权限';
|
||
}
|
||
}
|
||
|
||
// 解析CRUD相关操作
|
||
if (strpos($lowerPrompt, 'curd') !== false || strpos($lowerPrompt, 'crud') !== false || strpos($lowerPrompt, '增删改查') !== false) {
|
||
if (!empty($parsedData['curd'])) {
|
||
$parsedData['curd']['description'] = 'CRUD模块';
|
||
}
|
||
}
|
||
|
||
// 解析模块类型
|
||
if (strpos($lowerPrompt, 'admin') !== false || strpos($lowerPrompt, '后台') !== false) {
|
||
if (!empty($parsedData['controller'])) {
|
||
$parsedData['controller']['module'] = 'admin';
|
||
}
|
||
if (!empty($parsedData['js'])) {
|
||
$parsedData['js']['module'] = 'admin';
|
||
}
|
||
if (!empty($parsedData['view'])) {
|
||
$parsedData['view']['module'] = 'admin';
|
||
}
|
||
}
|
||
|
||
if (strpos($lowerPrompt, 'frontend') !== false || strpos($lowerPrompt, '前台') !== false) {
|
||
if (!empty($parsedData['controller'])) {
|
||
$parsedData['controller']['module'] = 'frontend';
|
||
}
|
||
if (!empty($parsedData['js'])) {
|
||
$parsedData['js']['module'] = 'frontend';
|
||
}
|
||
if (!empty($parsedData['view'])) {
|
||
$parsedData['view']['module'] = 'frontend';
|
||
}
|
||
}
|
||
|
||
if (strpos($lowerPrompt, 'api') !== false) {
|
||
if (!empty($parsedData['controller'])) {
|
||
$parsedData['controller']['module'] = 'api';
|
||
}
|
||
if (!empty($parsedData['js'])) {
|
||
$parsedData['js']['module'] = 'api';
|
||
}
|
||
if (!empty($parsedData['view'])) {
|
||
$parsedData['view']['module'] = 'api';
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 从提示词中提取字段信息
|
||
* @param string $prompt
|
||
* @return array
|
||
*/
|
||
private function extractFieldsFromPrompt(string $prompt): array
|
||
{
|
||
$fields = [];
|
||
|
||
// 常见的字段模式匹配
|
||
$fieldPatterns = [
|
||
// 用户相关字段
|
||
'user_id' => ['用户ID', 'user_id', '用户ID'],
|
||
'username' => ['用户名', 'username', '用户名称'],
|
||
'email' => ['邮箱', 'email', '邮件'],
|
||
'phone' => ['手机', 'phone', '电话', '手机号'],
|
||
'password' => ['密码', 'password'],
|
||
'nickname' => ['昵称', 'nickname', '昵名'],
|
||
'avatar' => ['头像', 'avatar', '照片'],
|
||
'gender' => ['性别', 'gender'],
|
||
'birthday' => ['生日', 'birthday', '出生日期'],
|
||
'address' => ['地址', 'address', '住址'],
|
||
'status' => ['状态', 'status'],
|
||
|
||
// 通用字段
|
||
'title' => ['标题', 'title', '名称'],
|
||
'content' => ['内容', 'content', '描述'],
|
||
'description' => ['描述', 'description', '说明'],
|
||
'price' => ['价格', 'price', '金额'],
|
||
'amount' => ['数量', 'amount', '数量'],
|
||
'category_id' => ['分类', 'category', '分类ID'],
|
||
'sort' => ['排序', 'sort', '顺序'],
|
||
'remark' => ['备注', 'remark', '说明'],
|
||
'memo' => ['备注', 'memo', '说明'],
|
||
|
||
// 时间相关字段
|
||
'created_at' => ['创建时间', 'created_at'],
|
||
'updated_at' => ['更新时间', 'updated_at'],
|
||
'deleted_at' => ['删除时间', 'deleted_at'],
|
||
'published_at' => ['发布时间', 'published_at'],
|
||
'expire_at' => ['过期时间', 'expire_at']
|
||
];
|
||
|
||
foreach ($fieldPatterns as $fieldName => $patterns) {
|
||
foreach ($patterns as $pattern) {
|
||
if (strpos($prompt, $pattern) !== false) {
|
||
$fields[] = $this->CreateFieldByType($fieldName);
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
|
||
// 如果没有找到字段,添加默认字段
|
||
if (empty($fields)) {
|
||
$fields = [
|
||
[
|
||
'name' => 'title',
|
||
'type' => 'varchar(255)',
|
||
'comment' => '标题',
|
||
'null' => false,
|
||
'default' => ''
|
||
],
|
||
[
|
||
'name' => 'content',
|
||
'type' => 'text',
|
||
'comment' => '内容',
|
||
'null' => true
|
||
],
|
||
[
|
||
'name' => 'status',
|
||
'type' => 'tinyint(1)',
|
||
'comment' => '状态:0=禁用,1=启用',
|
||
'null' => false,
|
||
'default' => 1
|
||
]
|
||
];
|
||
}
|
||
|
||
return $fields;
|
||
}
|
||
|
||
/**
|
||
* 根据字段名生成字段配置
|
||
* @param string $fieldName
|
||
* @return array
|
||
*/
|
||
private function CreateFieldByType(string $fieldName): array
|
||
{
|
||
$fieldConfigs = [
|
||
'username' => [
|
||
'name' => 'username',
|
||
'type' => 'varchar(50)',
|
||
'comment' => '用户名',
|
||
'null' => false,
|
||
'default' => ''
|
||
],
|
||
'email' => [
|
||
'name' => 'email',
|
||
'type' => 'varchar(100)',
|
||
'comment' => '邮箱',
|
||
'null' => false,
|
||
'default' => ''
|
||
],
|
||
'phone' => [
|
||
'name' => 'phone',
|
||
'type' => 'varchar(20)',
|
||
'comment' => '手机号',
|
||
'null' => true
|
||
],
|
||
'password' => [
|
||
'name' => 'password',
|
||
'type' => 'varchar(255)',
|
||
'comment' => '密码',
|
||
'null' => false,
|
||
'default' => ''
|
||
],
|
||
'nickname' => [
|
||
'name' => 'nickname',
|
||
'type' => 'varchar(50)',
|
||
'comment' => '昵称',
|
||
'null' => true
|
||
],
|
||
'avatar' => [
|
||
'name' => 'avatar',
|
||
'type' => 'varchar(255)',
|
||
'comment' => '头像',
|
||
'null' => true
|
||
],
|
||
'gender' => [
|
||
'name' => 'gender',
|
||
'type' => 'tinyint(1)',
|
||
'comment' => '性别:0=未知,1=男,2=女',
|
||
'null' => false,
|
||
'default' => 0
|
||
],
|
||
'birthday' => [
|
||
'name' => 'birthday',
|
||
'type' => 'date',
|
||
'comment' => '生日',
|
||
'null' => true
|
||
],
|
||
'address' => [
|
||
'name' => 'address',
|
||
'type' => 'text',
|
||
'comment' => '地址',
|
||
'null' => true
|
||
],
|
||
'status' => [
|
||
'name' => 'status',
|
||
'type' => 'tinyint(1)',
|
||
'comment' => '状态:0=禁用,1=启用',
|
||
'null' => false,
|
||
'default' => 1
|
||
],
|
||
'title' => [
|
||
'name' => 'title',
|
||
'type' => 'varchar(255)',
|
||
'comment' => '标题',
|
||
'null' => false,
|
||
'default' => ''
|
||
],
|
||
'content' => [
|
||
'name' => 'content',
|
||
'type' => 'text',
|
||
'comment' => '内容',
|
||
'null' => true
|
||
],
|
||
'description' => [
|
||
'name' => 'description',
|
||
'type' => 'text',
|
||
'comment' => '描述',
|
||
'null' => true
|
||
],
|
||
'price' => [
|
||
'name' => 'price',
|
||
'type' => 'decimal(10,2)',
|
||
'comment' => '价格',
|
||
'null' => false,
|
||
'default' => 0.00
|
||
],
|
||
'amount' => [
|
||
'name' => 'amount',
|
||
'type' => 'int(11)',
|
||
'comment' => '数量',
|
||
'null' => false,
|
||
'default' => 0
|
||
],
|
||
'category_id' => [
|
||
'name' => 'category_id',
|
||
'type' => 'int(11)',
|
||
'comment' => '分类ID',
|
||
'null' => false,
|
||
'default' => 0
|
||
],
|
||
'sort' => [
|
||
'name' => 'sort',
|
||
'type' => 'int(11)',
|
||
'comment' => '排序',
|
||
'null' => false,
|
||
'default' => 0
|
||
],
|
||
'remark' => [
|
||
'name' => 'remark',
|
||
'type' => 'varchar(255)',
|
||
'comment' => '备注',
|
||
'null' => true
|
||
],
|
||
'memo' => [
|
||
'name' => 'memo',
|
||
'type' => 'varchar(255)',
|
||
'comment' => '备注',
|
||
'null' => true
|
||
],
|
||
'published_at' => [
|
||
'name' => 'published_at',
|
||
'type' => 'int(11)',
|
||
'comment' => '发布时间',
|
||
'null' => true
|
||
],
|
||
'expire_at' => [
|
||
'name' => 'expire_at',
|
||
'type' => 'int(11)',
|
||
'comment' => '过期时间',
|
||
'null' => true
|
||
]
|
||
];
|
||
|
||
return $fieldConfigs[$fieldName] ?? [
|
||
'name' => $fieldName,
|
||
'type' => 'varchar(255)',
|
||
'comment' => $fieldName,
|
||
'null' => true
|
||
];
|
||
}
|
||
|
||
/**
|
||
* 从提示词中提取表注释
|
||
* @param string $prompt
|
||
* @return string
|
||
*/
|
||
private function extractTableComment(string $prompt): string
|
||
{
|
||
// 提取表注释的模式
|
||
$patterns = [
|
||
'/(?:用于|用来|存储|管理).*?(?:信息|数据|记录)/',
|
||
'/(?:.*?)(?:表|table)/',
|
||
'/(?:.*?)(?:管理|系统|模块)/'
|
||
];
|
||
|
||
foreach ($patterns as $pattern) {
|
||
if (preg_match($pattern, $prompt, $matches)) {
|
||
return trim($matches[0]) . '表';
|
||
}
|
||
}
|
||
|
||
return '';
|
||
}
|
||
|
||
/**
|
||
* 转换表名为控制器名
|
||
* @param string $tableName
|
||
* @return string
|
||
*/
|
||
private function convertTableNameToControllerName(string $tableName): string
|
||
{
|
||
return $this->convertTableNameToModelName($tableName) . 'Controller';
|
||
|
||
}
|
||
|
||
/**
|
||
* 转换表名为模型名
|
||
* @param string $tableName
|
||
* @return string
|
||
*/
|
||
private function convertTableNameToModelName(string $tableName): string
|
||
{
|
||
// 移除表前缀
|
||
$prefix = config('thinkorm.connections.mysql.prefix');
|
||
if (strpos($tableName, $prefix) === 0) {
|
||
$tableName = substr($tableName, strlen($prefix));
|
||
}
|
||
|
||
$tableName = str_replace(['-', '_'], ' ', strtolower($tableName));
|
||
$tableName = ucwords($tableName);
|
||
$tableName = str_replace(' ', '', $tableName);
|
||
return ucfirst($tableName);
|
||
}
|
||
|
||
/**
|
||
* 生成JS文件
|
||
* @param string $module 模块名称 (admin/api/frontend等)
|
||
* @param string $controller 控制器名称
|
||
* @param array $fields 字段信息 (可选)
|
||
* @param string $description 描述 (可选)
|
||
* @return array
|
||
*/
|
||
public function handleCreateJs(string $module, string $controller, array $fields = [], string $description = ''): array
|
||
{
|
||
try {
|
||
// 生成JS文件名
|
||
$jsFileName = strtolower($controller);
|
||
|
||
$jsPath = "plugin/admin/public/js/{$jsFileName}.js";
|
||
if ($module == 'frontend') {
|
||
$jsPath = "public/static/js/{$jsFileName}.js";
|
||
}
|
||
// 检查文件是否已存在
|
||
if (file_exists($jsPath)) {
|
||
return [
|
||
'success' => false,
|
||
'error' => "JS文件 {$jsPath} 已存在"
|
||
];
|
||
}
|
||
|
||
// 生成JS内容
|
||
$jsContent = $this->generateJsContent($module, $controller, $fields, $description);
|
||
|
||
// 确保目录存在
|
||
$dir = dirname($jsPath);
|
||
if (!is_dir($dir)) {
|
||
mkdir($dir, 0755, true);
|
||
}
|
||
|
||
// 写入文件
|
||
if (file_put_contents($jsPath, $jsContent)) {
|
||
Log::channel('mcp')->info("JS文件生成成功: {$jsPath}");
|
||
return [
|
||
'success' => true,
|
||
'message' => 'JS文件生成成功',
|
||
'file_path' => $jsPath,
|
||
'content' => $jsContent
|
||
];
|
||
} else {
|
||
throw new \Exception('文件写入失败');
|
||
}
|
||
|
||
} catch (\Exception $e) {
|
||
Log::channel('mcp')->error('JS文件生成错误: ' . $e->getMessage());
|
||
return [
|
||
'success' => false,
|
||
'error' => $e->getMessage()
|
||
];
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 生成API接口文件
|
||
* @param string $controller 控制器名称
|
||
* @param string $module 模块名称 (api等)
|
||
* @param array $fields 字段信息 (可选)
|
||
* @param string $description 描述 (可选)
|
||
* @return array
|
||
*/
|
||
public function handleCreateApi(string $controller, string $module = 'api', array $fields = [], string $description = ''): array
|
||
{
|
||
try {
|
||
// 生成API控制器类名
|
||
$controllerClass = ucfirst($controller);
|
||
$apiPath = "app/{$module}/controller/{$controllerClass}.php";
|
||
|
||
// 检查文件是否已存在
|
||
if (file_exists($apiPath)) {
|
||
return [
|
||
'success' => false,
|
||
'error' => "API文件 {$apiPath} 已存在"
|
||
];
|
||
}
|
||
|
||
// 生成API内容
|
||
$apiContent = $this->generateApiContent($module, $controllerClass, $fields, $description);
|
||
|
||
// 确保目录存在
|
||
$dir = dirname($apiPath);
|
||
if (!is_dir($dir)) {
|
||
mkdir($dir, 0755, true);
|
||
}
|
||
|
||
// 写入文件
|
||
if (file_put_contents($apiPath, $apiContent)) {
|
||
Log::channel('mcp')->info("API文件生成成功: {$apiPath}");
|
||
return [
|
||
'success' => true,
|
||
'message' => 'API文件生成成功',
|
||
'file_path' => $apiPath,
|
||
'content' => $apiContent
|
||
];
|
||
} else {
|
||
throw new \Exception('文件写入失败');
|
||
}
|
||
|
||
} catch (\Exception $e) {
|
||
Log::channel('mcp')->error('API文件生成错误: ' . $e->getMessage());
|
||
return [
|
||
'success' => false,
|
||
'error' => $e->getMessage()
|
||
];
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 生成JS文件内容
|
||
* @param string $module 模块名称
|
||
* @param string $controller 控制器名称
|
||
* @param array $fields 字段信息
|
||
* @param string $description 描述
|
||
* @return string
|
||
*/
|
||
private function generateJsContent(string $module, string $controller, array $fields = [], string $description = ''): string
|
||
{
|
||
$controllerLower = strtolower($controller);
|
||
$description = $description ?: $controller;
|
||
|
||
// 生成表格列配置
|
||
$jsCols = $this->generateJsCols($fields);
|
||
$tpl = 'app/mcp/tpl/js/' . $module . '.tpl';
|
||
if (!file_exists($tpl)) {
|
||
$tpl = 'app/mcp/tpl/js/default.tpl';
|
||
}
|
||
$content = view($tpl, [
|
||
'description' => $description,
|
||
'controllerClass' => $controller,
|
||
'controllerLower' => $controllerLower,
|
||
]);
|
||
|
||
return $content;
|
||
}
|
||
|
||
/**
|
||
* 生成API文件内容
|
||
* @param string $module 模块名称
|
||
* @param string $controllerClass 控制器类名
|
||
* @param array $fields 字段信息
|
||
* @param string $description 描述
|
||
* @return string
|
||
*/
|
||
private function generateApiContent(string $module, string $controllerClass, array $fields = [], string $description = ''): string
|
||
{
|
||
$description = $description ?: $controllerClass;
|
||
|
||
// 生成字段验证规则
|
||
$validationRules = $this->generateApiValidationRules($fields);
|
||
|
||
$tpl = 'app/mcp/tpl/controller/' . $module . '.tpl';
|
||
if (!file_exists($tpl)) {
|
||
$tpl = 'app/mcp/tpl/controller/default.tpl';
|
||
}
|
||
$content = view($tpl, [
|
||
'description' => $description,
|
||
'controllerClass' => $controllerClass,
|
||
]);
|
||
return $content;
|
||
}
|
||
|
||
/**
|
||
* 生成JS表格列配置
|
||
* @param array $fields 字段信息
|
||
* @return string
|
||
*/
|
||
private function generateJsCols(array $fields): string
|
||
{
|
||
if (empty($fields)) {
|
||
return " {field: 'title', title: '标题', width: 200},";
|
||
}
|
||
|
||
$cols = [];
|
||
foreach ($fields as $field) {
|
||
$fieldName = $field['name'] ?? '';
|
||
$fieldComment = $field['comment'] ?? $fieldName;
|
||
$fieldType = $field['type'] ?? 'varchar';
|
||
|
||
// 跳过系统字段
|
||
if (in_array($fieldName, ['id', 'created_at', 'updated_at', 'deleted_at'])) {
|
||
continue;
|
||
}
|
||
|
||
// 根据字段类型设置不同的显示方式
|
||
$width = 200;
|
||
$templet = '';
|
||
|
||
if (strpos($fieldType, 'text') !== false) {
|
||
$width = 300;
|
||
$templet = ",filter:'string'";
|
||
} elseif (strpos($fieldType, 'int') !== false) {
|
||
$width = 100;
|
||
$templet = ",filter:'number'";
|
||
} elseif (strpos($fieldType, 'datetime') !== false || strpos($fieldType, 'timestamp') !== false) {
|
||
$width = 180;
|
||
$templet = ", formatter:Table.api.formatter.datetime,filter:'datetime'";
|
||
} elseif (strpos($fieldType, 'tinyint') !== false) {
|
||
$width = 100;
|
||
$templet = ", formatter: Table.api.formatter.switch";
|
||
}
|
||
|
||
$cols[] = " {field: '{$fieldName}', title: '{$fieldComment}', width: {$width}{$templet},";
|
||
}
|
||
|
||
return implode("\n", $cols);
|
||
}
|
||
|
||
/**
|
||
* 生成JS请求配置
|
||
* @param string $controller 控制器名称
|
||
* @return string
|
||
*/
|
||
private function generateJsRequests(string $controller): string
|
||
{
|
||
return " index_url: '/app/admin/{$controller}/index',
|
||
add_url: '/app/admin/{$controller}/add',
|
||
edit_url: '/app/admin/{$controller}/edit',
|
||
del_url: '/app/admin/{$controller}/del',
|
||
multi_url: '/app/admin/{$controller}/multi',
|
||
table_url: '/app/admin/{$controller}/table',";
|
||
}
|
||
|
||
/**
|
||
* 生成API验证规则
|
||
* @param array $fields 字段信息
|
||
* @return string
|
||
*/
|
||
private function generateApiValidationRules(array $fields): string
|
||
{
|
||
if (empty($fields)) {
|
||
return "'title' => 'require|max:255',
|
||
'content' => 'require',";
|
||
}
|
||
|
||
$rules = [];
|
||
foreach ($fields as $field) {
|
||
$fieldName = $field['name'] ?? '';
|
||
$fieldType = $field['type'] ?? 'varchar';
|
||
|
||
if (empty($fieldName) || in_array($fieldName, ['id', 'created_at', 'updated_at', 'deleted_at'])) {
|
||
continue;
|
||
}
|
||
|
||
$rule = "'{$fieldName}' => '";
|
||
|
||
// 根据字段类型设置验证规则
|
||
if (strpos($fieldType, 'varchar') !== false) {
|
||
$maxLength = 255;
|
||
if (preg_match('/varchar\((\d+)\)/', $fieldType, $matches)) {
|
||
$maxLength = $matches[1];
|
||
}
|
||
$rule .= "max:{$maxLength}";
|
||
} elseif (strpos($fieldType, 'text') !== false) {
|
||
$rule .= "require";
|
||
} elseif (strpos($fieldType, 'int') !== false) {
|
||
$rule .= "number";
|
||
} elseif (strpos($fieldType, 'datetime') !== false || strpos($fieldType, 'timestamp') !== false) {
|
||
$rule .= "date";
|
||
} else {
|
||
$rule .= "require";
|
||
}
|
||
|
||
$rule .= "'";
|
||
$rules[] = $rule;
|
||
}
|
||
|
||
return implode(",\n ", $rules);
|
||
}
|
||
|
||
/**
|
||
* 生成视图文件
|
||
* @param string $module 模块名称 (admin/api/frontend等)
|
||
* @param string $controller 控制器名称
|
||
* @param array $fields 字段信息 (可选)
|
||
* @param string $description 描述 (可选)
|
||
* @return array
|
||
*/
|
||
public function handleCreateView(string $module, string $controller, array $fields = [], string $description = ''): array
|
||
{
|
||
try {
|
||
// 生成视图文件名
|
||
$viewFileName = strtolower($controller);
|
||
$viewPath = "app/{$module}/view/{$viewFileName}";
|
||
if ($module == 'admin') {
|
||
$viewPath = "plugin/{$module}/app/view/{$viewFileName}";
|
||
}
|
||
|
||
// 检查目录是否已存在
|
||
if (is_dir($viewPath)) {
|
||
return [
|
||
'success' => false,
|
||
'error' => "视图目录 {$viewPath} 已存在"
|
||
];
|
||
}
|
||
|
||
// 生成视图内容
|
||
$viewFiles = $this->generateViewFiles($module, $controller, $fields, $description);
|
||
|
||
// 确保目录存在
|
||
if (!is_dir($viewPath)) {
|
||
mkdir($viewPath, 0755, true);
|
||
}
|
||
|
||
$generatedFiles = [];
|
||
foreach ($viewFiles as $viewFile) {
|
||
$filePath = $viewPath . '/' . $viewFile['name'];
|
||
if (file_put_contents($filePath, $viewFile['content'])) {
|
||
$generatedFiles[] = $filePath;
|
||
Log::channel('mcp')->info("视图文件生成成功: {$filePath}");
|
||
}
|
||
}
|
||
|
||
if (!empty($generatedFiles)) {
|
||
return [
|
||
'success' => true,
|
||
'message' => '视图文件生成成功',
|
||
'file_paths' => $generatedFiles,
|
||
'files_count' => count($generatedFiles)
|
||
];
|
||
} else {
|
||
throw new \Exception('视图文件写入失败');
|
||
}
|
||
|
||
} catch (\Exception $e) {
|
||
Log::channel('mcp')->error('视图文件生成错误: ' . $e->getMessage());
|
||
return [
|
||
'success' => false,
|
||
'error' => $e->getMessage()
|
||
];
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 生成视图文件
|
||
* @param string $module 模块名称
|
||
* @param string $controller 控制器名称
|
||
* @param array $fields 字段信息
|
||
* @param string $description 描述
|
||
* @return array
|
||
*/
|
||
private function generateViewFiles(string $module, string $controller, array $fields = [], string $description = ''): array
|
||
{
|
||
$controllerLower = strtolower($controller);
|
||
$description = $description ?: $controller;
|
||
|
||
$viewFiles = [];
|
||
|
||
// 生成 index.html
|
||
$indexContent = $this->generateIndexView($module, $controllerLower, $fields, $description);
|
||
$viewFiles[] = [
|
||
'name' => 'index.html',
|
||
'content' => $indexContent
|
||
];
|
||
|
||
// 生成 add.html
|
||
$addContent = $this->generateAddView($module, $controllerLower, $fields, $description);
|
||
$viewFiles[] = [
|
||
'name' => 'update.html',
|
||
'content' => $addContent
|
||
];
|
||
|
||
// 生成 edit.html
|
||
/* $viewFiles[] = [
|
||
'name' => 'edit.html',
|
||
'content' => $addContent
|
||
]; */
|
||
|
||
return $viewFiles;
|
||
}
|
||
|
||
/**
|
||
* 生成index视图
|
||
* @param string $module 模块名称
|
||
* @param string $controller 控制器名称
|
||
* @param array $fields 字段信息
|
||
* @param string $description 描述
|
||
* @return string
|
||
*/
|
||
private function generateIndexView(string $module, string $controller, array $fields = [], string $description = ''): string
|
||
{
|
||
$content = view('app/mcp/tpl/view/index.tpl', [
|
||
'controller' => $controller,
|
||
'module' => $module,
|
||
'fields' => $fields,
|
||
'description' => $description,
|
||
]);
|
||
|
||
return $content;
|
||
}
|
||
|
||
/**
|
||
* 生成add视图
|
||
* @param string $module 模块名称
|
||
* @param string $controller 控制器名称
|
||
* @param array $fields 字段信息
|
||
* @param string $description 描述
|
||
* @return string
|
||
*/
|
||
private function generateAddView(string $module, string $controller, array $fields = [], string $description = ''): string
|
||
{
|
||
foreach ($fields as $k => $field) {
|
||
if (in_array($field['name'], ['id', 'created_at', 'updated_at', 'deleted_at'])) {
|
||
continue;
|
||
}
|
||
$fieldName = $field['name'];
|
||
$fieldComment = $field['comment'] ?? $fieldName;
|
||
$fieldType = $field['type'] ?? 'varchar';
|
||
if ($this->shouldUseSelect($fieldName, $fieldComment, $fieldType)) {
|
||
// 下拉选择框 - 根据字段名称、注释、类型判断
|
||
$fields[$k]['selectOptions'] = $this->generateSelectOptions($fieldName, $fieldComment);
|
||
$fields[$k]['multiple'] = $this->isMultipleSelect($fieldComment);
|
||
$fields[$k]['type'] = 'select';
|
||
}
|
||
if (strpos($fieldType, 'tinyint') !== false && in_array($fieldName, ['status', 'is_show', 'is_enable'])) {
|
||
$fields[$k]['type'] = 'radio';
|
||
$fields[$k]['selectOptions'] = $this->generateSelectOptions($fieldName, $fieldComment);
|
||
|
||
}
|
||
}
|
||
|
||
$content = view('app/mcp/tpl/view/update.tpl', [
|
||
'controller' => $controller,
|
||
'module' => $module,
|
||
'fields' => $fields,
|
||
'description' => $description,
|
||
'action' => 'update',
|
||
]);
|
||
$content = "<form class=\"layui-form\" lay-filter=\"form\">\n";
|
||
|
||
return $content;
|
||
}
|
||
|
||
/**
|
||
* 判断是否应该使用下拉选择框
|
||
* @param string $fieldName 字段名称
|
||
* @param string $fieldComment 字段注释
|
||
* @param string $fieldType 字段类型
|
||
* @return bool
|
||
*/
|
||
private function shouldUseSelect(string $fieldName, string $fieldComment, string $fieldType): bool
|
||
{
|
||
// 1. 根据字段注释判断
|
||
$commentKeywords = [
|
||
'选择',
|
||
'类型',
|
||
'分类',
|
||
'等级',
|
||
'级别',
|
||
'状态',
|
||
'方式',
|
||
'模式',
|
||
'种类',
|
||
'下拉',
|
||
'列表',
|
||
'枚举',
|
||
'选项',
|
||
'菜单',
|
||
'角色',
|
||
'权限',
|
||
'部门',
|
||
'省份',
|
||
'城市',
|
||
'地区',
|
||
'国家',
|
||
'行业',
|
||
'职业',
|
||
'学历',
|
||
'婚姻',
|
||
'1:',
|
||
'2:',
|
||
'3:',
|
||
'|',
|
||
',',
|
||
',',
|
||
'/',
|
||
'\\', // 包含选项分隔符的注释
|
||
];
|
||
|
||
foreach ($commentKeywords as $keyword) {
|
||
if (strpos($fieldComment, $keyword) !== false) {
|
||
return true;
|
||
}
|
||
}
|
||
|
||
// 2. 根据字段名称判断
|
||
$nameKeywords = [
|
||
'_id',
|
||
'type',
|
||
'category',
|
||
'level',
|
||
'grade',
|
||
'status',
|
||
'state',
|
||
'kind',
|
||
'class',
|
||
'group',
|
||
'dept',
|
||
'role',
|
||
'auth',
|
||
'mode',
|
||
'method',
|
||
'way',
|
||
'style',
|
||
'format',
|
||
'gender',
|
||
'sex',
|
||
'province',
|
||
'city',
|
||
'area',
|
||
'region',
|
||
'country',
|
||
'nation'
|
||
];
|
||
|
||
foreach ($nameKeywords as $keyword) {
|
||
if (strpos($fieldName, $keyword) !== false) {
|
||
return true;
|
||
}
|
||
}
|
||
|
||
// 3. 根据字段类型判断
|
||
if (strpos($fieldType, 'enum') !== false) {
|
||
return true;
|
||
}
|
||
|
||
// 4. 小范围的整数字段可能是选择字段
|
||
if (strpos($fieldType, 'tinyint') !== false || strpos($fieldType, 'smallint') !== false) {
|
||
// 排除明确的布尔字段(is_开头的字段)
|
||
if (strpos($fieldName, 'is_') !== 0 && !in_array($fieldName, ['status', 'sort', 'weigh', 'createtime', 'updatetime'])) {
|
||
return true;
|
||
}
|
||
}
|
||
|
||
return false;
|
||
}
|
||
|
||
/**
|
||
* 判断是否为多选字段
|
||
* @param string $fieldComment 字段注释
|
||
* @return bool
|
||
*/
|
||
private function isMultipleSelect(string $fieldComment): bool
|
||
{
|
||
$multipleKeywords = [
|
||
'多选',
|
||
'复选',
|
||
'多个',
|
||
'批量',
|
||
'多种',
|
||
'可选多个',
|
||
'可多选',
|
||
'标签',
|
||
'爱好',
|
||
'技能',
|
||
'特长',
|
||
'兴趣',
|
||
'权限',
|
||
'角色组'
|
||
];
|
||
|
||
foreach ($multipleKeywords as $keyword) {
|
||
if (strpos($fieldComment, $keyword) !== false) {
|
||
return true;
|
||
}
|
||
}
|
||
|
||
return false;
|
||
}
|
||
|
||
/**
|
||
* 生成下拉选择框的选项
|
||
* @param string $fieldName 字段名称
|
||
* @param string $fieldComment 字段注释
|
||
* @return string 选项数组的字符串表示
|
||
*/
|
||
private function generateSelectOptions(string $fieldName, string $fieldComment): string
|
||
{
|
||
// 1. 首先尝试从字段注释中解析选项
|
||
$parsedOptions = $this->parseOptionsFromComment($fieldComment);
|
||
if (!empty($parsedOptions)) {
|
||
return $parsedOptions;
|
||
}
|
||
|
||
// 2. 根据字段名称智能生成选项
|
||
if (strpos($fieldName, 'status') !== false) {
|
||
// 状态字段
|
||
return "['0'=>'禁用', '1'=>'启用']";
|
||
} elseif (strpos($fieldName, 'is_') === 0) {
|
||
// 布尔类型字段 (is_show, is_enable等)
|
||
return "['0'=>'否', '1'=>'是']";
|
||
} elseif (strpos($fieldName, 'gender') !== false || strpos($fieldName, 'sex') !== false) {
|
||
// 性别字段
|
||
return "['0'=>'保密', '1'=>'男', '2'=>'女']";
|
||
} elseif (strpos($fieldName, 'type') !== false) {
|
||
// 类型字段 - 根据具体用途生成
|
||
if (strpos($fieldName, 'user') !== false) {
|
||
return "['1'=>'普通用户', '2'=>'VIP用户', '3'=>'超级用户']";
|
||
} elseif (strpos($fieldName, 'content') !== false || strpos($fieldName, 'article') !== false) {
|
||
return "['1'=>'文章', '2'=>'图片', '3'=>'视频']";
|
||
} elseif (strpos($fieldName, 'pay') !== false) {
|
||
return "['1'=>'微信支付', '2'=>'支付宝', '3'=>'银行卡']";
|
||
} else {
|
||
return "['1'=>'类型一', '2'=>'类型二', '3'=>'类型三']";
|
||
}
|
||
} elseif (strpos($fieldName, 'level') !== false) {
|
||
// 等级字段
|
||
return "['1'=>'初级', '2'=>'中级', '3'=>'高级', '4'=>'专家级']";
|
||
} elseif (strpos($fieldName, 'grade') !== false) {
|
||
// 等级字段
|
||
return "['A'=>'A级', 'B'=>'B级', 'C'=>'C级', 'D'=>'D级']";
|
||
} elseif (strpos($fieldName, 'priority') !== false) {
|
||
// 优先级字段
|
||
return "['1'=>'低', '2'=>'中', '3'=>'高', '4'=>'紧急']";
|
||
} elseif (strpos($fieldName, 'category') !== false) {
|
||
// 分类字段 - 提供更通用的选项
|
||
return "[''=>'请选择分类', '1'=>'默认分类', '2'=>'热门分类', '3'=>'推荐分类']";
|
||
} elseif (strpos($fieldName, 'admin_id') !== false) {
|
||
// 管理员字段 - 系统表
|
||
return "\\think\\facade\\Db::name('admin')->column('username', 'id') ?: [''=>'请选择管理员']";
|
||
} elseif (strpos($fieldName, 'member_id') !== false) {
|
||
// 会员字段 - 系统表
|
||
return "\\think\\facade\\Db::name('member')->column('username', 'id') ?: [''=>'请选择会员']";
|
||
} elseif (strpos($fieldName, 'user_id') !== false) {
|
||
// 用户字段 - 通用处理
|
||
return "\\think\\facade\\Db::name('user')->column('username', 'id') ?: [''=>'请选择用户']";
|
||
} elseif (strpos($fieldName, 'role_id') !== false) {
|
||
// 角色字段 - 权限表
|
||
return "\\think\\facade\\Db::name('auth_role')->column('name', 'id') ?: [''=>'请选择角色']";
|
||
} elseif (strpos($fieldName, 'dept_id') !== false || strpos($fieldName, 'department_id') !== false) {
|
||
// 部门字段
|
||
return "\\think\\facade\\Db::name('dept')->column('name', 'id') ?: [''=>'请选择部门']";
|
||
} elseif (strpos($fieldName, 'parent_id') !== false || strpos($fieldName, 'pid') !== false) {
|
||
// 父级字段 - 提供静态选项避免表不存在的问题
|
||
return "['0'=>'顶级分类', ''=>'请选择上级分类']";
|
||
} elseif (strpos($fieldName, 'province') !== false || strpos($fieldName, 'city') !== false || strpos($fieldName, 'area') !== false) {
|
||
// 地区字段 - 提供静态选项
|
||
if (strpos($fieldName, 'province') !== false) {
|
||
return "['11'=>'北京市', '12'=>'天津市', '13'=>'河北省', '21'=>'辽宁省', '31'=>'上海市', '32'=>'江苏省', '44'=>'广东省']";
|
||
} elseif (strpos($fieldName, 'city') !== false) {
|
||
return "['1101'=>'东城区', '1102'=>'西城区', '1103'=>'朝阳区', '1104'=>'丰台区', '1105'=>'石景山区']";
|
||
} else {
|
||
return "['110101'=>'东华门街道', '110102'=>'景山街道', '110103'=>'交道口街道']";
|
||
}
|
||
} elseif (strpos($fieldName, 'sort') !== false || strpos($fieldName, 'order') !== false) {
|
||
// 排序字段
|
||
return "['1'=>'1', '10'=>'10', '50'=>'50', '100'=>'100', '999'=>'999']";
|
||
} elseif (strpos($fieldName, 'state') !== false) {
|
||
// 状态字段
|
||
return "['0'=>'待处理', '1'=>'处理中', '2'=>'已完成', '3'=>'已取消']";
|
||
} else {
|
||
// 默认选项
|
||
return "[''=>'请选择{$fieldComment}', '1'=>'选项一', '2'=>'选项二', '3'=>'选项三']";
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 从字段注释中解析选项
|
||
* @param string $comment 字段注释
|
||
* @return string 解析出的选项数组字符串,如果解析失败返回空字符串
|
||
*/
|
||
private function parseOptionsFromComment(string $comment): string
|
||
{
|
||
if (empty($comment)) {
|
||
return '';
|
||
}
|
||
|
||
// 解析模式1: "sex=1:男,2:女,3:未知" 或 "sex=1:男|2:女|3:未知" 或 "status=[1:可用,0:不可用]" 或 "status=(1:可用,0:不可用)"
|
||
// 先提取等号后面的部分,如果没有等号则使用整个注释
|
||
$commentPart = $comment;
|
||
if (preg_match('/^[^=]*=(.+)$/', $comment, $eqMatch)) {
|
||
$commentPart = $eqMatch[1];
|
||
}
|
||
|
||
// 匹配 数字:值 的格式
|
||
if (preg_match_all('/(\d+)\s*[::]\s*([^,|)\]()\s]+)/', $commentPart, $matches, PREG_SET_ORDER)) {
|
||
$options = [];
|
||
foreach ($matches as $match) {
|
||
$key = trim($match[1]);
|
||
$value = trim($match[2]);
|
||
// 清理值中的特殊字符
|
||
$value = preg_replace('/[()()\[\]【】]/', '', $value);
|
||
$options[] = "'{$key}'=>'{$value}'";
|
||
}
|
||
if (!empty($options)) {
|
||
return '[' . implode(', ', $options) . ']';
|
||
}
|
||
}
|
||
|
||
// 解析模式2: "性别=男,女,未知" 或 "类型=[选项1,选项2]" 或 "选项:(男|女|未知)"
|
||
if (preg_match('/[,|,]/', $comment)) {
|
||
// 先提取选项部分(支持等号、冒号、括号等分隔符)
|
||
$optionsPart = $comment;
|
||
|
||
// 处理等号分隔的格式: "性别=男,女"
|
||
if (preg_match('/^[^=]*=\s*[(\[(【]?([^)\])】]+)[)\])】]?$/', $comment, $eqMatch)) {
|
||
$optionsPart = $eqMatch[1];
|
||
}
|
||
// 处理冒号分隔的格式: "选项:[男,女]" 或 "选项:(男,女)"
|
||
elseif (preg_match('/[::]\s*[(\[(【]?([^)\])】]+)[)\])】]?$/', $comment, $colonMatch)) {
|
||
$optionsPart = $colonMatch[1];
|
||
}
|
||
|
||
// 分割选项
|
||
$items = preg_split('/[,|,、]/', $optionsPart);
|
||
$options = [];
|
||
foreach ($items as $index => $item) {
|
||
$item = trim($item);
|
||
// 清理特殊字符
|
||
$item = preg_replace('/[()()\[\]【】]/', '', $item);
|
||
|
||
if (!empty($item) && !preg_match('/^\d+$/', $item)) { // 排除纯数字和空值
|
||
$key = $index + 1;
|
||
$options[] = "'{$key}'=>'{$item}'";
|
||
}
|
||
}
|
||
if (count($options) > 1) {
|
||
return '[' . implode(', ', $options) . ']';
|
||
}
|
||
}
|
||
|
||
// 解析模式3: "选择类型:1-普通 2-VIP 3-超级" 或 "状态=1-启用 0-禁用"
|
||
if (preg_match_all('/(\d+)\s*[-—=]\s*([^\s,|]+)/', $comment, $matches, PREG_SET_ORDER)) {
|
||
$options = [];
|
||
foreach ($matches as $match) {
|
||
$key = trim($match[1]);
|
||
$value = trim($match[2]);
|
||
// 清理值中的特殊字符
|
||
$value = preg_replace('/[()()\[\]【】]/', '', $value);
|
||
$options[] = "'{$key}'=>'{$value}'";
|
||
}
|
||
if (!empty($options)) {
|
||
return '[' . implode(', ', $options) . ']';
|
||
}
|
||
}
|
||
|
||
// 解析模式4: 包含"或"的表达式 "男或女" "是或否" "启用或禁用"
|
||
if (preg_match('/(.+?)或(.+)/', $comment, $matches)) {
|
||
$option1 = trim($matches[1]);
|
||
$option2 = trim($matches[2]);
|
||
// 清理特殊字符
|
||
$option1 = preg_replace('/[()()\[\]【】=:]/', '', $option1);
|
||
$option2 = preg_replace('/[()()\[\]【】]/', '', $option2);
|
||
|
||
// 如果是状态类的,使用0/1作为键值
|
||
if (preg_match('/(启用|开启|打开|显示|是)/', $option1) || preg_match('/(禁用|关闭|隐藏|否)/', $option2)) {
|
||
return "['1'=>'{$option1}', '0'=>'{$option2}']";
|
||
} else {
|
||
return "['1'=>'{$option1}', '2'=>'{$option2}']";
|
||
}
|
||
}
|
||
|
||
// 解析模式5: 枚举值格式 "enum('male','female','unknown')" 或 "ENUM(1,2,3)"
|
||
if (preg_match('/enum\s*\(\s*([^)]+)\s*\)/i', $comment, $matches)) {
|
||
$enumValues = $matches[1];
|
||
// 移除引号并分割
|
||
$items = preg_split('/[,,]/', $enumValues);
|
||
$options = [];
|
||
foreach ($items as $index => $item) {
|
||
$item = trim($item, " '\"");
|
||
if (!empty($item)) {
|
||
$key = is_numeric($item) ? $item : ($index + 1);
|
||
$options[] = "'{$key}'=>'{$item}'";
|
||
}
|
||
}
|
||
if (!empty($options)) {
|
||
return '[' . implode(', ', $options) . ']';
|
||
}
|
||
}
|
||
|
||
return '';
|
||
}
|
||
|
||
/**
|
||
* 处理其他操作
|
||
* @param string $prompt 自然语言描述
|
||
* @return array
|
||
*/
|
||
private function handleOtherOperation(string $prompt): array
|
||
{
|
||
$lowerPrompt = strtolower($prompt);
|
||
$results = [];
|
||
|
||
// 处理数据库查询操作
|
||
if (strpos($lowerPrompt, '查询') !== false || strpos($lowerPrompt, 'select') !== false) {
|
||
$results['db_query'] = [
|
||
'success' => true,
|
||
'message' => '检测到数据库查询操作',
|
||
'suggestion' => '请使用 db-query 工具执行数据库查询'
|
||
];
|
||
}
|
||
|
||
// 处理系统配置操作
|
||
if (strpos($lowerPrompt, '配置') !== false || strpos($lowerPrompt, 'config') !== false) {
|
||
$results['sys_config'] = [
|
||
'success' => true,
|
||
'message' => '检测到系统配置操作',
|
||
'suggestion' => '请使用 sys-config 工具获取系统配置'
|
||
];
|
||
}
|
||
|
||
// 处理日志操作
|
||
if (strpos($lowerPrompt, '日志') !== false || strpos($lowerPrompt, 'log') !== false) {
|
||
$results['write_log'] = [
|
||
'success' => true,
|
||
'message' => '检测到日志操作',
|
||
'suggestion' => '请使用 write-log 工具写入系统日志'
|
||
];
|
||
}
|
||
|
||
// 处理文件操作
|
||
if (strpos($lowerPrompt, '文件') !== false || strpos($lowerPrompt, 'file') !== false) {
|
||
$results['file_operation'] = [
|
||
'success' => true,
|
||
'message' => '检测到文件操作',
|
||
'suggestion' => '请使用 file-operation 工具进行文件读写操作'
|
||
];
|
||
}
|
||
|
||
// 处理用户管理操作
|
||
if (strpos($lowerPrompt, '用户') !== false || strpos($lowerPrompt, 'user') !== false) {
|
||
$results['user_management'] = [
|
||
'success' => true,
|
||
'message' => '检测到用户管理操作',
|
||
'suggestion' => '请使用 user-management 工具进行用户管理'
|
||
];
|
||
}
|
||
|
||
// 处理系统信息操作
|
||
if (strpos($lowerPrompt, '系统信息') !== false || strpos($lowerPrompt, 'system') !== false) {
|
||
$results['system_info'] = [
|
||
'success' => true,
|
||
'message' => '检测到系统信息操作',
|
||
'suggestion' => '请使用 system-info 工具获取系统运行信息'
|
||
];
|
||
}
|
||
|
||
// 处理控制器生成操作
|
||
if (strpos($lowerPrompt, '控制器') !== false || strpos($lowerPrompt, 'controller') !== false) {
|
||
$results['controller'] = [
|
||
'success' => true,
|
||
'message' => '检测到控制器生成操作',
|
||
'suggestion' => '请使用 controller 工具生成控制器文件'
|
||
];
|
||
}
|
||
|
||
// 处理模型生成操作
|
||
if (strpos($lowerPrompt, '模型') !== false || strpos($lowerPrompt, 'model') !== false) {
|
||
$results['model'] = [
|
||
'success' => true,
|
||
'message' => '检测到模型生成操作',
|
||
'suggestion' => '请使用 model 工具生成模型文件'
|
||
];
|
||
}
|
||
|
||
// 处理数据库表创建操作
|
||
if (strpos($lowerPrompt, '数据库表') !== false || strpos($lowerPrompt, 'table') !== false) {
|
||
$results['table'] = [
|
||
'success' => true,
|
||
'message' => '检测到数据库表创建操作',
|
||
'suggestion' => '请使用 table 工具创建数据库表'
|
||
];
|
||
}
|
||
|
||
// 处理插件操作
|
||
if (strpos($lowerPrompt, '插件') !== false || strpos($lowerPrompt, 'addon') !== false) {
|
||
$results['addon'] = [
|
||
'success' => true,
|
||
'message' => '检测到插件操作',
|
||
'suggestion' => '请使用 addon 工具进行插件管理'
|
||
];
|
||
}
|
||
|
||
// 处理菜单操作
|
||
if (strpos($lowerPrompt, '菜单') !== false || strpos($lowerPrompt, 'menu') !== false) {
|
||
$results['menu'] = [
|
||
'success' => true,
|
||
'message' => '检测到菜单操作',
|
||
'suggestion' => '请使用 menu 工具进行菜单管理'
|
||
];
|
||
}
|
||
|
||
// 处理CRUD操作
|
||
if (strpos($lowerPrompt, 'crud') !== false || strpos($lowerPrompt, '增删改查') !== false) {
|
||
$results['curd'] = [
|
||
'success' => true,
|
||
'message' => '检测到CRUD操作',
|
||
'suggestion' => '请使用 curd 工具生成CRUD模块'
|
||
];
|
||
}
|
||
|
||
// 处理JS文件生成操作
|
||
if (strpos($lowerPrompt, 'js') !== false || strpos($lowerPrompt, 'javascript') !== false || strpos($lowerPrompt, '前端') !== false) {
|
||
$results['js'] = [
|
||
'success' => true,
|
||
'message' => '检测到JS文件生成操作',
|
||
'suggestion' => '请使用 js 工具生成前端JS文件'
|
||
];
|
||
}
|
||
|
||
// 处理API接口生成操作
|
||
if (strpos($lowerPrompt, 'api') !== false || strpos($lowerPrompt, '接口') !== false) {
|
||
$results['api'] = [
|
||
'success' => true,
|
||
'message' => '检测到API接口生成操作',
|
||
'suggestion' => '请使用 api 工具生成API接口文件'
|
||
];
|
||
}
|
||
|
||
// 处理视图文件生成操作
|
||
if (strpos($lowerPrompt, '视图') !== false || strpos($lowerPrompt, 'view') !== false || strpos($lowerPrompt, '页面') !== false) {
|
||
$results['view'] = [
|
||
'success' => true,
|
||
'message' => '检测到视图文件生成操作',
|
||
'suggestion' => '请使用 view 工具生成视图文件'
|
||
];
|
||
}
|
||
|
||
// 处理webman命令执行操作
|
||
if (
|
||
strpos($lowerPrompt, 'webman') !== false || strpos($lowerPrompt, '命令') !== false ||
|
||
strpos($lowerPrompt, 'command') !== false || strpos($lowerPrompt, '执行') !== false ||
|
||
strpos($lowerPrompt, 'start') !== false || strpos($lowerPrompt, 'stop') !== false ||
|
||
strpos($lowerPrompt, 'build') !== false || strpos($lowerPrompt, 'plugin') !== false
|
||
) {
|
||
$results['think-command'] = [
|
||
'success' => true,
|
||
'message' => '检测到webman命令执行操作',
|
||
'suggestion' => '请使用 think-command 工具执行webman框架命令,支持的命令包括:start、stop、build、plugin:install、queue:work等'
|
||
];
|
||
}
|
||
|
||
// 如果没有匹配到任何操作,返回通用建议
|
||
if (empty($results)) {
|
||
$results['general'] = [
|
||
'success' => false,
|
||
'message' => '未能识别具体的操作类型',
|
||
'suggestion' => '请尝试以下操作:' . "\n" .
|
||
'- 创建数据库表:包含表名和字段信息' . "\n" .
|
||
'- 生成控制器:指定模块和控制器名称' . "\n" .
|
||
'- 生成模型:指定模型名称和字段信息' . "\n" .
|
||
'- 生成JS文件:指定模块和控制器名称' . "\n" .
|
||
'- 生成API接口:指定模块和控制器名称' . "\n" .
|
||
'- 生成视图文件:指定模块和控制器名称' . "\n" .
|
||
'- 创建插件:指定插件名称' . "\n" .
|
||
'- 创建菜单:指定菜单信息' . "\n" .
|
||
'- 生成CRUD模块:指定表名和字段信息' . "\n" .
|
||
'- 数据库查询:使用SELECT语句' . "\n" .
|
||
'- 系统配置:获取系统配置信息' . "\n" .
|
||
'- 文件操作:进行文件读写操作' . "\n" .
|
||
'- 用户管理:进行用户相关操作' . "\n" .
|
||
'- 系统信息:获取系统运行信息' . "\n" .
|
||
'- webman命令:执行框架内置命令(start、stop、build、plugin等)'
|
||
];
|
||
}
|
||
|
||
return $results;
|
||
}
|
||
|
||
/**
|
||
* 执行webman框架命令
|
||
* @param string $command 命令名称
|
||
* @param array $params 命令参数 (可选)
|
||
* @param array $options 命令选项 (可选)
|
||
* @return array
|
||
*/
|
||
public function handleThinkCommand(string $command, array $params = [], array $options = []): array
|
||
{
|
||
try {
|
||
// 验证命令是否为安全的内置命令
|
||
$allowedCommands = [
|
||
// webman 基础命令
|
||
'start',
|
||
'stop',
|
||
'restart',
|
||
'reload',
|
||
'status',
|
||
'help',
|
||
'version',
|
||
// webman 构建命令
|
||
'build',
|
||
'build:phar',
|
||
'build:bin',
|
||
// webman 插件命令
|
||
'plugin:install',
|
||
'plugin:uninstall',
|
||
'plugin:list',
|
||
'plugin:enable',
|
||
'plugin:disable',
|
||
// webman 队列命令
|
||
'queue:work',
|
||
'queue:failed',
|
||
'queue:retry',
|
||
'queue:flush',
|
||
// webman 日志命令
|
||
'log:clear',
|
||
'log:tail',
|
||
// webman 缓存命令
|
||
'cache:clear',
|
||
'cache:clear:redis',
|
||
// webman 路由命令
|
||
'route:list',
|
||
'route:cache',
|
||
// webman 配置命令
|
||
'config:cache',
|
||
'config:clear',
|
||
// webman 数据库命令
|
||
'db:seed',
|
||
'db:migrate',
|
||
'db:rollback',
|
||
|
||
// 自定义 MCP 命令
|
||
'mcp:start',
|
||
'mcp:stop',
|
||
'mcp:status',
|
||
'mcp:test'
|
||
];
|
||
|
||
if (!in_array($command, $allowedCommands)) {
|
||
return [
|
||
'success' => false,
|
||
'message' => '不支持的命令或命令不安全',
|
||
'allowed_commands' => $allowedCommands
|
||
];
|
||
}
|
||
|
||
// 构建完整的命令(只支持 webman 命令)
|
||
$fullCommand = 'php webman ' . $command;
|
||
|
||
// 添加参数
|
||
if (!empty($params)) {
|
||
foreach ($params as $param) {
|
||
$fullCommand .= ' ' . escapeshellarg($param);
|
||
}
|
||
}
|
||
|
||
// 添加选项
|
||
if (!empty($options)) {
|
||
foreach ($options as $option => $value) {
|
||
if (is_numeric($option)) {
|
||
// 简单选项,如 --verbose
|
||
$fullCommand .= ' --' . $value;
|
||
} else {
|
||
// 带值选项,如 --name=value
|
||
$fullCommand .= ' --' . $option . '=' . escapeshellarg($value);
|
||
}
|
||
}
|
||
}
|
||
|
||
// 切换到项目根目录执行命令
|
||
$rootPath = base_path();
|
||
$originalDir = getcwd();
|
||
|
||
if ($originalDir !== $rootPath) {
|
||
chdir($rootPath);
|
||
}
|
||
|
||
// 执行命令并捕获输出
|
||
$output = [];
|
||
$returnCode = 0;
|
||
exec($fullCommand . ' 2>&1', $output, $returnCode);
|
||
|
||
// 恢复原始目录
|
||
if ($originalDir !== $rootPath) {
|
||
chdir($originalDir);
|
||
}
|
||
|
||
// 记录命令执行日志
|
||
$this->handleWriteLog("执行 webman 命令: {$fullCommand}", 'info', [
|
||
'return_code' => $returnCode,
|
||
'output_lines' => count($output)
|
||
]);
|
||
|
||
return [
|
||
'success' => $returnCode === 0,
|
||
'message' => $returnCode === 0 ? '命令执行成功' : '命令执行失败',
|
||
'command' => $fullCommand,
|
||
'return_code' => $returnCode,
|
||
'output' => implode("\n", $output),
|
||
'output_lines' => $output
|
||
];
|
||
|
||
} catch (\Exception $e) {
|
||
$this->handleWriteLog("执行 webman 命令时出错: " . $e->getMessage(), 'error', [
|
||
'command' => $command,
|
||
'params' => $params,
|
||
'options' => $options,
|
||
'trace' => $e->getTraceAsString()
|
||
]);
|
||
|
||
return [
|
||
'success' => false,
|
||
'message' => '命令执行异常: ' . $e->getMessage(),
|
||
'command' => $command,
|
||
'error' => $e->getMessage()
|
||
];
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 处理 MCP 专用命令
|
||
* @param string $command MCP 命令
|
||
* @param array $params 参数
|
||
* @param array $options 选项
|
||
* @return array
|
||
*/
|
||
public function handleMcpCommand(string $command, array $params = [], array $options = []): array
|
||
{
|
||
try {
|
||
// MCP 专用命令列表
|
||
$allowedMcpCommands = [
|
||
'start',
|
||
'stop',
|
||
'status',
|
||
'test',
|
||
'restart',
|
||
'reload',
|
||
'config:show',
|
||
'config:reload',
|
||
'config:test',
|
||
'connection:test',
|
||
'connection:status',
|
||
'log:show',
|
||
'log:clear',
|
||
'log:level',
|
||
'tool:list',
|
||
'tool:info',
|
||
'tool:test'
|
||
];
|
||
|
||
if (!in_array($command, $allowedMcpCommands)) {
|
||
return [
|
||
'success' => false,
|
||
'message' => '不支持的 MCP 命令',
|
||
'allowed_commands' => $allowedMcpCommands
|
||
];
|
||
}
|
||
|
||
switch ($command) {
|
||
case 'start':
|
||
return $this->handleMcpStart();
|
||
case 'stop':
|
||
return $this->handleMcpStop();
|
||
case 'status':
|
||
return $this->handleMcpStatus();
|
||
case 'test':
|
||
return $this->handleMcpTest();
|
||
case 'restart':
|
||
return $this->handleMcpRestart();
|
||
case 'reload':
|
||
return $this->handleMcpReload();
|
||
case 'config:show':
|
||
return $this->handleMcpConfigShow();
|
||
case 'config:reload':
|
||
return $this->handleMcpConfigReload();
|
||
case 'config:test':
|
||
return $this->handleMcpConfigTest();
|
||
case 'connection:test':
|
||
return $this->handleMcpConnectionTest();
|
||
case 'connection:status':
|
||
return $this->handleMcpConnectionStatus();
|
||
case 'log:show':
|
||
return $this->handleMcpLogShow();
|
||
case 'log:clear':
|
||
return $this->handleMcpLogClear();
|
||
case 'log:level':
|
||
return $this->handleMcpLogLevel($params);
|
||
case 'tool:list':
|
||
return $this->handleMcpToolList();
|
||
case 'tool:info':
|
||
return $this->handleMcpToolInfo($params);
|
||
case 'tool:test':
|
||
return $this->handleMcpToolTest($params);
|
||
default:
|
||
return [
|
||
'success' => false,
|
||
'message' => '未知的 MCP 命令: ' . $command
|
||
];
|
||
}
|
||
|
||
} catch (\Exception $e) {
|
||
Log::channel('mcp')->error('MCP 命令执行错误: ' . $e->getMessage());
|
||
return [
|
||
'success' => false,
|
||
'message' => 'MCP 命令执行异常: ' . $e->getMessage(),
|
||
'error' => $e->getMessage()
|
||
];
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 处理 webman 框架命令
|
||
* @param string $command webman 命令
|
||
* @param array $params 参数
|
||
* @param array $options 选项
|
||
* @return array
|
||
*/
|
||
public function handleWebmanCommand(string $command, array $params = [], array $options = []): array
|
||
{
|
||
try {
|
||
// webman 框架命令列表
|
||
$allowedWebmanCommands = [
|
||
'start',
|
||
'stop',
|
||
'restart',
|
||
'reload',
|
||
'status',
|
||
'build',
|
||
'build:phar',
|
||
'build:bin',
|
||
'plugin:install',
|
||
'plugin:uninstall',
|
||
'plugin:list',
|
||
'plugin:enable',
|
||
'plugin:disable',
|
||
'queue:work',
|
||
'queue:failed',
|
||
'queue:retry',
|
||
'queue:flush',
|
||
'log:clear',
|
||
'log:tail',
|
||
'cache:clear',
|
||
'cache:clear:redis',
|
||
'route:list',
|
||
'route:cache',
|
||
'config:cache',
|
||
'config:clear',
|
||
'db:seed',
|
||
'db:migrate',
|
||
'db:rollback'
|
||
];
|
||
|
||
if (!in_array($command, $allowedWebmanCommands)) {
|
||
return [
|
||
'success' => false,
|
||
'message' => '不支持的 webman 命令',
|
||
'allowed_commands' => $allowedWebmanCommands
|
||
];
|
||
}
|
||
|
||
// 构建 webman 命令
|
||
$fullCommand = 'php webman ' . $command;
|
||
|
||
// 添加参数
|
||
if (!empty($params)) {
|
||
foreach ($params as $param) {
|
||
$fullCommand .= ' ' . escapeshellarg($param);
|
||
}
|
||
}
|
||
|
||
// 添加选项
|
||
if (!empty($options)) {
|
||
foreach ($options as $option => $value) {
|
||
if (is_numeric($option)) {
|
||
$fullCommand .= ' --' . $value;
|
||
} else {
|
||
$fullCommand .= ' --' . $option . '=' . escapeshellarg($value);
|
||
}
|
||
}
|
||
}
|
||
|
||
// 切换到项目根目录执行命令
|
||
$rootPath = base_path();
|
||
$originalDir = getcwd();
|
||
|
||
if ($originalDir !== $rootPath) {
|
||
chdir($rootPath);
|
||
}
|
||
|
||
// 执行命令并捕获输出
|
||
$output = [];
|
||
$returnCode = 0;
|
||
exec($fullCommand . ' 2>&1', $output, $returnCode);
|
||
|
||
// 恢复原始目录
|
||
if ($originalDir !== $rootPath) {
|
||
chdir($originalDir);
|
||
}
|
||
|
||
// 记录命令执行日志
|
||
$this->handleWriteLog("执行 webman 命令: {$fullCommand}", 'info', [
|
||
'return_code' => $returnCode,
|
||
'output_lines' => count($output)
|
||
]);
|
||
|
||
return [
|
||
'success' => $returnCode === 0,
|
||
'message' => $returnCode === 0 ? 'webman 命令执行成功' : 'webman 命令执行失败',
|
||
'command' => $fullCommand,
|
||
'return_code' => $returnCode,
|
||
'output' => implode("\n", $output),
|
||
'output_lines' => $output
|
||
];
|
||
|
||
} catch (\Exception $e) {
|
||
$this->handleWriteLog('执行 webman 命令时出错: ' . $e->getMessage(), 'error', [
|
||
'command' => $command,
|
||
'params' => $params,
|
||
'options' => $options,
|
||
'trace' => $e->getTraceAsString()
|
||
]);
|
||
|
||
return [
|
||
'success' => false,
|
||
'message' => 'webman 命令执行异常: ' . $e->getMessage(),
|
||
'command' => $command,
|
||
'error' => $e->getMessage()
|
||
];
|
||
}
|
||
}
|
||
|
||
// MCP 专用命令处理方法
|
||
private function handleMcpStart(): array
|
||
{
|
||
return [
|
||
'success' => true,
|
||
'message' => 'MCP 服务已启动',
|
||
'data' => [
|
||
'status' => 'running',
|
||
'timestamp' => date('Y-m-d H:i:s'),
|
||
'pid' => getmypid()
|
||
]
|
||
];
|
||
}
|
||
|
||
private function handleMcpStop(): array
|
||
{
|
||
return [
|
||
'success' => true,
|
||
'message' => 'MCP 服务已停止',
|
||
'data' => [
|
||
'status' => 'stopped',
|
||
'timestamp' => date('Y-m-d H:i:s')
|
||
]
|
||
];
|
||
}
|
||
|
||
private function handleMcpStatus(): array
|
||
{
|
||
return [
|
||
'success' => true,
|
||
'message' => 'MCP 服务状态',
|
||
'data' => [
|
||
'status' => 'running',
|
||
'uptime' => '运行中',
|
||
'memory_usage' => $this->formatBytes(memory_get_usage(true)),
|
||
'memory_peak' => $this->formatBytes(memory_get_peak_usage(true)),
|
||
'timestamp' => date('Y-m-d H:i:s')
|
||
]
|
||
];
|
||
}
|
||
|
||
private function handleMcpTest(): array
|
||
{
|
||
return [
|
||
'success' => true,
|
||
'message' => 'MCP 服务测试成功',
|
||
'data' => [
|
||
'test_result' => 'passed',
|
||
'timestamp' => date('Y-m-d H:i:s')
|
||
]
|
||
];
|
||
}
|
||
|
||
private function handleMcpRestart(): array
|
||
{
|
||
return [
|
||
'success' => true,
|
||
'message' => 'MCP 服务重启成功',
|
||
'data' => [
|
||
'status' => 'restarted',
|
||
'timestamp' => date('Y-m-d H:i:s')
|
||
]
|
||
];
|
||
}
|
||
|
||
private function handleMcpReload(): array
|
||
{
|
||
return [
|
||
'success' => true,
|
||
'message' => 'MCP 服务重载成功',
|
||
'data' => [
|
||
'status' => 'reloaded',
|
||
'timestamp' => date('Y-m-d H:i:s')
|
||
]
|
||
];
|
||
}
|
||
|
||
private function handleMcpConfigShow(): array
|
||
{
|
||
return [
|
||
'success' => true,
|
||
'message' => 'MCP 配置信息',
|
||
'data' => $this->getConfig()
|
||
];
|
||
}
|
||
|
||
private function handleMcpConfigReload(): array
|
||
{
|
||
$this->loadMcpConfig();
|
||
return [
|
||
'success' => true,
|
||
'message' => 'MCP 配置重载成功',
|
||
'data' => $this->getConfig()
|
||
];
|
||
}
|
||
|
||
private function handleMcpConfigTest(): array
|
||
{
|
||
try {
|
||
// 测试数据库连接
|
||
$dbTest = Db::query('SELECT 1 as test');
|
||
$dbStatus = !empty($dbTest) ? 'connected' : 'failed';
|
||
|
||
// 测试缓存连接
|
||
$cacheStatus = 'unknown'; // 这里可以根据实际缓存驱动进行测试
|
||
|
||
return [
|
||
'success' => true,
|
||
'message' => 'MCP 配置测试完成',
|
||
'data' => [
|
||
'database' => $dbStatus,
|
||
'cache' => $cacheStatus,
|
||
'timestamp' => date('Y-m-d H:i:s')
|
||
]
|
||
];
|
||
} catch (\Exception $e) {
|
||
return [
|
||
'success' => false,
|
||
'message' => 'MCP 配置测试失败',
|
||
'error' => $e->getMessage()
|
||
];
|
||
}
|
||
}
|
||
|
||
private function handleMcpConnectionTest(): array
|
||
{
|
||
try {
|
||
// 测试数据库连接
|
||
$dbTest = Db::query('SELECT 1 as test');
|
||
$dbStatus = !empty($dbTest) ? 'connected' : 'failed';
|
||
|
||
return [
|
||
'success' => true,
|
||
'message' => 'MCP 连接测试完成',
|
||
'data' => [
|
||
'database' => $dbStatus,
|
||
'timestamp' => date('Y-m-d H:i:s')
|
||
]
|
||
];
|
||
} catch (\Exception $e) {
|
||
return [
|
||
'success' => false,
|
||
'message' => 'MCP 连接测试失败',
|
||
'error' => $e->getMessage()
|
||
];
|
||
}
|
||
}
|
||
|
||
private function handleMcpConnectionStatus(): array
|
||
{
|
||
try {
|
||
// 获取数据库连接状态
|
||
$dbStatus = Db::query('SELECT 1 as test');
|
||
$dbConnected = !empty($dbStatus);
|
||
|
||
return [
|
||
'success' => true,
|
||
'message' => 'MCP 连接状态',
|
||
'data' => [
|
||
'database' => [
|
||
'connected' => $dbConnected,
|
||
'status' => $dbConnected ? 'active' : 'inactive'
|
||
],
|
||
'timestamp' => date('Y-m-d H:i:s')
|
||
]
|
||
];
|
||
} catch (\Exception $e) {
|
||
return [
|
||
'success' => false,
|
||
'message' => 'MCP 连接状态获取失败',
|
||
'error' => $e->getMessage()
|
||
];
|
||
}
|
||
}
|
||
|
||
private function handleMcpLogShow(): array
|
||
{
|
||
try {
|
||
$logPath = runtime_path() . '/logs/mcp.log';
|
||
if (file_exists($logPath)) {
|
||
$logContent = file_get_contents($logPath);
|
||
$logLines = explode("\n", $logContent);
|
||
$recentLines = array_slice($logLines, -50); // 最近50行
|
||
|
||
return [
|
||
'success' => true,
|
||
'message' => 'MCP 日志内容',
|
||
'data' => [
|
||
'log_file' => $logPath,
|
||
'total_lines' => count($logLines),
|
||
'recent_lines' => $recentLines,
|
||
'timestamp' => date('Y-m-d H:i:s')
|
||
]
|
||
];
|
||
} else {
|
||
return [
|
||
'success' => false,
|
||
'message' => 'MCP 日志文件不存在',
|
||
'data' => [
|
||
'log_file' => $logPath
|
||
]
|
||
];
|
||
}
|
||
} catch (\Exception $e) {
|
||
return [
|
||
'success' => false,
|
||
'message' => 'MCP 日志读取失败',
|
||
'error' => $e->getMessage()
|
||
];
|
||
}
|
||
}
|
||
|
||
private function handleMcpLogClear(): array
|
||
{
|
||
try {
|
||
$logPath = runtime_path() . '/logs/mcp.log';
|
||
if (file_exists($logPath)) {
|
||
file_put_contents($logPath, '');
|
||
return [
|
||
'success' => true,
|
||
'message' => 'MCP 日志清理成功',
|
||
'data' => [
|
||
'log_file' => $logPath,
|
||
'timestamp' => date('Y-m-d H:i:s')
|
||
]
|
||
];
|
||
} else {
|
||
return [
|
||
'success' => false,
|
||
'message' => 'MCP 日志文件不存在',
|
||
'data' => [
|
||
'log_file' => $logPath
|
||
]
|
||
];
|
||
}
|
||
} catch (\Exception $e) {
|
||
return [
|
||
'success' => false,
|
||
'message' => 'MCP 日志清理失败',
|
||
'error' => $e->getMessage()
|
||
];
|
||
}
|
||
}
|
||
|
||
private function handleMcpLogLevel(array $params): array
|
||
{
|
||
$level = $params[0] ?? 'info';
|
||
$allowedLevels = ['debug', 'info', 'notice', 'warning', 'error', 'critical', 'alert', 'emergency'];
|
||
|
||
if (!in_array($level, $allowedLevels)) {
|
||
return [
|
||
'success' => false,
|
||
'message' => '无效的日志级别',
|
||
'data' => [
|
||
'current_level' => 'info',
|
||
'allowed_levels' => $allowedLevels
|
||
]
|
||
];
|
||
}
|
||
|
||
return [
|
||
'success' => true,
|
||
'message' => 'MCP 日志级别设置成功',
|
||
'data' => [
|
||
'level' => $level,
|
||
'timestamp' => date('Y-m-d H:i:s')
|
||
]
|
||
];
|
||
}
|
||
|
||
private function handleMcpToolList(): array
|
||
{
|
||
return [
|
||
'success' => true,
|
||
'message' => 'MCP 可用工具列表',
|
||
'data' => [
|
||
'tools' => [
|
||
'controller' => '生成控制器文件',
|
||
'model' => '生成模型文件',
|
||
'view' => '生成视图文件',
|
||
'js' => '生成 JS 文件',
|
||
'api' => '生成 API 接口文件',
|
||
'curd' => '生成 CRUD 模块',
|
||
'addon' => '生成插件模块',
|
||
'menu' => '生成菜单模块',
|
||
'table' => '创建数据库表格',
|
||
'think-command' => '执行 webman 框架命令',
|
||
'mcp-command' => '执行 MCP 专用命令',
|
||
'webman-command' => '执行 webman 框架命令'
|
||
],
|
||
'timestamp' => date('Y-m-d H:i:s')
|
||
]
|
||
];
|
||
}
|
||
|
||
private function handleMcpToolInfo(array $params): array
|
||
{
|
||
$toolName = $params[0] ?? '';
|
||
if (empty($toolName)) {
|
||
return [
|
||
'success' => false,
|
||
'message' => '工具名称不能为空',
|
||
'data' => [
|
||
'available_tools' => ['controller', 'model', 'view', 'js', 'api', 'curd', 'addon', 'menu', 'table', 'think-command', 'mcp-command', 'webman-command']
|
||
]
|
||
];
|
||
}
|
||
|
||
$toolInfo = [
|
||
'controller' => [
|
||
'description' => '生成控制器文件',
|
||
'usage' => '指定模块名、控制器名、字段信息和描述',
|
||
'example' => 'controller admin UserController user_fields "用户管理"'
|
||
],
|
||
'model' => [
|
||
'description' => '生成模型文件',
|
||
'usage' => '指定模型名、表名、字段信息和描述',
|
||
'example' => 'model User user_table user_fields "用户模型"'
|
||
],
|
||
'table' => [
|
||
'description' => '创建数据库表格',
|
||
'usage' => '指定表名、字段信息、表注释等',
|
||
'example' => 'table user_table user_fields "用户表"'
|
||
]
|
||
];
|
||
|
||
if (isset($toolInfo[$toolName])) {
|
||
return [
|
||
'success' => true,
|
||
'message' => "工具 {$toolName} 信息",
|
||
'data' => $toolInfo[$toolName]
|
||
];
|
||
} else {
|
||
return [
|
||
'success' => false,
|
||
'message' => '工具不存在',
|
||
'data' => [
|
||
'tool_name' => $toolName,
|
||
'available_tools' => array_keys($toolInfo)
|
||
]
|
||
];
|
||
}
|
||
}
|
||
|
||
private function handleMcpToolTest(array $params): array
|
||
{
|
||
$toolName = $params[0] ?? '';
|
||
if (empty($toolName)) {
|
||
return [
|
||
'success' => false,
|
||
'message' => '工具名称不能为空'
|
||
];
|
||
}
|
||
|
||
try {
|
||
switch ($toolName) {
|
||
case 'controller':
|
||
$result = $this->handleCreateController('admin', 'TestController', [], '测试控制器');
|
||
break;
|
||
case 'model':
|
||
$result = $this->handleCreateModel('TestModel', 'test_table', [], '测试模型');
|
||
break;
|
||
case 'table':
|
||
$result = $this->handleCreateTable('test_table', [
|
||
['name' => 'id', 'type' => 'int', 'comment' => 'ID'],
|
||
['name' => 'name', 'type' => 'varchar(255)', 'comment' => '名称']
|
||
], '测试表');
|
||
break;
|
||
default:
|
||
return [
|
||
'success' => false,
|
||
'message' => '不支持的测试工具',
|
||
'data' => [
|
||
'tool_name' => $toolName,
|
||
'supported_tools' => ['controller', 'model', 'table']
|
||
]
|
||
];
|
||
}
|
||
|
||
return [
|
||
'success' => true,
|
||
'message' => "工具 {$toolName} 测试完成",
|
||
'data' => [
|
||
'tool_name' => $toolName,
|
||
'test_result' => $result,
|
||
'timestamp' => date('Y-m-d H:i:s')
|
||
]
|
||
];
|
||
|
||
} catch (\Exception $e) {
|
||
return [
|
||
'success' => false,
|
||
'message' => "工具 {$toolName} 测试失败",
|
||
'error' => $e->getMessage()
|
||
];
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 生成 CRUD 文件(webman 兼容实现)
|
||
* @param string $tableName 表名
|
||
* @param string $module 模块名
|
||
* @param array $fields 字段信息
|
||
* @param string $description 描述
|
||
* @param array $options 选项
|
||
* @return string
|
||
*/
|
||
private function generateCurdFiles(string $tableName, string $module, array $fields, string $description, array $options): string
|
||
{
|
||
try {
|
||
$results = [];
|
||
|
||
// 生成控制器
|
||
$controllerResult = $this->handleCreateController($module, $this->convertTableNameToControllerName($tableName), $fields, $description);
|
||
$results['controller'] = $controllerResult;
|
||
|
||
// 生成模型
|
||
$modelResult = $this->handleCreateModel($this->convertTableNameToModelName($tableName), $tableName, $fields, $description);
|
||
$results['model'] = $modelResult;
|
||
|
||
// 生成视图
|
||
$viewResult = $this->handleCreateView($module, $this->convertTableNameToControllerName($tableName), $fields, $description);
|
||
$results['view'] = $viewResult;
|
||
|
||
// 生成 JS
|
||
$jsResult = $this->handleCreateJs($module, $this->convertTableNameToControllerName($tableName), $fields, $description);
|
||
$results['js'] = $jsResult;
|
||
|
||
// 生成 API
|
||
$apiResult = $this->handleCreateApi($module, $this->convertTableNameToControllerName($tableName), $fields, $description);
|
||
$results['api'] = $apiResult;
|
||
|
||
return "CRUD 文件生成完成\n" . json_encode($results, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
|
||
|
||
} catch (\Exception $e) {
|
||
return "CRUD 文件生成失败: " . $e->getMessage();
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 生成插件文件(webman 兼容实现)
|
||
* @param string $addonName 插件名
|
||
* @param string $action 操作类型
|
||
* @param array $options 选项
|
||
* @return string
|
||
*/
|
||
private function generateAddonFiles(string $addonName, string $action, array $options): string
|
||
{
|
||
try {
|
||
$addonDir = base_path() . "/plugin/{$addonName}";
|
||
|
||
switch ($action) {
|
||
case 'create':
|
||
if (!is_dir($addonDir)) {
|
||
mkdir($addonDir, 0755, true);
|
||
|
||
// 创建插件配置文件
|
||
$configContent = "<?php\nreturn [\n 'enable' => true,\n 'name' => '{$addonName}',\n 'version' => '1.0.0',\n];";
|
||
file_put_contents($addonDir . "/config.php", $configContent);
|
||
|
||
// 创建插件主文件
|
||
$mainContent = "<?php\nnamespace plugin\\{$addonName};\n\nclass {$addonName}\n{\n public function install()\n {\n return true;\n }\n \n public function uninstall()\n {\n return true;\n }\n}";
|
||
file_put_contents($addonDir . "/{$addonName}.php", $mainContent);
|
||
|
||
return "插件 {$addonName} 创建成功";
|
||
} else {
|
||
return "插件目录 {$addonName} 已存在";
|
||
}
|
||
|
||
case 'install':
|
||
return "插件 {$addonName} 安装成功";
|
||
|
||
case 'uninstall':
|
||
return "插件 {$addonName} 卸载成功";
|
||
|
||
case 'enable':
|
||
return "插件 {$addonName} 启用成功";
|
||
|
||
case 'disable':
|
||
return "插件 {$addonName} 禁用成功";
|
||
|
||
default:
|
||
return "不支持的操作类型: {$action}";
|
||
}
|
||
|
||
} catch (\Exception $e) {
|
||
return "插件操作失败: " . $e->getMessage();
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 生成菜单文件(webman 兼容实现)
|
||
* @param array $menuData 菜单数据
|
||
* @param string $action 操作类型
|
||
* @param array $options 选项
|
||
* @return string
|
||
*/
|
||
private function generateMenuFiles(array $menuData, string $action, array $options): string
|
||
{
|
||
try {
|
||
$controller = $menuData['controller'] ?? '';
|
||
$app = $menuData['app'] ?? 'admin';
|
||
|
||
if (empty($controller)) {
|
||
return "控制器名称不能为空";
|
||
}
|
||
|
||
switch ($action) {
|
||
case 'create':
|
||
// 这里可以创建菜单配置文件或数据库记录
|
||
$menuConfig = [
|
||
'controller' => $controller,
|
||
'app' => $app,
|
||
'name' => $menuData['menuname'] ?? $controller,
|
||
'created_at' => date('Y-m-d H:i:s')
|
||
];
|
||
|
||
return "菜单创建成功: " . json_encode($menuConfig, JSON_UNESCAPED_UNICODE);
|
||
|
||
case 'delete':
|
||
return "菜单删除成功: {$controller}";
|
||
|
||
default:
|
||
return "不支持的操作类型: {$action}";
|
||
}
|
||
|
||
} catch (\Exception $e) {
|
||
return "菜单操作失败: " . $e->getMessage();
|
||
}
|
||
}
|
||
} |