202 lines
9.3 KiB
PHP
202 lines
9.3 KiB
PHP
<?php
|
||
declare(strict_types=1);
|
||
|
||
namespace app\mcp\controller;
|
||
|
||
use app\adminapi\logic\LoginLogic;
|
||
use app\BaseController;
|
||
use app\common\cache\AdminTokenCache;
|
||
use app\mcp\service\Catalog;
|
||
use app\mcp\service\GrantService;
|
||
use app\mcp\service\Guard;
|
||
use app\mcp\service\McpConfig;
|
||
use app\mcp\service\PermissionService;
|
||
use think\facade\Db;
|
||
use think\Response;
|
||
|
||
/**
|
||
* 后台管理页面用的接口(甄养堂后台“AI 助手”菜单):沿用后台登录令牌(token 头)识别管理员。
|
||
* GET /mcp/admin/grants AI 授权列表(有 ai.grant/lists 看全部,否则只看自己的)
|
||
* POST /mcp/admin/revoke 撤销授权(自己的,或有 ai.grant/revoke)
|
||
* GET /mcp/admin/logs AI 访问日志(有 ai.accessLog/lists 看全部,否则只看自己的)
|
||
* GET /mcp/admin/catalog AI 数据目录与覆盖率(需 ai.catalog/lists)
|
||
*/
|
||
class AdminController extends BaseController
|
||
{
|
||
private array $adminInfo = [];
|
||
|
||
public function grants(): Response
|
||
{
|
||
if ($denied = $this->authorize('GET')) {
|
||
return $denied;
|
||
}
|
||
$params = $this->request->get();
|
||
$query = Db::name('ai_grant')->alias('g')->leftJoin('admin a', 'a.id = g.admin_id')
|
||
->field('g.id,g.admin_id,a.name as admin_name,a.account as admin_account,g.token_prefix,g.client,g.client_instance,g.label,g.status,'
|
||
. 'g.expire_time,g.idle_days,g.last_used_time,g.last_used_ip,g.created_ip,g.revoke_time,g.revoke_reason,g.create_time');
|
||
if (!$this->can('ai.grant/lists')) {
|
||
$query->where('g.admin_id', $this->adminId());
|
||
} elseif (!empty($params['admin_id'])) {
|
||
$query->where('g.admin_id', (int) $params['admin_id']);
|
||
}
|
||
if (isset($params['status']) && $params['status'] !== '') {
|
||
$query->where('g.status', (int) $params['status']);
|
||
}
|
||
if (!empty($params['keyword'])) {
|
||
$keyword = '%' . trim((string) $params['keyword']) . '%';
|
||
$query->where(static fn ($q) => $q->whereLike('a.name', $keyword)->whereOr('a.account', 'like', $keyword)->whereOr('g.label', 'like', $keyword));
|
||
}
|
||
[$pageNo, $pageSize] = $this->page($params);
|
||
$count = (clone $query)->count();
|
||
$rows = $query->order('g.id', 'desc')->page($pageNo, $pageSize)->select()->toArray();
|
||
$now = time();
|
||
foreach ($rows as &$row) {
|
||
$idleUntil = (int) $row['last_used_time'] + (int) $row['idle_days'] * 86400;
|
||
$active = (int) $row['status'] === GrantService::STATUS_ACTIVE && (int) $row['expire_time'] > $now && $idleUntil > $now;
|
||
$row['status_text'] = $active ? '有效' : ((int) $row['status'] === GrantService::STATUS_REVOKED ? '已撤销' : '已过期');
|
||
$row['can_revoke'] = $active && ((int) $row['admin_id'] === $this->adminId() || $this->can('ai.grant/revoke'));
|
||
foreach (['expire_time', 'last_used_time', 'revoke_time', 'create_time'] as $field) {
|
||
$row[$field . '_text'] = (int) $row[$field] > 0 ? date('Y-m-d H:i', (int) $row[$field]) : '';
|
||
}
|
||
}
|
||
unset($row);
|
||
return $this->lists($rows, $count, $pageNo, $pageSize, ['enabled' => McpConfig::enabled()]);
|
||
}
|
||
|
||
public function revoke(): Response
|
||
{
|
||
if ($denied = $this->authorize('POST')) {
|
||
return $denied;
|
||
}
|
||
$id = (int) ($this->request->post('id') ?? 0);
|
||
$grant = GrantService::find($id);
|
||
if (!$grant) {
|
||
return Guard::envelope(0, '授权不存在', [], 200, 1);
|
||
}
|
||
if ((int) $grant['admin_id'] !== $this->adminId() && !$this->can('ai.grant/revoke')) {
|
||
return Guard::envelope(0, '权限不足,无法访问或操作', [], 200, 1);
|
||
}
|
||
GrantService::close($id, GrantService::STATUS_REVOKED, 'admin_revoke', $this->adminId());
|
||
return Guard::envelope(1, '已撤销', [], 200, 1);
|
||
}
|
||
|
||
public function logs(): Response
|
||
{
|
||
if ($denied = $this->authorize('GET')) {
|
||
return $denied;
|
||
}
|
||
$params = $this->request->get();
|
||
$query = Db::name('ai_access_log')->alias('l')->leftJoin('admin a', 'a.id = l.admin_id')
|
||
->field('l.*,a.name as admin_name,a.account as admin_account');
|
||
if (!$this->can('ai.accessLog/lists')) {
|
||
$query->where('l.admin_id', $this->adminId());
|
||
} elseif (!empty($params['admin_id'])) {
|
||
$query->where('l.admin_id', (int) $params['admin_id']);
|
||
}
|
||
foreach (['status' => 'l.status', 'tool' => 'l.tool', 'client_task_id' => 'l.client_task_id'] as $param => $column) {
|
||
if (!empty($params[$param])) {
|
||
$query->where($column, (string) $params[$param]);
|
||
}
|
||
}
|
||
if (!empty($params['resource'])) {
|
||
$query->whereLike('l.resource', '%' . trim((string) $params['resource']) . '%');
|
||
}
|
||
if (!empty($params['record_id'])) {
|
||
$query->whereRaw('FIND_IN_SET(:rid, l.record_ids)', ['rid' => (string) $params['record_id']]);
|
||
}
|
||
if (!empty($params['start_time']) && strtotime((string) $params['start_time'])) {
|
||
$query->where('l.create_time', '>=', strtotime((string) $params['start_time']));
|
||
}
|
||
if (!empty($params['end_time']) && strtotime((string) $params['end_time'])) {
|
||
$query->where('l.create_time', '<=', strtotime((string) $params['end_time']));
|
||
}
|
||
[$pageNo, $pageSize] = $this->page($params);
|
||
$count = (clone $query)->count();
|
||
$rows = $query->order('l.id', 'desc')->page($pageNo, $pageSize)->select()->toArray();
|
||
$names = [];
|
||
foreach (Catalog::all() as $key => $r) {
|
||
$names[$key] = $r['name'];
|
||
}
|
||
foreach ($rows as &$row) {
|
||
$row['create_time_text'] = date('Y-m-d H:i:s', (int) $row['create_time']);
|
||
$row['resource_name'] = $names[$row['resource']] ?? '';
|
||
}
|
||
unset($row);
|
||
return $this->lists($rows, $count, $pageNo, $pageSize);
|
||
}
|
||
|
||
public function catalog(): Response
|
||
{
|
||
if ($denied = $this->authorize('GET', 'ai.catalog/lists')) {
|
||
return $denied;
|
||
}
|
||
$params = $this->request->get();
|
||
$rows = [];
|
||
foreach (Catalog::all() as $key => $r) {
|
||
if (!empty($params['status']) && $r['status'] !== $params['status']) {
|
||
continue;
|
||
}
|
||
if (!empty($params['domain']) && $r['domain'] !== $params['domain']) {
|
||
continue;
|
||
}
|
||
if (!empty($params['keyword']) && mb_stripos($r['name'] . ' ' . $key, trim((string) $params['keyword'])) === false) {
|
||
continue;
|
||
}
|
||
$rows[] = ['resource' => $key, 'name' => $r['name'], 'domain' => $r['domain'], 'kind' => $r['kind'], 'status' => $r['status'],
|
||
'reason' => $r['reason'], 'reviewed' => $r['reviewed'], 'registered' => $r['registered']];
|
||
}
|
||
[$pageNo, $pageSize] = $this->page($params, 100);
|
||
$domains = array_values(array_unique(array_column(Catalog::all(), 'domain')));
|
||
sort($domains);
|
||
return $this->lists(array_slice($rows, ($pageNo - 1) * $pageSize, $pageSize), count($rows), $pageNo, $pageSize,
|
||
['counts' => Catalog::counts(), 'domains' => $domains]);
|
||
}
|
||
|
||
/** 后台登录令牌 + IP 绑定 + 企微强制绑定,与后台登录/权限中间件一致;可再要求一个权限点 */
|
||
private function authorize(string $method, string $perm = ''): ?Response
|
||
{
|
||
if ($this->request->method(true) !== $method) {
|
||
return response('', 405)->header(['Allow' => $method]);
|
||
}
|
||
$token = (string) $this->request->header('token', '');
|
||
$adminInfo = $token !== '' ? (new AdminTokenCache())->getAdminInfo($token) : false;
|
||
if (empty($adminInfo)) {
|
||
return Guard::envelope(-1, '登录超时,请重新登录', [], 200, 0);
|
||
}
|
||
if (($adminInfo['login_ip'] ?? '') != $this->request->ip()) {
|
||
return Guard::envelope(-1, 'ip地址发生变化,请重新登录', [], 200, 0);
|
||
}
|
||
if (LoginLogic::adminMustBindWorkWechat($adminInfo)) {
|
||
return Guard::envelope(LoginLogic::CODE_NEED_BIND_WORK_WECHAT, '请先绑定企业微信后再使用系统', [], 200, 0);
|
||
}
|
||
$this->adminInfo = $adminInfo;
|
||
if ($perm !== '' && !$this->can($perm)) {
|
||
return Guard::envelope(0, '权限不足,无法访问或操作', [], 200, 1);
|
||
}
|
||
return null;
|
||
}
|
||
|
||
private function can(string $perm): bool
|
||
{
|
||
if ((int) ($this->adminInfo['root'] ?? 0) === 1) {
|
||
return true;
|
||
}
|
||
return PermissionService::isRegistered($perm) && isset(PermissionService::adminPerms($this->adminId())[PermissionService::normalize($perm)]);
|
||
}
|
||
|
||
private function adminId(): int
|
||
{
|
||
return (int) ($this->adminInfo['admin_id'] ?? 0);
|
||
}
|
||
|
||
private function page(array $params, int $max = 100): array
|
||
{
|
||
return [max(1, (int) ($params['page_no'] ?? 1)), max(1, min($max, (int) ($params['page_size'] ?? 15)))];
|
||
}
|
||
|
||
private function lists(array $rows, int $count, int $pageNo, int $pageSize, array $extend = []): Response
|
||
{
|
||
return Guard::envelope(1, '', ['lists' => $rows, 'count' => $count, 'page_no' => $pageNo, 'page_size' => $pageSize, 'extend' => $extend ?: new \stdClass()]);
|
||
}
|
||
}
|