diff --git a/app/admin/controller/system/McpKey.php b/app/admin/controller/system/McpKey.php new file mode 100644 index 0000000..be90a46 --- /dev/null +++ b/app/admin/controller/system/McpKey.php @@ -0,0 +1,14 @@ +table('system_mcp_key') + ->setComment('MCP密钥表') + ->addColumn('title', 'char', ['limit' => 50, 'null' => true, 'comment' => '密钥名称']) + ->addColumn('key', 'char', ['limit' => 64, 'null' => true, 'comment' => '密钥SHA-256哈希']) + ->addColumn('key_prefix', 'char', ['limit' => 16, 'null' => true, 'default' => '', 'comment' => '密钥前缀(展示用)']) + ->addColumn('bind_admin_id', 'biginteger', ['limit' => 11, 'null' => true, 'signed' => false, 'comment' => '绑定创建者ID']) + ->addColumn('status', 'integer', ['limit' => 11, 'null' => true, 'default' => 1, 'comment' => '状态 {radio} (0:禁用,1:启用)']) + ->addColumn('remark', 'char', ['limit' => 255, 'null' => true, 'default' => '', 'comment' => '']) + ->addColumn('use_num', 'biginteger', ['limit' => 11, 'null' => true, 'default' => 0, 'signed' => false, 'comment' => '调用次数']) + ->addColumn('last_use_time', 'integer', ['limit' => 11, 'null' => true, 'default' => 0, 'signed' => false, 'comment' => '最后使用时间']) + ->addColumn('create_time', 'integer', ['limit' => 11, 'null' => true, 'default' => 0, 'signed' => false, 'comment' => '创建时间']) + ->addColumn('update_time', 'integer', ['limit' => 11, 'null' => true, 'default' => 0, 'signed' => false, 'comment' => '更新时间']) + ->addColumn('delete_time', 'integer', ['limit' => 11, 'null' => true, 'default' => 0, 'signed' => false, 'comment' => '删除时间']) + ->addIndex(['key'], ['unique' => true, 'name' => 'key']) + ->addIndex(['delete_time'], ['name' => 'delete_time']) + ->create(); + } +} diff --git a/database/migrations/20260816203002_system_mcp_key_node.php b/database/migrations/20260816203002_system_mcp_key_node.php new file mode 100644 index 0000000..7875470 --- /dev/null +++ b/database/migrations/20260816203002_system_mcp_key_node.php @@ -0,0 +1,21 @@ +table('system_mcp_key_node') + ->setComment('MCP密钥与节点关系表') + ->addColumn('key_id', 'biginteger', ['limit' => 11, 'null' => true, 'signed' => false, 'comment' => '密钥ID']) + ->addColumn('node', 'char', ['limit' => 100, 'null' => true, 'default' => '', 'comment' => '']) + ->addIndex(['key_id'], ['name' => 'key_id']) + ->create(); + } +} diff --git a/database/migrations/20260816203003_system_mcp_log.php b/database/migrations/20260816203003_system_mcp_log.php new file mode 100644 index 0000000..8741718 --- /dev/null +++ b/database/migrations/20260816203003_system_mcp_log.php @@ -0,0 +1,25 @@ +table('system_mcp_log') + ->setComment('MCP调用日志表') + ->addColumn('key_id', 'biginteger', ['limit' => 11, 'null' => true, 'signed' => false, 'comment' => '密钥ID']) + ->addColumn('node', 'char', ['limit' => 100, 'null' => true, 'comment' => '调用节点']) + ->addColumn('arguments', 'text', ['null' => true, 'comment' => '参数摘要JSON']) + ->addColumn('is_success', 'integer', ['limit' => 1, 'null' => true, 'default' => 1, 'comment' => '是否成功:0=失败,1=成功']) + ->addColumn('cost_ms', 'integer', ['limit' => 11, 'null' => true, 'default' => 0, 'comment' => '耗时毫秒']) + ->addColumn('create_time', 'integer', ['limit' => 11, 'null' => true, 'default' => 0, 'signed' => false, 'comment' => '创建时间']) + ->addIndex(['key_id'], ['name' => 'key_id']) + ->create(); + } +} diff --git a/extend/base/admin/controller/system/McpKeyBase.php b/extend/base/admin/controller/system/McpKeyBase.php new file mode 100644 index 0000000..3401ccb --- /dev/null +++ b/extend/base/admin/controller/system/McpKeyBase.php @@ -0,0 +1,142 @@ + 'desc', + ]; + + public function __construct(App $app) + { + parent::__construct($app); + + $this->model = new \app\admin\model\SystemMcpKey(); + + $this->assign('select_list_status', $this->model::SELECT_LIST_STATUS, true); + + // 行内修改仅允许 status;key/key_prefix/bind_admin_id 为敏感字段,一律排除 + $this->allowModifyFields = [ + 'status', + ]; + } + + /** + * @NodeAnotation(title="列表") + */ + public function index() + { + if ($this->request->isAjax()) { + if (input('selectFields')) { + return $this->selectList(); + } + list($page, $limit, $where, $excludes, $request_options, $group) = $this->buildTableParames(); + $count = $this->model + ->where($where) + ->group($group) + ->count(); + $list = $this->model + ->where($where) + ->page($page, $limit) + ->order($this->sort) + ->group($group) + ->select(); + + // 附加创建者用户名:不用 withJoin(system_admin 与主表有 id/status 等同名字段会歧义), + // 一次 IN 查询建立 id => username 映射后回填,避免 N+1。 + $adminIds = []; + foreach ($list as $vo) { + !empty($vo->bind_admin_id) && $adminIds[] = $vo->bind_admin_id; + } + $adminNames = empty($adminIds) ? [] : SystemAdmin::whereIn('id', array_unique($adminIds))->column('username', 'id'); + foreach ($list as $vo) { + $vo->bind_admin_username = $adminNames[$vo->bind_admin_id] ?? ''; + } + + $data = [ + 'code' => 0, + 'msg' => '', + 'count' => $count, + 'data' => $list, + ]; + + return json($data); + } + + return $this->fetch(); + } + + /** + * @NodeAnotation(title="添加") + */ + public function add() + { + if ($this->request->isPost()) { + $post = $this->request->post(); + // 表单仅提交 title/status/remark;密钥三要素与统计字段一律服务端生成,忽略客户端传入 + unset($post['id'], $post['key'], $post['key_prefix'], $post['bind_admin_id'], $post['use_num'], $post['last_use_time']); + $rule = [ + 'title|密钥名称' => 'require|max:50', + ]; + $this->validate($post, $rule); + + // 明文密钥仅此一次出现在内存与响应中:不落库、不写日志 + $secretKey = 'sk-mcp-' . bin2hex(random_bytes(24)); + $post['key'] = hash('sha256', $secretKey); + $post['key_prefix'] = substr($secretKey, 0, 16); + $post['bind_admin_id'] = $this->getAdminId(); + try { + $save = $this->model->save($post); + } catch (\Exception $e) { + $this->error('保存失败:' . $e->getMessage()); + } + if ($save) { + // data 携带明文密钥,前端弹层展示一次后即丢弃 + $this->success('保存成功', ['secret_key' => $secretKey]); + } + $this->error('保存失败'); + } + + return $this->fetch(); + } + + /** + * @NodeAnotation(title="编辑") + */ + public function edit($id) + { + $row = $this->model->find($id); + empty($row) && $this->error('数据不存在'); + if ($this->request->isPost()) { + $post = $this->request->post(); + // 白名单:仅允许改 title/status/remark;key/key_prefix/bind_admin_id 及统计字段不可改 + $post = array_intersect_key($post, array_flip(['title', 'status', 'remark'])); + $rule = [ + 'title|密钥名称' => 'require|max:50', + ]; + $this->validate($post, $rule); + try { + $save = $row->save($post); + } catch (\Exception $e) { + $this->error('保存失败:' . $e->getMessage()); + } + $save ? $this->success('保存成功') : $this->error('保存失败'); + } + $this->assign('row', $row); + + return $this->fetch(); + } +} diff --git a/extend/base/admin/model/SystemMcpKeyBase.php b/extend/base/admin/model/SystemMcpKeyBase.php index 2fc151e..7b72346 100644 --- a/extend/base/admin/model/SystemMcpKeyBase.php +++ b/extend/base/admin/model/SystemMcpKeyBase.php @@ -4,7 +4,33 @@ namespace base\admin\model; use app\common\model\TimeModel; +/** + * @property int $id + * @property string $title 密钥名称 + * @property string $key 密钥SHA-256哈希 + * @property string $key_prefix 密钥前缀(展示用) + * @property int $bind_admin_id 绑定创建者ID + * @property int $status 状态 0:禁用,1:启用 + * @property string $remark 备注 + * @property int $use_num 调用次数 + * @property int $last_use_time 最后使用时间 + * @property int $create_time 创建时间 + */ class SystemMcpKeyBase extends TimeModel { + protected $name = 'system_mcp_key'; + protected $deleteTime = 'delete_time'; + + public const SELECT_LIST_STATUS = ['0' => '禁用', '1' => '启用']; + + /** + * 创建者(后台用户). + * 仅按需显式调用时才查询;列表页请勿用 withJoin 预载 + * (system_admin 与主表存在 id/status 等同名字段,join 会产生歧义)。 + */ + public function bindAdmin() + { + return $this->belongsTo('app\admin\model\SystemAdmin', 'bind_admin_id', 'id'); + } } diff --git a/extend/base/admin/view/system/mcp_key/_common.js b/extend/base/admin/view/system/mcp_key/_common.js new file mode 100644 index 0000000..2458be2 --- /dev/null +++ b/extend/base/admin/view/system/mcp_key/_common.js @@ -0,0 +1,11 @@ +var init = { + tableElem: '#currentTable', + tableRenderId: 'currentTableRenderId', + indexUrl: 'system.mcp_key/index', + addUrl: 'system.mcp_key/add' + location.search, + editUrl: 'system.mcp_key/edit', + readUrl: 'system.mcp_key/read', + deleteUrl: 'system.mcp_key/delete', + exportUrl: 'system.mcp_key/export', + modifyUrl: 'system.mcp_key/modify', +}; diff --git a/extend/base/admin/view/system/mcp_key/add.html b/extend/base/admin/view/system/mcp_key/add.html new file mode 100644 index 0000000..9d83673 --- /dev/null +++ b/extend/base/admin/view/system/mcp_key/add.html @@ -0,0 +1,40 @@ +