mirror of
https://github.com/larksuite/cli.git
synced 2026-07-03 22:24:31 +08:00
Compare commits
2 Commits
v1.0.49
...
fix/ppe-re
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
21b81f7ae4 | ||
|
|
5eaeab2bb3 |
41
CHANGELOG.md
41
CHANGELOG.md
@@ -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
|
||||
|
||||
@@ -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
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user