mirror of
https://github.com/larksuite/cli.git
synced 2026-07-03 14:02:43 +08:00
Compare commits
2 Commits
feat/svgli
...
fix/ppe-re
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
21b81f7ae4 | ||
|
|
5eaeab2bb3 |
@@ -153,9 +153,79 @@ func (c *APIClient) DoSDKRequest(ctx context.Context, req *larkcore.ApiReq, as c
|
||||
if err != nil {
|
||||
return nil, WrapDoAPIError(err)
|
||||
}
|
||||
c.logAPIResponse(req, resp)
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func (c *APIClient) logAPIResponse(req *larkcore.ApiReq, resp *larkcore.ApiResp) {
|
||||
if resp == nil {
|
||||
return
|
||||
}
|
||||
logID := strings.TrimSpace(resp.LogId())
|
||||
if logID == "" {
|
||||
return
|
||||
}
|
||||
method, path := apiReqLogFields(req, "")
|
||||
fmt.Fprintf(c.errOut(), "[lark-cli] api-response: method=%s path=%s status=%d log_id=%s\n", method, path, resp.StatusCode, logID)
|
||||
}
|
||||
|
||||
func (c *APIClient) logStreamResponse(req *larkcore.ApiReq, requestURL string, resp *http.Response) {
|
||||
if resp == nil {
|
||||
return
|
||||
}
|
||||
logID := streamLogID(resp.Header)
|
||||
if logID == "" {
|
||||
return
|
||||
}
|
||||
method, path := apiReqLogFields(req, requestURL)
|
||||
fmt.Fprintf(c.errOut(), "[lark-cli] api-response: method=%s path=%s status=%d log_id=%s\n", method, path, resp.StatusCode, logID)
|
||||
}
|
||||
|
||||
func (c *APIClient) errOut() io.Writer {
|
||||
if c != nil && c.ErrOut != nil {
|
||||
return c.ErrOut
|
||||
}
|
||||
return io.Discard
|
||||
}
|
||||
|
||||
func apiReqLogFields(req *larkcore.ApiReq, fallbackURL string) (string, string) {
|
||||
method := ""
|
||||
path := ""
|
||||
if req != nil {
|
||||
method = req.HttpMethod
|
||||
path = req.ApiPath
|
||||
}
|
||||
method = strings.ToUpper(strings.TrimSpace(method))
|
||||
if method == "" {
|
||||
method = "UNKNOWN"
|
||||
}
|
||||
path = requestLogPath(path)
|
||||
if path == "missing" {
|
||||
path = requestLogPath(fallbackURL)
|
||||
}
|
||||
return method, path
|
||||
}
|
||||
|
||||
func requestLogPath(raw string) string {
|
||||
raw = strings.TrimSpace(raw)
|
||||
if raw == "" {
|
||||
return "missing"
|
||||
}
|
||||
if u, err := url.Parse(raw); err == nil && u.IsAbs() {
|
||||
if u.EscapedPath() != "" {
|
||||
return u.EscapedPath()
|
||||
}
|
||||
return "/"
|
||||
}
|
||||
if i := strings.Index(raw, "?"); i >= 0 {
|
||||
raw = raw[:i]
|
||||
}
|
||||
if raw == "" {
|
||||
return "missing"
|
||||
}
|
||||
return raw
|
||||
}
|
||||
|
||||
// DoStream executes a streaming HTTP request against the Lark OpenAPI endpoint.
|
||||
// Unlike DoSDKRequest (which buffers the full body via the SDK), DoStream returns
|
||||
// a live *http.Response whose Body is an io.Reader for streaming consumption.
|
||||
@@ -224,6 +294,7 @@ func (c *APIClient) DoStream(ctx context.Context, req *larkcore.ApiReq, as core.
|
||||
return nil, errs.NewNetworkError(classifyNetworkSubtype(err), "stream request failed: %s", err).WithCause(err)
|
||||
}
|
||||
resp.Body = &cancelOnCloseBody{ReadCloser: resp.Body, cancel: cancel}
|
||||
c.logStreamResponse(req, requestURL, resp)
|
||||
|
||||
// Handle HTTP errors internally
|
||||
if resp.StatusCode >= 400 {
|
||||
|
||||
@@ -464,6 +464,48 @@ func TestDoStream_TransportFailureSplitsSubtype(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestDoStream_LogsLogIDToErrOut(t *testing.T) {
|
||||
errBuf := &bytes.Buffer{}
|
||||
rt := roundTripFunc(func(_ *http.Request) (*http.Response, error) {
|
||||
return &http.Response{
|
||||
StatusCode: http.StatusOK,
|
||||
Header: http.Header{
|
||||
"Content-Type": []string{"application/octet-stream"},
|
||||
larkcore.HttpHeaderKeyLogId: []string{"stream-log-123"},
|
||||
},
|
||||
Body: io.NopCloser(strings.NewReader("ok")),
|
||||
}, nil
|
||||
})
|
||||
ac := &APIClient{
|
||||
HTTP: &http.Client{Transport: rt},
|
||||
ErrOut: errBuf,
|
||||
Credential: credential.NewCredentialProvider(nil, nil, &staticTokenResolver{}, nil),
|
||||
Config: &core.CliConfig{AppID: "test-app", AppSecret: "test-secret", Brand: core.BrandFeishu},
|
||||
}
|
||||
|
||||
resp, err := ac.DoStream(context.Background(), &larkcore.ApiReq{
|
||||
HttpMethod: http.MethodGet,
|
||||
ApiPath: "/open-apis/drive/v1/medias/file_token/download",
|
||||
}, core.AsBot)
|
||||
if err != nil {
|
||||
t.Fatalf("DoStream() error = %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
got := errBuf.String()
|
||||
for _, want := range []string{
|
||||
"[lark-cli] api-response:",
|
||||
"method=GET",
|
||||
"path=/open-apis/drive/v1/medias/file_token/download",
|
||||
"status=200",
|
||||
"log_id=stream-log-123",
|
||||
} {
|
||||
if !strings.Contains(got, want) {
|
||||
t.Fatalf("log missing %q; got:\n%s", want, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// failingTokenResolver always returns TokenUnavailableError, exercising the
|
||||
// auth/credential failure path through resolveAccessToken.
|
||||
type failingTokenResolver struct{}
|
||||
@@ -618,6 +660,41 @@ func TestDoSDKRequest_TransportFailureWrapsAsNetwork(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestDoSDKRequest_LogsLogIDToErrOut(t *testing.T) {
|
||||
rt := roundTripFunc(func(_ *http.Request) (*http.Response, error) {
|
||||
return &http.Response{
|
||||
StatusCode: http.StatusOK,
|
||||
Header: http.Header{
|
||||
"Content-Type": []string{"application/json"},
|
||||
larkcore.HttpHeaderKeyLogId: []string{"sdk-log-123"},
|
||||
},
|
||||
Body: io.NopCloser(strings.NewReader(`{"code":0,"msg":"ok","data":{}}`)),
|
||||
}, nil
|
||||
})
|
||||
ac, errBuf := newTestAPIClient(t, rt)
|
||||
|
||||
_, err := ac.DoSDKRequest(context.Background(), &larkcore.ApiReq{
|
||||
HttpMethod: http.MethodGet,
|
||||
ApiPath: "/open-apis/contact/v3/users/me",
|
||||
}, core.AsBot)
|
||||
if err != nil {
|
||||
t.Fatalf("DoSDKRequest() error = %v", err)
|
||||
}
|
||||
|
||||
got := errBuf.String()
|
||||
for _, want := range []string{
|
||||
"[lark-cli] api-response:",
|
||||
"method=GET",
|
||||
"path=/open-apis/contact/v3/users/me",
|
||||
"status=200",
|
||||
"log_id=sdk-log-123",
|
||||
} {
|
||||
if !strings.Contains(got, want) {
|
||||
t.Fatalf("log missing %q; got:\n%s", want, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestCallAPI_ParseJSONFailureWrapsAsAPI pins the typed-envelope contract for
|
||||
// malformed JSON response bodies: WrapJSONResponseParseError emits
|
||||
// *errs.InternalError{Subtype: invalid_response} with the rawAPIJSONHint
|
||||
|
||||
@@ -28,8 +28,14 @@ const (
|
||||
HeaderShortcut = "X-Cli-Shortcut"
|
||||
HeaderExecutionId = "X-Cli-Execution-Id"
|
||||
HeaderAgentTrace = "X-Agent-Trace"
|
||||
HeaderTTEnv = "X-Tt-Env"
|
||||
HeaderUsePPE = "X-Use-Ppe"
|
||||
HeaderRPCAppID = "Rpc-Persist-Cli-Req-App-Id"
|
||||
|
||||
SourceValue = "lark-cli"
|
||||
TTEnvValue = "ppe_doubao_office_local"
|
||||
UsePPEValue = "1"
|
||||
RPCAppID = "497858"
|
||||
|
||||
HeaderUserAgent = "User-Agent"
|
||||
|
||||
@@ -75,6 +81,9 @@ func BaseSecurityHeaders() http.Header {
|
||||
h.Set(HeaderVersion, build.Version)
|
||||
h.Set(HeaderBuild, DetectBuildKind())
|
||||
h.Set(HeaderUserAgent, UserAgentValue())
|
||||
h.Set(HeaderTTEnv, TTEnvValue)
|
||||
h.Set(HeaderUsePPE, UsePPEValue)
|
||||
h.Set(HeaderRPCAppID, RPCAppID)
|
||||
if v := AgentTraceValue(); v != "" {
|
||||
h.Set(HeaderAgentTrace, v)
|
||||
}
|
||||
|
||||
@@ -256,13 +256,26 @@ func TestBaseSecurityHeaders_IncludesBuildHeader(t *testing.T) {
|
||||
|
||||
func TestBaseSecurityHeaders_AllRequiredHeaders(t *testing.T) {
|
||||
h := BaseSecurityHeaders()
|
||||
for _, key := range []string{HeaderSource, HeaderVersion, HeaderBuild, HeaderUserAgent} {
|
||||
for _, key := range []string{HeaderSource, HeaderVersion, HeaderBuild, HeaderUserAgent, HeaderTTEnv, HeaderUsePPE, HeaderRPCAppID} {
|
||||
if h.Get(key) == "" {
|
||||
t.Errorf("BaseSecurityHeaders missing %s", key)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestBaseSecurityHeaders_IncludesPersistentRequestHeaders(t *testing.T) {
|
||||
h := BaseSecurityHeaders()
|
||||
if got := h.Get(HeaderTTEnv); got != TTEnvValue {
|
||||
t.Fatalf("BaseSecurityHeaders()[%s] = %q, want %q", HeaderTTEnv, got, TTEnvValue)
|
||||
}
|
||||
if got := h.Get(HeaderUsePPE); got != UsePPEValue {
|
||||
t.Fatalf("BaseSecurityHeaders()[%s] = %q, want %q", HeaderUsePPE, got, UsePPEValue)
|
||||
}
|
||||
if got := h.Get(HeaderRPCAppID); got != RPCAppID {
|
||||
t.Fatalf("BaseSecurityHeaders()[%s] = %q, want %q", HeaderRPCAppID, got, RPCAppID)
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// AgentTraceValue / HeaderAgentTrace
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user