You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
107 lines
3.9 KiB
107 lines
3.9 KiB
package user
|
|
|
|
import (
|
|
"time"
|
|
|
|
"yunyan/comm"
|
|
"yunyan/pb"
|
|
|
|
"github.com/golang-jwt/jwt/v5"
|
|
)
|
|
|
|
/*
|
|
签发 MCP 访问令牌。
|
|
|
|
## 为什么需要它
|
|
|
|
EMAI 助手在百炼那边调我们的 MCP 工具(查会议纪要、查待办…)时,MCP 必须知道
|
|
「这是哪个用户」。但这条链路上没有任何一处能天然带上身份:
|
|
|
|
- 客户端连百炼只带**百炼的 API Key**,没有把用户自己的 JWT 交出去;
|
|
- 百炼调 MCP 时的 `Authorization` 头是控制台里**配死的**,所有用户共用一个值。
|
|
|
|
唯一可行的通道是 Start 指令的 `biz_params.user_defined_params`——协议文档明确说
|
|
它是「透传给 agent 和 mcp 服务的参数」,会作为工具参数交到 MCP 手里。
|
|
|
|
⚠️ 实测确认过(2026-09-07):往 `biz_params` 里塞 `rag_options` 之类协议没定义的
|
|
字段,服务端**静默丢弃**(对照组塞一个纯属虚构的字段,服务端同样回 Started),
|
|
所以只能用协议已有的这三个字段,不能自创。
|
|
|
|
## 为什么不直接用登录 JWT
|
|
|
|
因为这条路要经过百炼(第三方)。登录 JWT 等价于账号的全部权限,一旦落到日志里
|
|
或被第三方留存,用户的一切接口都能被调用。本令牌**只用于 MCP、且短时效**,
|
|
即使泄露,能做的也只有读该用户的会议纪要/待办这类只读工具。
|
|
|
|
## 为什么用独立密钥而不是 gateway 的 TokenKey
|
|
|
|
同一把密钥签出来的令牌,gateway 也会认——那就等于又造了一把万能钥匙,
|
|
「限定用途」这件事就白做了。所以用 `McpTokenKey`,MCP 侧用同一个值校验,
|
|
gateway 不认识它。
|
|
*/
|
|
|
|
// mcpTokenTTL 令牌有效期。
|
|
//
|
|
// 取 2 小时是个折中:太短会让一次长对话中途失效(客户端得在会话里重签、
|
|
// 而 Start 的参数是握手时定死的,改不了);太长则削弱了「短时效」本身的意义。
|
|
// 客户端应在过期前重新申请,别等报错再补。
|
|
const mcpTokenTTL = 2 * time.Hour
|
|
|
|
// mcpTokenAudience 令牌用途标记。MCP 侧必须校验它,否则将来若有别的地方
|
|
// 也用 McpTokenKey 签东西,那些令牌会被 MCP 一并接受。
|
|
const mcpTokenAudience = "mcp"
|
|
|
|
// @Summary 申请 MCP 访问令牌
|
|
// @Description 签发短时效、仅限 MCP 工具调用的令牌,供客户端放进百炼会话的 biz_params 透传
|
|
// @Tags User
|
|
// @Accept json
|
|
// @Produce json
|
|
// @Security BearerAuth
|
|
// @Param user body pb.UserGetMcpTokenReq true "请求数据"
|
|
// @Success 200 {object} comm.HttpResult{data=pb.UserGetMcpTokenResp} "响应数据"
|
|
// @Router /api/home/user_getmcptoken [post]
|
|
func (this *apiComp) GetMcpToken(session comm.IUserSession, req *pb.UserGetMcpTokenReq) (resp *pb.UserGetMcpTokenResp, errdata *pb.ErrorData) {
|
|
uid := session.GetUserId()
|
|
if uid == "" {
|
|
errdata = &pb.ErrorData{
|
|
Code: pb.ErrorCode_NoLogin,
|
|
Message: "未登录",
|
|
}
|
|
return
|
|
}
|
|
|
|
key := this.module.options.McpTokenKey
|
|
if key == "" {
|
|
// 没配就直说,不要签一个空密钥的令牌。
|
|
// 空密钥签出来的东西谁都能伪造,比功能不可用严重得多。
|
|
errdata = &pb.ErrorData{
|
|
Code: pb.ErrorCode_SystemError,
|
|
Message: "服务端未配置 McpTokenKey",
|
|
}
|
|
return
|
|
}
|
|
|
|
expiresAt := time.Now().Add(mcpTokenTTL)
|
|
// 与 gateway/MCP 同一套口径:uid 放在 RegisteredClaims 的 ID 字段。
|
|
token := jwt.NewWithClaims(jwt.SigningMethodHS256, &jwt.RegisteredClaims{
|
|
ID: uid,
|
|
Audience: jwt.ClaimStrings{mcpTokenAudience},
|
|
ExpiresAt: jwt.NewNumericDate(expiresAt),
|
|
IssuedAt: jwt.NewNumericDate(time.Now()),
|
|
})
|
|
signed, err := token.SignedString([]byte(key))
|
|
if err != nil {
|
|
this.module.Errorf("GetMcpToken uid:%s 签发失败 err:%v", uid, err)
|
|
errdata = &pb.ErrorData{
|
|
Code: pb.ErrorCode_SystemError,
|
|
Message: "令牌签发失败",
|
|
}
|
|
return
|
|
}
|
|
|
|
resp = &pb.UserGetMcpTokenResp{
|
|
Token: signed,
|
|
Expiresat: expiresAt.Unix(),
|
|
}
|
|
return
|
|
}
|
|
|