Bläddra i källkod

feat: 完善头部参数问题

lipan 1 vecka sedan
förälder
incheckning
1ff6f80736

+ 31 - 0
internal/handler/mountservicehandler.go

@@ -0,0 +1,31 @@
+// Code scaffolded by goctl. Safe to edit.
+
+package handler
+
+import (
+	"net/http"
+
+	"github.com/zeromicro/go-zero/rest/httpx"
+	"udeServer/internal/logic"
+	"udeServer/internal/svc"
+	"udeServer/internal/types"
+)
+
+// MountServiceHandler 启用服务联动挂载:POST /api/v1/services/:id/mount
+func MountServiceHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
+	return func(w http.ResponseWriter, r *http.Request) {
+		var req types.ServiceIdReq
+		if err := httpx.Parse(r, &req); err != nil {
+			httpx.ErrorCtx(r.Context(), w, err)
+			return
+		}
+
+		l := logic.NewMountServiceLogic(r.Context(), svcCtx)
+		resp, err := l.MountService(&req)
+		if err != nil {
+			httpx.ErrorCtx(r.Context(), w, err)
+		} else {
+			httpx.OkJsonCtx(r.Context(), w, resp)
+		}
+	}
+}

+ 31 - 0
internal/handler/unmountservicehandler.go

@@ -0,0 +1,31 @@
+// Code scaffolded by goctl. Safe to edit.
+
+package handler
+
+import (
+	"net/http"
+
+	"github.com/zeromicro/go-zero/rest/httpx"
+	"udeServer/internal/logic"
+	"udeServer/internal/svc"
+	"udeServer/internal/types"
+)
+
+// UnmountServiceHandler 停用服务联动卸载:POST /api/v1/services/:id/unmount
+func UnmountServiceHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
+	return func(w http.ResponseWriter, r *http.Request) {
+		var req types.ServiceIdReq
+		if err := httpx.Parse(r, &req); err != nil {
+			httpx.ErrorCtx(r.Context(), w, err)
+			return
+		}
+
+		l := logic.NewUnmountServiceLogic(r.Context(), svcCtx)
+		resp, err := l.UnmountService(&req)
+		if err != nil {
+			httpx.ErrorCtx(r.Context(), w, err)
+		} else {
+			httpx.OkJsonCtx(r.Context(), w, resp)
+		}
+	}
+}

+ 34 - 21
internal/logic/createservicelogic.go

