Compare commits

..

2 Commits

Author SHA1 Message Date
wangweiming
21b81f7ae4 fix: update ppe request headers
Change-Id: I7ea680f3f553646b41b0591b701eb14057f6c11d
2026-06-09 21:19:40 +08:00
wangweiming
5eaeab2bb3 feat: add PPE headers and log IDs
Inject PPE request headers into base CLI headers, log OpenAPI response log IDs, and point Feishu endpoints at the pre environment for this branch.

Change-Id: Id8a5328796ab8cdfa25147daace976e2fa242017
2026-06-08 21:41:57 +08:00
6 changed files with 172 additions and 43 deletions

View File

@@ -2,46 +2,6 @@
All notable changes to this project will be documented in this file.
## [v1.0.49] - 2026-06-08
### Features
- **events**: Add whiteboard event domain with per-board subscription (#1265)
- **im**: Support feed group (#1102)
- **im**: Add feed shortcut create, list, and remove shortcuts (#1273)
- **im**: Format feed group error handling (#1308)
- **im**: Return typed error envelopes across the im domain (#1230)
- **base**: Emit typed error envelopes across the base domain (#1248)
- **calendar**: Emit typed error envelopes across the calendar domain (#1232)
- **task**: Emit typed error envelopes across the task domain (#1231)
- **okr,whiteboard**: Emit typed error envelopes across both domains (#1236)
- **minutes,vc**: Emit typed error envelopes across both domains (#1234)
- **markdown**: Harden create upload failures (#1325)
- **drive**: Harden inspect shortcut failures (#1324)
- **slides**: Add IconPark lookup for Lark slides (#1123)
- **doc**: Remove docs v1 API (#1291)
- **cli**: Add `skills` command to read embedded skill content (#1318)
- **cli**: Fetch official skills index (#1301)
- **shared**: Document relative-path-only file arguments (#1319)
- **scopes**: Clear `recommend.allow` scope auto-approve overrides (#1272)
- **shortcuts**: Check shortcut example commands against the live CLI tree (#1244)
### Bug Fixes
- **events**: Keep bounded event consume runs alive after stdin EOF (#1285)
- **drive**: Use docs secure label read scope (#1281)
### Documentation
- **approval**: Restructure skill with intent table and scope boundaries (#1307)
- **skills**: Tighten drive and markdown guardrails (#1326)
- **skills**: Optimize calendar, vc, and minutes skill guidance (#1269)
- **markdown**: Add markdown domain template (#1293)
- **markdown**: Improve lark-markdown skill guidance (#1279)
- **doc**: Improve lark-doc skill guidance (#1283)
- **wiki**: Optimize skill guidance and routing boundaries (#1275)
- **slides**: Tighten routing/boundary and reconcile in-slide whiteboard (#1169)
## [v1.0.48] - 2026-06-04
### Features
@@ -1066,7 +1026,6 @@ Bundled AI agent skills for intelligent assistance:
- Bilingual documentation (English & Chinese).
- CI/CD pipelines: linting, testing, coverage reporting, and automated releases.
[v1.0.49]: https://github.com/larksuite/cli/releases/tag/v1.0.49
[v1.0.48]: https://github.com/larksuite/cli/releases/tag/v1.0.48
[v1.0.47]: https://github.com/larksuite/cli/releases/tag/v1.0.47
[v1.0.46]: https://github.com/larksuite/cli/releases/tag/v1.0.46

View File

@@ -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 {

View File

@@ -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

View File

@@ -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)
}

View File

@@ -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
// ---------------------------------------------------------------------------

View File

@@ -1,6 +1,6 @@
{
"name": "@larksuite/cli",
"version": "1.0.49",
"version": "1.0.48",
"description": "The official CLI for Lark/Feishu open platform",
"bin": {
"lark-cli": "scripts/run.js"