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

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
}