@@ -45,18 +45,12 @@ func (l *CreateServiceLogic) CreateService(req *types.CreateServiceReq) (*types.
 		svcModel := l.svcCtx.ServiceModel.WithSession(session)
 		dirModel := l.svcCtx.DirectoryModel.WithSession(session)
 
-		// 从接入标识获取到秘钥
-		apiSecret, err := l.svcCtx.ApplicationCredModel.FindByApiKey(l.ctx, req.Iden)
-		fmt.Println("从接入标识获取到秘钥:", apiSecret)
-		if err != nil {
-			return fmt.Errorf("接入应用标识不存在: %w", err)
-		}
 		// 1. 插服务
 		svcData := &model.TService{
 			Name:        nullString(req.Name),
 			Ip:          nullString(req.Ip),
 			Port:        req.Port, // 端口 int 类型,int64 直传(0=未配置)
-			Iden:        nullString(apiSecret),
+			Iden:        nullString(req.Iden),
 			KeyIden:     nullString(req.KeyIden),
 			KeyAlgo:     nullString(req.KeyAlgo),
 			Status:      nullString(req.Status),
@@ -130,7 +124,8 @@ func (l *CreateServiceLogic) CreateService(req *types.CreateServiceReq) (*types.
 		}
 	} else if err := l.notifyFuseMount(created, dirList); err != nil {
 		l.Errorf("service created(id=%d), but fuse mount notify failed: %v", svcId, err)
-		warnings = append(warnings, fmt.Sprintf("服务已创建,但通知外挂程序挂载失败: %v", err))
+		// err 内容即失败目录的逐条消息(带目录名,分号分隔),直接进警告,不再包前缀段
+		warnings = append(warnings, err.Error())
 		resp.MountStatus = "failed"
 		if uerr := l.svcCtx.ServiceModel.UpdateMountStatus(l.ctx, svcId, "failed"); uerr != nil {
 			l.Errorf("update mount_status=failed failed(id=%d): %v", svcId, uerr)
@@ -167,14 +162,15 @@ func (l *CreateServiceLogic) notifyFuseBrush(svc *model.TService, dirs []string)
 	return dispatchFuseBrush(l.ctx, l.svcCtx, svc, dirs)
 }
 
-// doFuseRequest 发送发往外挂程序的 HTTP 请求,统一携带逐服务的 X-TOKEN 鉴权头。
-// X-TOKEN 的值 = 服务表 t_service 的 iden(接入标识)字段;外挂程序拿它与
-// 自己环境变量 TOKEN 配的值恒定时间比对(见 FuseDemo 的 NewAuthInterceptor),
-// 因此每台外挂机的 TOKEN 要配成"接在该机上的服务"的 iden 值。
+// doFuseRequest 发送发往外挂程序的 HTTP 请求,统一携带 X-TOKEN 鉴权头。
+// 服务表 t_service 的 iden 存的是接入应用的 apiKey;外挂机 TOKEN 环境变量配的是
+// 密钥值 apiSecret。这里按 apiKey 查接入应用表(t_application_credentials)换算出
+// apiSecret 再作为 X-TOKEN 发送,外挂程序拿它与自己环境变量 TOKEN 恒定时间比对
+// (见 FuseDemo 的 NewAuthInterceptor)。
 // client 按场景传:挂载/卸载/目录浏览用 fuseMountClient(10s),刷库用 fuseBrushClient(10min)。
 // 之所以不用 client.Post/client.Get 快捷方式:它们构造的请求没法逐个加 Header,
 // 必须先 http.NewRequest 再 client.Do。
-func doFuseRequest(client *http.Client, iden, method, url string, body io.Reader, contentType string) (*http.Response, error) {
+func doFuseRequest(ctx context.Context, svcCtx *svc.ServiceContext, client *http.Client, iden, method, url string, body io.Reader, contentType string) (*http.Response, error) {
 	req, err := http.NewRequest(method, url, body)
 	if err != nil {
 		return nil, err
@@ -182,7 +178,16 @@ func doFuseRequest(client *http.Client, iden, method, url string, body io.Reader
 	if contentType != "" {
 		req.Header.Set("Content-Type", contentType)
 	}
-	req.Header.Set("X-TOKEN", iden)
+	// 按 iden(apiKey) 查接入应用表,取真正的密钥值 apiSecret 作为 X-TOKEN。
+	// iden 为空时(目录树接口的 iden 是可选项,新增弹窗未选接入标识就点"选择目录")跳过:
+	// 目标接口 /path/lists 在外挂侧本就免鉴权,不设头也能通。
+	if iden != "" {
+		apiSecret, err := svcCtx.ApplicationCredModel.FindByApiKey(ctx, iden)
+		if err != nil {
+			return nil, fmt.Errorf("接入标识(%s)对应的应用不存在,无法获取密钥: %w", iden, err)
+		}
+		req.Header.Set("X-TOKEN", apiSecret)
+	}
 	return client.Do(req)
 }
 
@@ -232,7 +237,7 @@ func dispatchFuseBrush(ctx context.Context, svcCtx *svc.ServiceContext, svc *mod
 		if err != nil {
 			return fmt.Errorf("序列化刷库请求失败: %w", err)
 		}
-		resp, err := doFuseRequest(fuseBrushClient, svc.Iden.String, http.MethodPost, baseURL, bytes.NewReader(body), "application/json")
+		resp, err := doFuseRequest(ctx, svcCtx, fuseBrushClient, svc.Iden.String, http.MethodPost, baseURL, bytes.NewReader(body), "application/json")
 		if err != nil {
 			return fmt.Errorf("请求 %s 失败(外挂程序未启动?): %w", baseURL, err)
 		}
@@ -290,6 +295,9 @@ func (l *CreateServiceLogic) notifyFuseMount(svc *model.TService, dirs []string)
 	}
 
 	baseURL := fmt.Sprintf("http://%s:%d/api/fuseMount", ip, port) // %d:port 是 int64,数字占位符
+	// 逐目录尝试挂载,失败的目录各自记一条带目录名的消息后继续下一个,
+	// 全部尝试完才返回:多目录场景失败几个目录就聚合几条,成功目录不产生消息。
+	var msgs []string
 	for _, mount := range dirs {
 		dirLinuxPath := path.Clean(mount)
 		cipherDir := dirLinuxPath + "-cipher"
@@ -300,30 +308,35 @@ func (l *CreateServiceLogic) notifyFuseMount(svc *model.TService, dirs []string)
 		}
 		body, err := json.Marshal(payload)
 		if err != nil {
-			return fmt.Errorf("序列化挂载请求失败: %w", err)
+			msgs = append(msgs, fmt.Sprintf("挂载目录 %s 失败: 序列化挂载请求失败: %v", dirLinuxPath, err))
+			continue
 		}
 
 		// 发起 POST(DoSimple 记录 type=2 接口日志,父 Span 指向本次入站请求)
 		if _, callErr := audit.DoSimple(l.svcCtx.SpanRecorder, l.ctx, "FuseMount", baseURL, body,
 			func() (map[string]interface{}, int64, error) {
-				// doFuseRequest 注入 X-TOKEN=svc.Iden(接入标识);client.Post 快捷方式带不了自定义头,
+				// doFuseRequest 注入 X-TOKEN(按 iden 换算的 apiSecret);client.Post 快捷方式带不了自定义头,
 				// 裸发会被外挂鉴权拦截器 401 拒绝
-				resp, rerr := doFuseRequest(fuseMountClient, svc.Iden.String, http.MethodPost, baseURL, bytes.NewReader(body), "application/json")
+				resp, rerr := doFuseRequest(l.ctx, l.svcCtx, fuseMountClient, svc.Iden.String, http.MethodPost, baseURL, bytes.NewReader(body), "application/json")
 				if rerr != nil {
-					return nil, 0, fmt.Errorf("请求 %s 失败(外挂程序未启动?): %w", baseURL, rerr)
+					return nil, 0, fmt.Errorf("挂载目录 %s 失败(外挂程序未启动?): %w", dirLinuxPath, rerr)
 				}
 				// 读到 EOF 保证底层 TCP 连接池可以复用 Keep-Alive
 				raw, _ := io.ReadAll(resp.Body)
 				resp.Body.Close()
 				status := int64(resp.StatusCode)
 				if status >= http.StatusBadRequest {
-					return nil, status, fmt.Errorf("挂载目录 %s 失败,外挂程序返回 HTTP %d: %s", mount, status, string(raw))
+					return nil, status, fmt.Errorf("挂载目录 %s 失败,外挂程序返回 HTTP %d: %s", dirLinuxPath, status, string(raw))
 				}
 				return map[string]interface{}{"status": status, "body": string(raw)}, status, nil
 			}); callErr != nil {
-			return callErr
+			msgs = append(msgs, callErr.Error())
+			continue
 		}
 		l.Infof("fuse mount ok: %s -> %s", mount, mount+"-cipher")
 	}
+	if len(msgs) > 0 {
+		return fmt.Errorf("%s", strings.Join(msgs, "; "))
+	}
 	return nil
 }

+ 11 - 17
internal/logic/deleteservicelogic.go

@@ -45,9 +45,13 @@ func (l *DeleteServiceLogic) DeleteService(req *types.ServiceIdReq) (*types.Dele
 		return nil, err
 	}
 
-	// P1 联动卸载:先卸载后删库。卸载失败软处理(不阻塞删除,外挂机器可能已下线),
-	// 聚合成 unmountWarning 由前端弹黄色警告。
-	unmountWarning := l.notifyFuseUnmount(svc)
+	// P1 联动卸载:先卸载后删库,卸载成功是删除的前置条件(硬阻断)。
+	// 任一目录卸载失败(外挂不可达/返回错误)都中止删除,失败原因原样返回给前端,
+	// 避免服务记录删了、挂载还挂在外挂机上成为无人管理的孤儿挂载。
+	unmountErr := l.notifyFuseUnmount(svc)
+	if unmountErr != "" {
+		return nil, fmt.Errorf("删除已中止,请先解决挂载卸载问题后重试: %s", unmountErr)
+	}
 
 	err = l.svcCtx.DB.Transact(func(session sqlx.Session) error {
 		dirModel := l.svcCtx.DirectoryModel.WithSession(session)
@@ -66,20 +70,10 @@ func (l *DeleteServiceLogic) DeleteService(req *types.ServiceIdReq) (*types.Dele
 		return nil, err
 	}
 
-	resp := &types.DeleteResp{Ok: true}
-	if unmountWarning != "" {
-		resp.UnmountWarning = unmountWarning
-	}
-	return resp, nil
+	return &types.DeleteResp{Ok: true}, nil
 }
 
 // notifyFuseUnmount 删库前回调外挂程序(svc.Ip:svc.Port)的 GET /api/fuseUnMount 接口,
-// 逐目录卸载该服务全部 FUSE 挂载。
-// 前置判断:只有挂载状态 mount_status=mounted 的服务才真正调外挂卸载接口;
-// unmounted/failed 视为无活动挂载,直接跳过不发请求——避免删除"本来就没挂载"
-// 的服务时还要去外挂机器白跑一趟、机器不可达时弹一堆无意义的卸载失败警告。
-// 软处理:单个目录失败只记录并聚合,绝不阻塞删除流程(外挂机器可能已关机,
-// 服务记录必须还能删)。返回空串 = 全部卸载成功或无需卸载。
 func (l *DeleteServiceLogic) notifyFuseUnmount(svc *model.TService) string {
 	// MountStatus 是 sql.NullString 类型,.String 取其中的字符串值
 	if svc.MountStatus.String != "mounted" {
@@ -115,8 +109,8 @@ func (l *DeleteServiceLogic) notifyFuseUnmount(svc *model.TService) string {
 		if _, callErr := audit.DoSimple(l.svcCtx.SpanRecorder, l.ctx, "FuseUnMount", reqURL.String(),
 			audit.BuildRemoteGetRequest(http.MethodGet, reqURL.String()),
 			func() (map[string]interface{}, int64, error) {
-				// doFuseRequest 注入 X-TOKEN=svc.Iden(接入标识);client.Get 快捷方式带不了自定义头,裸发会被外挂 401 拒绝
-				resp, rerr := doFuseRequest(fuseMountClient, svc.Iden.String, http.MethodGet, reqURL.String(), nil, "") // 复用挂载逻辑里的 HTTP 客户端(同包变量)
+				// doFuseRequest 注入 X-TOKEN(按 iden 换算的 apiSecret);client.Get 快捷方式带不了自定义头,裸发会被外挂 401 拒绝
+				resp, rerr := doFuseRequest(l.ctx, l.svcCtx, fuseMountClient, svc.Iden.String, http.MethodGet, reqURL.String(), nil, "") // 复用挂载逻辑里的 HTTP 客户端(同包变量)
 				if rerr != nil {
 					return nil, 0, fmt.Errorf("卸载目录 %s 失败(外挂程序未启动?): %w", cleanMount, rerr)
 				}
@@ -136,5 +130,5 @@ func (l *DeleteServiceLogic) notifyFuseUnmount(svc *model.TService) string {
 	if len(msgs) == 0 {
 		return ""
 	}
-	return "服务已删除,但卸载挂载失败: " + strings.Join(msgs, "; ")
+	return "卸载挂载失败: " + strings.Join(msgs, "; ")
 }

+ 0 - 25
internal/logic/helpers.go

@@ -4,7 +4,6 @@ import (
 	"context"
 	"database/sql"
 	"errors"
-	"strings"
 	"time"
 
 	"udeServer/internal/apierr"
@@ -122,27 +121,3 @@ func toServiceResp(data *model.TService) *types.ServiceResp {
 		CreateAt:    timeValue(data.CreateAt),
 	}
 }
-
-func toListServiceResp(data *model.TService) *types.ServiceResp {
-	return &types.ServiceResp{
-		Id:          data.Id,
-		Name:        stringValue(data.Name),
-		Ip:          stringValue(data.Ip),
-		Port:        data.Port,
-		Iden:        maskSensitive(stringValue(data.Iden)),
-		KeyIden:     stringValue(data.KeyIden),
-		KeyAlgo:     stringValue(data.KeyAlgo),
-		Status:      stringValue(data.Status),
-		MountStatus: stringValue(data.MountStatus),
-		Brush:       data.Brush,
-		CreateAt:    timeValue(data.CreateAt),
-	}
-}
-
-// maskSensitive 掩码展示:前 4 后 4 明文,中间打星;长度不大于 8 时原样返回。
-func maskSensitive(value string) string {
-	if len(value) <= 8 {
-		return value
-	}
-	return value[:4] + strings.Repeat("*", len(value)-8) + value[len(value)-4:]
-}

+ 2 - 2
internal/logic/listdirchildrenlogic.go

@@ -116,8 +116,8 @@ func (l *ListDirChildrenLogic) notifyListDirChildren(ip, port, dirs, iden string
 	reqDesc := audit.BuildRemoteGetRequest(http.MethodGet, finalURL)
 	result, callErr := audit.DoSimple(l.svcCtx.SpanRecorder, l.ctx, "ListDirChildren", finalURL, reqDesc,
 		func() (*types.DirChildrenResp, int64, error) {
-			// doFuseRequest 注入 X-TOKEN=iden;外挂 /path/lists 已免鉴权,带上无害,统一收口避免遗漏
-			resp, rerr := doFuseRequest(fuseMountClient, iden, http.MethodGet, finalURL, nil, "")
+			// doFuseRequest 注入 X-TOKEN(按 iden 换算的 apiSecret);外挂 /path/lists 已免鉴权,带上无害,统一收口避免遗漏
+			resp, rerr := doFuseRequest(l.ctx, l.svcCtx, fuseMountClient, iden, http.MethodGet, finalURL, nil, "")
 			if rerr != nil {
 				return nil, 0, fmt.Errorf("请求 %s 失败: %w", baseURL, rerr)
 			}

+ 1 - 1
internal/logic/listserviceslogic.go

@@ -40,7 +40,7 @@ func (l *ListServicesLogic) ListServices(req *types.ListServiceReq) (*types.Serv
 		Total: total,
 	}
 	for i := range list {
-		resp.List = append(resp.List, *toListServiceResp(&list[i]))
+		resp.List = append(resp.List, *toServiceResp(&list[i]))
 	}
 
 	return resp, nil

+ 1 - 1
internal/logic/mountservicelogic.go

@@ -86,7 +86,7 @@ func (l *MountServiceLogic) MountService(req *types.ServiceIdReq) (*types.Servic
 		baseURL := fmt.Sprintf("http://%s:%d/api/fuseMount", ip, port) // %d:port 是 int64
 		if _, callErr := audit.DoSimple(l.svcCtx.SpanRecorder, l.ctx, "FuseMount", baseURL, body,
 			func() (map[string]interface{}, int64, error) {
-				resp, rerr := doFuseRequest(fuseMountClient, svc.Iden.String, http.MethodPost, baseURL, bytes.NewReader(body), "application/json")
+				resp, rerr := doFuseRequest(l.ctx, l.svcCtx, fuseMountClient, svc.Iden.String, http.MethodPost, baseURL, bytes.NewReader(body), "application/json")
 				if rerr != nil {
 					return nil, 0, fmt.Errorf("挂载目录 %s 失败(外挂程序未启动?): %w", dirLinuxPath, rerr)
 				}

+ 2 - 2
internal/logic/unmountservicelogic.go

@@ -72,11 +72,11 @@ func (l *UnmountServiceLogic) UnmountService(req *types.ServiceIdReq) (*types.Se
 				reqURL.RawQuery = q.Encode()
 				finalURL := reqURL.String()
 				// DoSimple 记 type=2 审计日志(GET 用 url 描述请求原文),
-				// doFuseRequest 注入 X-TOKEN=iden(外挂侧与环境变量 TOKEN 恒定时间比对)
+				// doFuseRequest 注入 X-TOKEN(按 iden 换算的 apiSecret,外挂侧与环境变量 TOKEN 恒定时间比对)
 				if _, callErr := audit.DoSimple(l.svcCtx.SpanRecorder, l.ctx, "FuseUnMount", finalURL,
 					audit.BuildRemoteGetRequest(http.MethodGet, finalURL),
 					func() (map[string]interface{}, int64, error) {
-						resp, rerr := doFuseRequest(fuseMountClient, svc.Iden.String, http.MethodGet, finalURL, nil, "")
+						resp, rerr := doFuseRequest(l.ctx, l.svcCtx, fuseMountClient, svc.Iden.String, http.MethodGet, finalURL, nil, "")
 						if rerr != nil {
 							return nil, 0, fmt.Errorf("卸载目录 %s 失败(外挂程序未启动?): %w", cleanMount, rerr)
 						}

+ 18 - 10
internal/logic/updateservicelogic.go

@@ -84,12 +84,19 @@ func (l *UpdateServiceLogic) UpdateService(req *types.UpdateServiceReq) (*types.
 		mountWarning, mountStatus = l.applyMountChanges(svc, oldDirs, newDirs, req)
 	}
 
-	// 编辑联动失败时,服务状态与挂载结果强绑定(与新增服务同规则):
-	// 挂载/卸载任一失败 → mount_status=failed 且 status 同步置"停用",
-	// 避免"启用+failed"的错位组合;req.Directories==nil 的调用方(状态开关)不受影响。
+	// 编辑联动时,服务状态与挂载结果强绑定(与新增/启用服务同规则):
+	// 联动挂载成功 → mount_status=mounted 且 status 同步置"启用"
+	//   (否则挂载状态变了、服务状态没变,好端端挂上的服务在列表里仍显示停用);
+	// 联动任一失败 → mount_status=failed 且 status 同步置"停用",避免"启用+failed"错位;
+	// 没有挂载动作(unmounted:已挂载服务换机全卸 / 未挂载服务清空目录) → status 保持原值。
+	// req.Directories==nil 的调用方(状态开关)不受影响。
 	finalStatus := req.Status
-	if req.Directories != nil && mountStatus == "failed" {
-		finalStatus = "停用"
+	if req.Directories != nil {
+		if mountStatus == "mounted" {
+			finalStatus = "启用"
+		} else if mountStatus == "failed" {
+			finalStatus = "停用"
+		}
 	}
 
 	// 事务:更新服务 + 目录全删全插(与新增服务同款后端收口模式,任一步失败整体回滚)
@@ -244,8 +251,8 @@ func (l *UpdateServiceLogic) fuseMountOne(iden, ip string, port int64, mount, ik
 	// DoSimple 记录 type=2 接口日志,父 Span 指向本次入站请求
 	if _, callErr := audit.DoSimple(l.svcCtx.SpanRecorder, l.ctx, "FuseMount", baseURL, body,
 		func() (map[string]interface{}, int64, error) {
-			// doFuseRequest 注入 X-TOKEN=iden(接入标识);client.Post 快捷方式带不了自定义头,裸发会被外挂 401 拒绝
-			resp, rerr := doFuseRequest(fuseMountClient, iden, http.MethodPost, baseURL, bytes.NewReader(body), "application/json")
+			// doFuseRequest 注入 X-TOKEN(按 iden 换算的 apiSecret);client.Post 快捷方式带不了自定义头,裸发会被外挂 401 拒绝
+			resp, rerr := doFuseRequest(l.ctx, l.svcCtx, fuseMountClient, iden, http.MethodPost, baseURL, bytes.NewReader(body), "application/json")
 			if rerr != nil {
 				return nil, 0, fmt.Errorf("挂载目录 %s 失败(外挂程序未启动?): %w", dirLinuxPath, rerr)
 			}
@@ -273,14 +280,15 @@ func (l *UpdateServiceLogic) fuseUnmountOne(iden, ip string, port int64, mount s
 		return fmt.Sprintf("解析 URL 失败: %v", uerr)
 	}
 	q := reqURL.Query()
-	q.Set("mount", cleanMount) // q.Encode() 自动做 URL 编码(/ → %2F),外挂侧 Query().Get 自动解码还原
+	q.Set("mount", cleanMount)   // q.Encode() 自动做 URL 编码(/ → %2F),外挂侧 Query().Get 自动解码还原
+	reqURL.RawQuery = q.Encode() // 关键:Query() 返回的是副本,Set 只改副本;必须写回 RawQuery 才真正拼进 URL(停用/删除卸载同款写法)
 	finalURL := reqURL.String()
 	// DoSimple 记录 type=2 接口日志,父 Span 指向本次入站请求(GET 请求用 url 描述请求原文)
 	if _, callErr := audit.DoSimple(l.svcCtx.SpanRecorder, l.ctx, "FuseUnMount", finalURL,
 		audit.BuildRemoteGetRequest(http.MethodGet, finalURL),
 		func() (map[string]interface{}, int64, error) {
-			// doFuseRequest 注入 X-TOKEN=iden(接入标识);client.Get 快捷方式带不了自定义头,裸发会被外挂 401 拒绝
-			resp, rerr := doFuseRequest(fuseMountClient, iden, http.MethodGet, finalURL, nil, "")
+			// doFuseRequest 注入 X-TOKEN(按 iden 换算的 apiSecret);client.Get 快捷方式带不了自定义头,裸发会被外挂 401 拒绝
+			resp, rerr := doFuseRequest(l.ctx, l.svcCtx, fuseMountClient, iden, http.MethodGet, finalURL, nil, "")
 			if rerr != nil {
 				return nil, 0, fmt.Errorf("卸载目录 %s 失败(外挂程序未启动?): %w", cleanMount, rerr)
 			}