diff --git a/extend/base/admin/controller/system/McpKeyBase.php b/extend/base/admin/controller/system/McpKeyBase.php
index 3401ccb..1a7ae76 100644
--- a/extend/base/admin/controller/system/McpKeyBase.php
+++ b/extend/base/admin/controller/system/McpKeyBase.php
@@ -3,9 +3,12 @@
namespace base\admin\controller\system;
use app\admin\model\SystemAdmin;
+use app\admin\model\SystemMcpKeyNode;
use app\admin\service\annotation\ControllerAnnotation;
use app\admin\service\annotation\NodeAnotation;
+use app\admin\service\NodeService;
use app\common\controller\AdminController;
+use app\common\service\AuthService;
use think\App;
/**
@@ -139,4 +142,120 @@ class McpKeyBase extends AdminController
return $this->fetch();
}
+
+ /**
+ * 授权页:勾选创建者当前权限的子集给密钥.
+ * @NodeAnotation(title="授权")
+ */
+ public function authorize($id)
+ {
+ $row = $this->model->find($id);
+ empty($row) && $this->error('数据不存在');
+
+ // 全量动作节点来自 getNodeTree 展开全部 type=2 节点
+ $nodeService = new NodeService();
+ $module_list = $nodeService->getNodeTree();
+
+ // 创建者当前节点集合:与 checkNode 完全一致的判定链(含超管直通/auth_on/动态黑名单)
+ $authService = new AuthService($row->bind_admin_id);
+ $allActionNodes = [];
+ foreach ($module_list as $module) {
+ foreach ($module['children'] as $controller) {
+ foreach ($controller['children'] as $action) {
+ $allActionNodes[] = $action['node'];
+ }
+ }
+ }
+ $creatorAllowedNodes = $authService->getAdminAllowedNodes($allActionNodes, $row->bind_admin_id);
+
+ // 已选节点 = 库中记录 ∩ 创建者当前集合(防漂移:创建者权限被回收后旧勾选自动失效)
+ $checkedNodeList = (new SystemMcpKeyNode())
+ ->where('key_id', $id)
+ ->column('node');
+ $checkedNodeList = array_values(array_intersect($checkedNodeList, $creatorAllowedNodes));
+
+ // 树过滤:动作节点不在创建者集合内整级剔除;无动作节点的控制器、无控制器的模块不显示
+ foreach ($module_list as $module_key => $module) {
+ foreach ($module['children'] as $controllerKey => $controller) {
+ foreach ($controller['children'] as $actionKey => $action) {
+ if (!in_array($action['node'], $creatorAllowedNodes)) {
+ unset($module_list[$module_key]['children'][$controllerKey]['children'][$actionKey]);
+ continue;
+ }
+ $checked = in_array($action['node'], $checkedNodeList);
+ $checked_string = $checked ? 'checked' : '';
+ $disabled_string = '';
+ if (!$action['auth']) {
+ // 免鉴权节点创建者恒有权限,照 auth/authorize 惯例固定勾选并禁用
+ $checked_string = 'checked';
+ $disabled_string = 'disabled';
+ }
+ $module_list[$module_key]['children'][$controllerKey]['children'][$actionKey]['checked'] = $checked_string;
+ $module_list[$module_key]['children'][$controllerKey]['children'][$actionKey]['disabled'] = $disabled_string;
+ }
+ if (empty($module_list[$module_key]['children'][$controllerKey]['children'])) {
+ unset($module_list[$module_key]['children'][$controllerKey]);
+ }
+ }
+ if (empty($module_list[$module_key]['children'])) {
+ unset($module_list[$module_key]);
+ }
+ }
+
+ $this->assign('row', $row);
+ $this->assign('checked_node_list', $checkedNodeList);
+ $this->assign('module_list', array_values($module_list));
+
+ return $this->fetch();
+ }
+
+ /**
+ * 授权保存:先删后插,服务端强校验防越权.
+ * @NodeAnotation(title="授权保存")
+ */
+ public function saveAuthorize()
+ {
+ $this->checkPostRequest();
+ $id = $this->request->post('id');
+ $node = (array) $this->request->post('node', []);
+
+ $row = $this->model->find($id);
+ empty($row) && $this->error('数据不存在');
+
+ // 服务端强校验:每个提交节点必须 ∈ 创建者当前节点集合(重新计算,防权限已变)
+ $nodeService = new NodeService();
+ $allActionNodes = [];
+ foreach ($nodeService->getNodeTree() as $module) {
+ foreach ($module['children'] as $controller) {
+ foreach ($controller['children'] as $action) {
+ $allActionNodes[] = $action['node'];
+ }
+ }
+ }
+ $authService = new AuthService($row->bind_admin_id);
+ $creatorAllowedNodes = $authService->getAdminAllowedNodes($allActionNodes, $row->bind_admin_id);
+ foreach ($node as $vo) {
+ if (!in_array($authService->parseNodeStr($vo), $creatorAllowedNodes)) {
+ $this->error('包含无权限节点');
+ }
+ }
+
+ try {
+ $keyNode = new SystemMcpKeyNode();
+ $keyNode->where('key_id', $id)->delete();
+ if (!empty($node)) {
+ $saveAll = [];
+ foreach ($node as $vo) {
+ $saveAll[] = [
+ 'key_id' => $id,
+ 'node' => $authService->parseNodeStr($vo),
+ ];
+ }
+ $keyNode->saveAll($saveAll);
+ }
+ } catch (\Exception $e) {
+ $this->error('保存失败');
+ }
+ $this->success('保存成功');
+ }
}
diff --git a/extend/base/admin/view/system/mcp_key/_common.js b/extend/base/admin/view/system/mcp_key/_common.js
index 2458be2..41d9c83 100644
--- a/extend/base/admin/view/system/mcp_key/_common.js
+++ b/extend/base/admin/view/system/mcp_key/_common.js
@@ -8,4 +8,5 @@ var init = {
deleteUrl: 'system.mcp_key/delete',
exportUrl: 'system.mcp_key/export',
modifyUrl: 'system.mcp_key/modify',
+ authorize_url: 'system.mcp_key/authorize',
};
diff --git a/extend/base/admin/view/system/mcp_key/authorize.html b/extend/base/admin/view/system/mcp_key/authorize.html
new file mode 100644
index 0000000..6719244
--- /dev/null
+++ b/extend/base/admin/view/system/mcp_key/authorize.html
@@ -0,0 +1,59 @@
+
diff --git a/extend/base/admin/view/system/mcp_key/authorize.js b/extend/base/admin/view/system/mcp_key/authorize.js
new file mode 100644
index 0000000..be939ac
--- /dev/null
+++ b/extend/base/admin/view/system/mcp_key/authorize.js
@@ -0,0 +1,50 @@
+$(function () {
+
+ layui.form.on('checkbox(controller-checkbox)', function (data) {
+ var elem = data.elem;
+ var checked = elem.checked;
+ $(elem).closest('tr').find('input').each(function(i,input){
+ if(!$(input).prop('disabled')){
+ $(input).prop('checked', checked);
+ }
+ })
+ });
+ layui.form.on('checkbox(action-checkbox)', function (data) {
+ initCheckedStatus();
+ });
+ function initCheckedStatus() {
+ $('.auth-node-tr').each(function (i, elem) {
+ var checkedCount = 0;
+ var totalCount = 0;
+ $(elem).find('[lay-filter="action-checkbox"]').each(function (j, checkbox) {
+ totalCount++;
+ if ($(checkbox).prop('checked')) {
+ checkedCount++;
+ }
+ });
+
+ var checkedStatus;
+ if (checkedCount === totalCount) {
+ checkedStatus = 1; // 全部选中
+ } else if (checkedCount === 0) {
+ checkedStatus = -1; // 全部未选中
+ } else {
+ checkedStatus = 0; // 部分选中
+ }
+ if (checkedStatus === 0) {
+ $(elem).find('[lay-filter="controller-checkbox"]').prop('checked', false);
+ $(elem).find('[lay-filter="controller-checkbox"]').prop('indeterminate', true);
+ } else if (checkedStatus > 0) {
+ $(elem).find('[lay-filter="controller-checkbox"]').prop('checked', true);
+ $(elem).find('[lay-filter="controller-checkbox"]').prop('indeterminate', false);
+ } else {
+ $(elem).find('[lay-filter="controller-checkbox"]').prop('checked', false);
+ $(elem).find('[lay-filter="controller-checkbox"]').prop('indeterminate', false);
+ }
+ layui.form.render('checkbox');
+ });
+ }
+ initCheckedStatus();
+
+ ua.listen();
+});
diff --git a/extend/base/admin/view/system/mcp_key/index.html b/extend/base/admin/view/system/mcp_key/index.html
index cb45858..68e8969 100644
--- a/extend/base/admin/view/system/mcp_key/index.html
+++ b/extend/base/admin/view/system/mcp_key/index.html
@@ -6,6 +6,7 @@
data-auth-edit="{:auth('system.mcp_key/edit')}"
data-auth-delete="{:auth('system.mcp_key/delete')}"
data-auth-modify="{:auth('system.mcp_key/modify')}"
+ data-auth-authorize="{:auth('system.mcp_key/authorize')}"
lay-filter="currentTable">
diff --git a/extend/base/admin/view/system/mcp_key/index.js b/extend/base/admin/view/system/mcp_key/index.js
index 22cb9fe..2ccb33f 100644
--- a/extend/base/admin/view/system/mcp_key/index.js
+++ b/extend/base/admin/view/system/mcp_key/index.js
@@ -40,7 +40,15 @@ $(function(){
{field: 'status', search: 'select', selectList: ua.getDataBrage('select_list_status'), title: '状态', width: 100, templet: ua.table.switch},
{field: 'remark', title: '备注', templet: ua.table.text},
{
- width: 150, title: '操作', templet: ua.table.tool, fixed: 'right', operat: [
+ width: 210, title: '操作', templet: ua.table.tool, fixed: 'right', operat: [
+ [{
+ text: '授权',
+ url: init.authorize_url,
+ method: 'open',
+ auth: 'authorize',
+ class: 'layui-btn layui-btn-normal layui-btn-xs',
+ extend: 'data-full="true"'
+ }],
'edit',
'delete'
]
diff --git a/extend/base/common/service/AuthServiceBase.php b/extend/base/common/service/AuthServiceBase.php
index 1345c46..ab96f2f 100644
--- a/extend/base/common/service/AuthServiceBase.php
+++ b/extend/base/common/service/AuthServiceBase.php
@@ -96,6 +96,33 @@ class AuthServiceBase
return static::$dynamicNodeList[$node];
}
+ // 判断是否需要获取当前节点
+ if (empty($node)) {
+ $node = $this->getCurrentNode();
+ } else {
+ $node = $this->parseNodeStr($node);
+ }
+
+ return $this->checkNodeResolved($node);
+ }
+
+ /**
+ * 对已规范化(parseNodeStr 后)的单个节点执行完整判定链.
+ *
+ * checkNode 与 getAdminAllowedNodes 共用此实现,严禁平行复制导致语义漂移。
+ * 判定链(顺序与原 checkNode 保持一致):
+ * 动态节点黑名单 → 超管直通 → auth_on 开关 → 未注册节点 default_auth_check
+ * → 注解 auth=false → status/auth_ids 校验 → 白名单成员判定
+ * @param string $node
+ * @return bool
+ */
+ protected function checkNodeResolved($node)
+ {
+ // 动态节点开关(按规范化节点命中)
+ if (isset(static::$dynamicNodeList[$node])) {
+ return static::$dynamicNodeList[$node];
+ }
+
// 判断是否为超级管理员
if ($this->isSuperAdmin()) {
return true;
@@ -104,12 +131,6 @@ class AuthServiceBase
if ($this->config['auth_on'] == false) {
return true;
}
- // 判断是否需要获取当前节点
- if (empty($node)) {
- $node = $this->getCurrentNode();
- } else {
- $node = $this->parseNodeStr($node);
- }
// 判断是否加入节点控制,优先获取缓存信息
if (!isset($this->nodeList[$node])) {
@@ -131,6 +152,35 @@ class AuthServiceBase
return false;
}
+ /**
+ * 批量获取管理员对候选节点集合的允许子集.
+ *
+ * 对每个候选节点走与 checkNode() 完全一致的判定链(共用 checkNodeResolved),
+ * 返回其中允许访问的节点(规范化后的形式,保持候选顺序并去重)。
+ * 用于 MCP 密钥授权(创建者权限子集)等批量场景。
+ * @param array $candidateNodes 候选节点列表(controller/action 形式,内部会做 parseNodeStr 规范化)
+ * @param int|null $adminId 管理员ID,为 null 时沿用当前实例的管理员
+ * @return array 允许的节点列表
+ */
+ public function getAdminAllowedNodes(array $candidateNodes, ?int $adminId = null): array
+ {
+ if (empty($adminId)) {
+ $adminId = $this->adminId;
+ }
+ // 目标管理员与当前实例不一致时,按目标管理员重建判定上下文
+ $service = ($adminId == $this->adminId) ? $this : new static($adminId);
+
+ $allowedNodes = [];
+ foreach ($candidateNodes as $node) {
+ $node = $service->parseNodeStr($node);
+ if ($service->checkNodeResolved($node) && !in_array($node, $allowedNodes)) {
+ $allowedNodes[] = $node;
+ }
+ }
+
+ return $allowedNodes;
+ }
+
/**
* 获取当前节点.
* @return string