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 }