mirror of
https://github.com/larksuite/cli.git
synced 2026-08-03 08:32:46 +08:00
fix(base): handle paginated agent lists
This commit is contained in:
@@ -91,19 +91,19 @@ func listTasks(ctx context.Context, rt iagents.Runtime, contextID string, page i
|
||||
query["limit"] = strconv.Itoa(page.Size)
|
||||
}
|
||||
putQuery(query, "state", p.State)
|
||||
got, err := callPayload[[]adapterTask](ctx, rt, "GET", agentRoot(p.BaseToken)+"/tasks", query, nil)
|
||||
got, err := callPayload[adapterTaskList](ctx, rt, "GET", agentRoot(p.BaseToken)+"/tasks", query, nil)
|
||||
if err != nil {
|
||||
return nil, iagents.PageInfo{}, err
|
||||
}
|
||||
out := make([]iagents.TaskSummary, 0, len(got))
|
||||
for _, item := range got {
|
||||
out := make([]iagents.TaskSummary, 0, len(got.Tasks))
|
||||
for _, item := range got.Tasks {
|
||||
summary, err := mapTaskSummary(item)
|
||||
if err != nil {
|
||||
return nil, iagents.PageInfo{}, err
|
||||
}
|
||||
out = append(out, summary)
|
||||
}
|
||||
return out, iagents.PageInfo{}, nil
|
||||
return out, iagents.PageInfo{HasMore: got.HasMore, NextToken: got.NextCursor}, nil
|
||||
}
|
||||
|
||||
func cancelTask(ctx context.Context, rt iagents.Runtime, taskID string) error {
|
||||
@@ -132,19 +132,19 @@ func listContexts(ctx context.Context, rt iagents.Runtime, page iagents.PagePara
|
||||
query["limit"] = strconv.Itoa(page.Size)
|
||||
}
|
||||
putQuery(query, "status", p.Status)
|
||||
got, err := callPayload[[]adapterContext](ctx, rt, "GET", agentRoot(p.BaseToken)+"/contexts", query, nil)
|
||||
got, err := callPayload[adapterContextList](ctx, rt, "GET", agentRoot(p.BaseToken)+"/contexts", query, nil)
|
||||
if err != nil {
|
||||
return nil, iagents.PageInfo{}, err
|
||||
}
|
||||
out := make([]iagents.ContextSummary, 0, len(got))
|
||||
for _, item := range got {
|
||||
out := make([]iagents.ContextSummary, 0, len(got.Contexts))
|
||||
for _, item := range got.Contexts {
|
||||
mapped, err := mapContextSummary(item)
|
||||
if err != nil {
|
||||
return nil, iagents.PageInfo{}, err
|
||||
}
|
||||
out = append(out, mapped)
|
||||
}
|
||||
return out, iagents.PageInfo{}, nil
|
||||
return out, iagents.PageInfo{HasMore: got.HasMore, NextToken: got.NextCursor}, nil
|
||||
}
|
||||
|
||||
func getContext(ctx context.Context, rt iagents.Runtime, contextID string) (*iagents.ContextDetail, error) {
|
||||
|
||||
@@ -289,6 +289,66 @@ func TestTaskHooksAndMapping(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestListHooksMapPaginationEnvelope(t *testing.T) {
|
||||
rt := &fakeRuntime{
|
||||
params: map[string]string{"base_token": "b1", "state": "done", "status": "active"},
|
||||
responses: []json.RawMessage{
|
||||
dataResponse(t, `{"tasks":[{"task_id":"t1","context_id":"c1","state":"done","updated_at":1710000060}],"has_more":true,"next_cursor":"task-next"}`),
|
||||
dataResponse(t, `{"contexts":[{"context_id":"c1","title":"Quarterly plan","created_at":1710000000,"updated_at":1710000060}],"has_more":true,"next_cursor":"context-next"}`),
|
||||
},
|
||||
}
|
||||
|
||||
tasks, taskPage, err := assistantSpec.ListTasks.Handler(context.Background(), rt, "c1", iagents.PageParams{Size: 1})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(tasks) != 1 || taskPage != (iagents.PageInfo{HasMore: true, NextToken: "task-next"}) {
|
||||
t.Fatalf("tasks=%+v page=%+v", tasks, taskPage)
|
||||
}
|
||||
|
||||
contexts, contextPage, err := assistantSpec.ListContexts.Handler(context.Background(), rt, iagents.PageParams{Size: 1})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(contexts) != 1 || contextPage != (iagents.PageInfo{HasMore: true, NextToken: "context-next"}) {
|
||||
t.Fatalf("contexts=%+v page=%+v", contexts, contextPage)
|
||||
}
|
||||
}
|
||||
|
||||
func TestListHooksRejectMalformedPaginationEnvelope(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
payload string
|
||||
contexts bool
|
||||
}{
|
||||
{name: "null task response", payload: `null`},
|
||||
{name: "missing tasks", payload: `{"has_more":false}`},
|
||||
{name: "missing task has_more", payload: `{"tasks":[]}`},
|
||||
{name: "task cursor missing", payload: `{"tasks":[],"has_more":true}`},
|
||||
{name: "unexpected task cursor", payload: `{"tasks":[],"has_more":false,"next_cursor":"next"}`},
|
||||
{name: "null contexts", payload: `{"contexts":null,"has_more":false}`, contexts: true},
|
||||
{name: "missing contexts", payload: `{"has_more":false}`, contexts: true},
|
||||
{name: "missing context has_more", payload: `{"contexts":[]}`, contexts: true},
|
||||
{name: "context cursor missing", payload: `{"contexts":[],"has_more":true}`, contexts: true},
|
||||
{name: "null context cursor", payload: `{"contexts":[],"has_more":false,"next_cursor":null}`, contexts: true},
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
rt := &fakeRuntime{
|
||||
params: map[string]string{"base_token": "b1"},
|
||||
responses: []json.RawMessage{dataResponse(t, test.payload)},
|
||||
}
|
||||
var err error
|
||||
if test.contexts {
|
||||
_, _, err = assistantSpec.ListContexts.Handler(context.Background(), rt, iagents.PageParams{})
|
||||
} else {
|
||||
_, _, err = assistantSpec.ListTasks.Handler(context.Background(), rt, "c1", iagents.PageParams{})
|
||||
}
|
||||
problem(t, err, errs.CategoryInternal, errs.SubtypeInvalidResponse)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnknownStateAndInvalidPayloadAreTyped(t *testing.T) {
|
||||
for _, payload := range []string{`{"schema_version":1,"task_id":"t1","status":"paused","outputs":[]}`, `{not-json`} {
|
||||
rt := &fakeRuntime{params: map[string]string{"base_token": "b1"}, responses: []json.RawMessage{dataResponse(t, payload)}}
|
||||
|
||||
@@ -3,7 +3,11 @@
|
||||
|
||||
package base
|
||||
|
||||
import "encoding/json"
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
type adapterSendRequest struct {
|
||||
ContextID string `json:"context_id,omitempty"`
|
||||
@@ -57,6 +61,21 @@ type adapterTask struct {
|
||||
Artifacts []adapterArtifact `json:"artifacts,omitempty"`
|
||||
}
|
||||
|
||||
type adapterTaskList struct {
|
||||
Tasks []adapterTask `json:"tasks"`
|
||||
HasMore bool `json:"has_more"`
|
||||
NextCursor string `json:"next_cursor,omitempty"`
|
||||
}
|
||||
|
||||
func (l *adapterTaskList) UnmarshalJSON(data []byte) error {
|
||||
tasks, hasMore, nextCursor, err := decodeAdapterList[adapterTask](data, "tasks")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
*l = adapterTaskList{Tasks: tasks, HasMore: hasMore, NextCursor: nextCursor}
|
||||
return nil
|
||||
}
|
||||
|
||||
type adapterOutput struct {
|
||||
ID string `json:"id"`
|
||||
Type string `json:"type"`
|
||||
@@ -163,6 +182,74 @@ type adapterContext struct {
|
||||
Tasks []adapterTask `json:"tasks,omitempty"`
|
||||
}
|
||||
|
||||
type adapterContextList struct {
|
||||
Contexts []adapterContext `json:"contexts"`
|
||||
HasMore bool `json:"has_more"`
|
||||
NextCursor string `json:"next_cursor,omitempty"`
|
||||
}
|
||||
|
||||
func (l *adapterContextList) UnmarshalJSON(data []byte) error {
|
||||
contexts, hasMore, nextCursor, err := decodeAdapterList[adapterContext](data, "contexts")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
*l = adapterContextList{Contexts: contexts, HasMore: hasMore, NextCursor: nextCursor}
|
||||
return nil
|
||||
}
|
||||
|
||||
func decodeAdapterList[T any](data []byte, itemsField string) ([]T, bool, string, error) {
|
||||
trimmed := bytes.TrimSpace(data)
|
||||
if len(trimmed) == 0 {
|
||||
return nil, false, "", fmt.Errorf("Base Agent list response is empty")
|
||||
}
|
||||
if trimmed[0] == '[' {
|
||||
var items []T
|
||||
if err := json.Unmarshal(trimmed, &items); err != nil {
|
||||
return nil, false, "", err
|
||||
}
|
||||
return items, false, "", nil
|
||||
}
|
||||
if trimmed[0] != '{' {
|
||||
return nil, false, "", fmt.Errorf("Base Agent list response must be an object")
|
||||
}
|
||||
|
||||
var envelope map[string]json.RawMessage
|
||||
if err := json.Unmarshal(trimmed, &envelope); err != nil {
|
||||
return nil, false, "", err
|
||||
}
|
||||
itemsRaw, ok := envelope[itemsField]
|
||||
if !ok || bytes.Equal(bytes.TrimSpace(itemsRaw), []byte("null")) {
|
||||
return nil, false, "", fmt.Errorf("Base Agent list response is missing %q", itemsField)
|
||||
}
|
||||
var items []T
|
||||
if err := json.Unmarshal(itemsRaw, &items); err != nil {
|
||||
return nil, false, "", fmt.Errorf("decode Base Agent list response %q: %w", itemsField, err)
|
||||
}
|
||||
|
||||
hasMoreRaw, ok := envelope["has_more"]
|
||||
if !ok || bytes.Equal(bytes.TrimSpace(hasMoreRaw), []byte("null")) {
|
||||
return nil, false, "", fmt.Errorf("Base Agent list response is missing %q", "has_more")
|
||||
}
|
||||
var hasMore bool
|
||||
if err := json.Unmarshal(hasMoreRaw, &hasMore); err != nil {
|
||||
return nil, false, "", fmt.Errorf("decode Base Agent list response %q: %w", "has_more", err)
|
||||
}
|
||||
|
||||
var nextCursor string
|
||||
if nextCursorRaw, ok := envelope["next_cursor"]; ok {
|
||||
if bytes.Equal(bytes.TrimSpace(nextCursorRaw), []byte("null")) {
|
||||
return nil, false, "", fmt.Errorf("Base Agent list response %q must be a string", "next_cursor")
|
||||
}
|
||||
if err := json.Unmarshal(nextCursorRaw, &nextCursor); err != nil {
|
||||
return nil, false, "", fmt.Errorf("decode Base Agent list response %q: %w", "next_cursor", err)
|
||||
}
|
||||
}
|
||||
if hasMore != (nextCursor != "") {
|
||||
return nil, false, "", fmt.Errorf("Base Agent list response has inconsistent pagination fields")
|
||||
}
|
||||
return items, hasMore, nextCursor, nil
|
||||
}
|
||||
|
||||
type adapterBusinessError struct {
|
||||
Category string `json:"category,omitempty"`
|
||||
Code string `json:"code,omitempty"`
|
||||
|
||||
Reference in New Issue
Block a user