diff --git a/cmd/event/list_test.go b/cmd/event/list_test.go index 65a2113cf..1779e0646 100644 --- a/cmd/event/list_test.go +++ b/cmd/event/list_test.go @@ -26,6 +26,7 @@ func TestRunList_TextOutput(t *testing.T) { "KEY", "AUTH", "PARAMS", "DESCRIPTION", "im.message.receive_v1", "im.message.message_read_v1", + "task.task.update_user_access_v2", } { if !strings.Contains(out, want) { t.Errorf("list output missing %q; full output:\n%s", want, out) @@ -55,4 +56,17 @@ func TestRunList_JSONOutput(t *testing.T) { } } } + + var foundTask bool + for _, row := range rows { + if row["key"] == "task.task.update_user_access_v2" { + foundTask = true + if row["single_consumer"] != true { + t.Errorf("task row single_consumer = %v, want true", row["single_consumer"]) + } + } + } + if !foundTask { + t.Fatal("event list JSON missing task.task.update_user_access_v2") + } } diff --git a/cmd/event/schema_test.go b/cmd/event/schema_test.go index e02f6758d..c586dc1a1 100644 --- a/cmd/event/schema_test.go +++ b/cmd/event/schema_test.go @@ -96,6 +96,34 @@ func TestRunSchema_JSONOutput(t *testing.T) { } } +func TestRunSchema_TaskUpdateUserAccessJSON(t *testing.T) { + f, stdout, _, _ := cmdutil.TestFactory(t, &core.CliConfig{AppID: "test"}) + + if err := runSchema(f, "task.task.update_user_access_v2", true); err != nil { + t.Fatalf("runSchema json: %v", err) + } + + var payload map[string]interface{} + if err := json.Unmarshal(stdout.Bytes(), &payload); err != nil { + t.Fatalf("output is not valid JSON: %v\n%s", err, stdout.String()) + } + if payload["jq_root_path"] != ".event" { + t.Errorf("jq_root_path = %v, want .event", payload["jq_root_path"]) + } + if payload["single_consumer"] != true { + t.Errorf("single_consumer = %v, want true", payload["single_consumer"]) + } + resolved := payload["resolved_output_schema"].(map[string]interface{}) + props := resolved["properties"].(map[string]interface{}) + eventProps := props["event"].(map[string]interface{})["properties"].(map[string]interface{}) + if got := eventProps["task_guid"].(map[string]interface{})["format"]; got != "task_guid" { + t.Errorf("task_guid format = %v, want task_guid", got) + } + if _, ok := eventProps["event_types"].(map[string]interface{})["items"].(map[string]interface{})["enum"]; !ok { + t.Fatalf("event_types enum missing in schema: %#v", eventProps["event_types"]) + } +} + func TestSchema_RendersSubscriptionKeyMarker(t *testing.T) { const syntheticKey = "test.evt_sub" t.Cleanup(func() { eventlib.UnregisterKeyForTest(syntheticKey) }) diff --git a/events/register.go b/events/register.go index a356483ea..ef00d077b 100644 --- a/events/register.go +++ b/events/register.go @@ -7,6 +7,7 @@ package events import ( "github.com/larksuite/cli/events/im" "github.com/larksuite/cli/events/minutes" + "github.com/larksuite/cli/events/task" "github.com/larksuite/cli/events/vc" "github.com/larksuite/cli/events/whiteboard" "github.com/larksuite/cli/internal/event" @@ -17,6 +18,7 @@ func init() { all := [][]event.KeyDefinition{ im.Keys(), minutes.Keys(), + task.Keys(), vc.Keys(), whiteboard.Keys(), } diff --git a/events/task/native.go b/events/task/native.go new file mode 100644 index 000000000..7b44d669b --- /dev/null +++ b/events/task/native.go @@ -0,0 +1,23 @@ +// Copyright (c) 2026 Lark Technologies Pte. Ltd. +// SPDX-License-Identifier: MIT + +package task + +// TaskUpdateUserAccessV2Data is the Task v2 update event payload under the +// standard Lark V2 event envelope. +type TaskUpdateUserAccessV2Data struct { + EventTypes []string `json:"event_types,omitempty" desc:"Task commit types included in this event" enum:"task_create,task_deleted,task_summary_update,task_desc_update,task_assignees_update,task_followers_update,task_reminders_update,task_start_due_update,task_completed_update"` + TaskGUID string `json:"task_guid,omitempty" desc:"Task GUID that changed" kind:"task_guid"` +} + +var taskUpdateUserAccessCommitTypes = []string{ + "task_create", + "task_deleted", + "task_summary_update", + "task_desc_update", + "task_assignees_update", + "task_followers_update", + "task_reminders_update", + "task_start_due_update", + "task_completed_update", +} diff --git a/events/task/preconsume.go b/events/task/preconsume.go new file mode 100644 index 000000000..347f0d013 --- /dev/null +++ b/events/task/preconsume.go @@ -0,0 +1,32 @@ +// Copyright (c) 2026 Lark Technologies Pte. Ltd. +// SPDX-License-Identifier: MIT + +package task + +import ( + "context" + + "github.com/larksuite/cli/errs" + "github.com/larksuite/cli/internal/event" +) + +const taskSubscriptionPath = "/open-apis/task/v2/task_v2/task_subscription?user_id_type=open_id" + +func taskSubscriptionPreConsume(ctx context.Context, rt event.APIClient, _ map[string]string) (func() error, error) { + if rt == nil { + return nil, errs.NewInternalError(errs.SubtypeUnknown, + "runtime API client is required for pre-consume subscription") + } + + if _, err := rt.CallAPI(ctx, "POST", taskSubscriptionPath, nil); err != nil { + if _, ok := errs.ProblemOf(err); ok { + return nil, err + } + return nil, errs.NewNetworkError( + errs.SubtypeNetworkTransport, + "failed to subscribe task event", + ).WithCause(err) + } + + return nil, nil +} diff --git a/events/task/preconsume_test.go b/events/task/preconsume_test.go new file mode 100644 index 000000000..acb09d9d7 --- /dev/null +++ b/events/task/preconsume_test.go @@ -0,0 +1,119 @@ +// Copyright (c) 2026 Lark Technologies Pte. Ltd. +// SPDX-License-Identifier: MIT + +package task + +import ( + "context" + "encoding/json" + "errors" + "testing" + + "github.com/larksuite/cli/errs" +) + +type stubAPIClient struct { + err error + + method string + path string + body interface{} + calls int +} + +func (s *stubAPIClient) CallAPI(_ context.Context, method, path string, body interface{}) (json.RawMessage, error) { + s.method = method + s.path = path + s.body = body + s.calls++ + if s.err != nil { + return nil, s.err + } + return json.RawMessage(`{"code":0,"msg":"success","data":{}}`), nil +} + +func TestTaskSubscriptionPreConsumeCallsSubscribeAPI(t *testing.T) { + rt := &stubAPIClient{} + cleanup, err := taskSubscriptionPreConsume(context.Background(), rt, nil) + if err != nil { + t.Fatalf("taskSubscriptionPreConsume error = %v", err) + } + if cleanup != nil { + t.Fatal("cleanup = non-nil, want nil because task subscription has no unsubscribe API") + } + if rt.calls != 1 { + t.Fatalf("calls = %d, want 1", rt.calls) + } + if rt.method != "POST" { + t.Errorf("method = %q, want POST", rt.method) + } + if rt.path != taskSubscriptionPath { + t.Errorf("path = %q, want %q", rt.path, taskSubscriptionPath) + } + if rt.body != nil { + t.Errorf("body = %#v, want nil", rt.body) + } +} + +func TestTaskSubscriptionPreConsumeRequiresRuntime(t *testing.T) { + _, err := taskSubscriptionPreConsume(context.Background(), nil, nil) + if err == nil { + t.Fatal("expected error") + } + p, ok := errs.ProblemOf(err) + if !ok { + t.Fatalf("expected typed error, got %T: %v", err, err) + } + if p.Category != errs.CategoryInternal { + t.Errorf("category = %s, want %s", p.Category, errs.CategoryInternal) + } + if p.Subtype != errs.SubtypeUnknown { + t.Errorf("subtype = %s, want %s", p.Subtype, errs.SubtypeUnknown) + } +} + +func TestTaskSubscriptionPreConsumePassesThroughAPIError(t *testing.T) { + wantErr := errs.NewValidationError(errs.SubtypeFailedPrecondition, "subscription already exists") + rt := &stubAPIClient{err: wantErr} + + _, err := taskSubscriptionPreConsume(context.Background(), rt, nil) + if err != wantErr { + t.Fatalf("err identity changed: got %T %v, want original %T %v", err, err, wantErr, wantErr) + } + if !errors.Is(err, wantErr) { + t.Fatalf("err = %v, want %v", err, wantErr) + } + p, ok := errs.ProblemOf(err) + if !ok { + t.Fatalf("expected typed error, got %T: %v", err, err) + } + if p.Category != errs.CategoryValidation { + t.Errorf("category = %s, want %s", p.Category, errs.CategoryValidation) + } + if p.Subtype != errs.SubtypeFailedPrecondition { + t.Errorf("subtype = %s, want %s", p.Subtype, errs.SubtypeFailedPrecondition) + } +} + +func TestTaskSubscriptionPreConsumeWrapsUntypedAPIError(t *testing.T) { + cause := errors.New("connection reset") + rt := &stubAPIClient{err: cause} + + _, err := taskSubscriptionPreConsume(context.Background(), rt, nil) + if err == nil { + t.Fatal("expected error") + } + if !errors.Is(err, cause) { + t.Fatalf("err = %v, want cause %v", err, cause) + } + p, ok := errs.ProblemOf(err) + if !ok { + t.Fatalf("expected typed error, got %T: %v", err, err) + } + if p.Category != errs.CategoryNetwork { + t.Errorf("category = %s, want %s", p.Category, errs.CategoryNetwork) + } + if p.Subtype != errs.SubtypeNetworkTransport { + t.Errorf("subtype = %s, want %s", p.Subtype, errs.SubtypeNetworkTransport) + } +} diff --git a/events/task/register.go b/events/task/register.go new file mode 100644 index 000000000..91373e6c0 --- /dev/null +++ b/events/task/register.go @@ -0,0 +1,33 @@ +// Copyright (c) 2026 Lark Technologies Pte. Ltd. +// SPDX-License-Identifier: MIT + +// Package task registers Task-domain EventKeys. +package task + +import ( + "reflect" + + "github.com/larksuite/cli/internal/event" +) + +const eventTypeTaskUpdateUserAccessV2 = "task.task.update_user_access_v2" + +// Keys returns all Task-domain EventKey definitions. +func Keys() []event.KeyDefinition { + return []event.KeyDefinition{ + { + Key: eventTypeTaskUpdateUserAccessV2, + DisplayName: "Task updated", + Description: "Triggered when tasks visible to the current user or app are created, deleted, or updated", + EventType: eventTypeTaskUpdateUserAccessV2, + Schema: event.SchemaDef{ + Native: &event.SchemaSpec{Type: reflect.TypeOf(TaskUpdateUserAccessV2Data{})}, + }, + PreConsume: taskSubscriptionPreConsume, + Scopes: []string{"task:task:read"}, + AuthTypes: []string{"user", "bot"}, + RequiredConsoleEvents: []string{eventTypeTaskUpdateUserAccessV2}, + SingleConsumer: true, + }, + } +} diff --git a/events/task/register_test.go b/events/task/register_test.go new file mode 100644 index 000000000..06897c112 --- /dev/null +++ b/events/task/register_test.go @@ -0,0 +1,95 @@ +// Copyright (c) 2026 Lark Technologies Pte. Ltd. +// SPDX-License-Identifier: MIT + +package task + +import ( + "encoding/json" + "reflect" + "testing" + + "github.com/larksuite/cli/internal/event" + "github.com/larksuite/cli/internal/event/schemas" +) + +func TestKeysTaskUpdateUserAccessMetadata(t *testing.T) { + keys := Keys() + if len(keys) != 1 { + t.Fatalf("len(Keys()) = %d, want 1", len(keys)) + } + + def := keys[0] + if def.Key != eventTypeTaskUpdateUserAccessV2 { + t.Errorf("Key = %q, want %q", def.Key, eventTypeTaskUpdateUserAccessV2) + } + if def.EventType != eventTypeTaskUpdateUserAccessV2 { + t.Errorf("EventType = %q, want %q", def.EventType, eventTypeTaskUpdateUserAccessV2) + } + if def.Schema.Native == nil { + t.Fatal("Schema.Native is nil") + } + if def.Schema.Native.Type != reflect.TypeOf(TaskUpdateUserAccessV2Data{}) { + t.Errorf("native type = %v, want TaskUpdateUserAccessV2Data", def.Schema.Native.Type) + } + if def.Process != nil { + t.Fatal("Native Task EventKey must not set Process") + } + if def.PreConsume == nil { + t.Fatal("PreConsume is nil") + } + if !def.SingleConsumer { + t.Fatal("SingleConsumer = false, want true") + } + if !reflect.DeepEqual(def.Scopes, []string{"task:task:read"}) { + t.Errorf("Scopes = %#v", def.Scopes) + } + if !reflect.DeepEqual(def.AuthTypes, []string{"user", "bot"}) { + t.Errorf("AuthTypes = %#v", def.AuthTypes) + } + if !reflect.DeepEqual(def.RequiredConsoleEvents, []string{eventTypeTaskUpdateUserAccessV2}) { + t.Errorf("RequiredConsoleEvents = %#v", def.RequiredConsoleEvents) + } +} + +func TestTaskUpdateUserAccessSchemaAnnotations(t *testing.T) { + raw := schemas.WrapV2Envelope(schemas.FromType(reflect.TypeOf(TaskUpdateUserAccessV2Data{}))) + var schema map[string]interface{} + if err := json.Unmarshal(raw, &schema); err != nil { + t.Fatalf("unmarshal schema: %v", err) + } + + eventProps := schema["properties"].(map[string]interface{})["event"].(map[string]interface{})["properties"].(map[string]interface{}) + taskGUID := eventProps["task_guid"].(map[string]interface{}) + if got := taskGUID["format"]; got != "task_guid" { + t.Errorf("task_guid format = %v, want task_guid", got) + } + + eventTypes := eventProps["event_types"].(map[string]interface{}) + items := eventTypes["items"].(map[string]interface{}) + rawEnum, ok := items["enum"].([]interface{}) + if !ok { + t.Fatalf("event_types item enum missing: %#v", items["enum"]) + } + got := make(map[string]bool, len(rawEnum)) + for _, v := range rawEnum { + got[v.(string)] = true + } + for _, want := range taskUpdateUserAccessCommitTypes { + if !got[want] { + t.Errorf("event_types enum missing %q; enum=%v", want, rawEnum) + } + } +} + +func TestTaskUpdateUserAccessRegistersCleanly(t *testing.T) { + const key = eventTypeTaskUpdateUserAccessV2 + event.UnregisterKeyForTest(key) + t.Cleanup(func() { event.UnregisterKeyForTest(key) }) + + for _, def := range Keys() { + event.RegisterKey(def) + } + if _, ok := event.Lookup(key); !ok { + t.Fatalf("event.Lookup(%q) not registered", key) + } +} diff --git a/shortcuts/task/shortcuts.go b/shortcuts/task/shortcuts.go index c6d694ec6..7de989ca5 100644 --- a/shortcuts/task/shortcuts.go +++ b/shortcuts/task/shortcuts.go @@ -242,7 +242,6 @@ func Shortcuts() []common.Shortcut { GetMyTasks, GetRelatedTasks, SearchTask, - SubscribeTaskEvent, UploadAttachmentTask, CreateTasklist, SearchTasklist, diff --git a/shortcuts/task/task_subscribe_event.go b/shortcuts/task/task_subscribe_event.go deleted file mode 100644 index 44e0bd873..000000000 --- a/shortcuts/task/task_subscribe_event.go +++ /dev/null @@ -1,40 +0,0 @@ -// Copyright (c) 2026 Lark Technologies Pte. Ltd. -// SPDX-License-Identifier: MIT - -package task - -import ( - "context" - "fmt" - "io" - "net/http" - - "github.com/larksuite/cli/shortcuts/common" -) - -var SubscribeTaskEvent = common.Shortcut{ - Service: "task", - Command: "+subscribe-event", - Description: "subscribe to task events", - Risk: "write", - Scopes: []string{"task:task:read"}, - AuthTypes: []string{"user", "bot"}, - HasFormat: true, - DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI { - return common.NewDryRunAPI(). - POST("/open-apis/task/v2/task_v2/task_subscription"). - Params(map[string]interface{}{"user_id_type": "open_id"}) - }, - Execute: func(ctx context.Context, runtime *common.RuntimeContext) error { - params := map[string]interface{}{"user_id_type": "open_id"} - if _, err := callTaskAPITyped(runtime, http.MethodPost, "/open-apis/task/v2/task_v2/task_subscription", params, nil); err != nil { - return err - } - - outData := map[string]interface{}{"ok": true} - runtime.OutFormat(outData, nil, func(w io.Writer) { - fmt.Fprintln(w, "✅ Task event subscription created successfully!") - }) - return nil - }, -} diff --git a/shortcuts/task/task_subscribe_event_test.go b/shortcuts/task/task_subscribe_event_test.go deleted file mode 100644 index 42b3b7a4b..000000000 --- a/shortcuts/task/task_subscribe_event_test.go +++ /dev/null @@ -1,163 +0,0 @@ -// Copyright (c) 2026 Lark Technologies Pte. Ltd. -// SPDX-License-Identifier: MIT - -package task - -import ( - "errors" - "strings" - "testing" - - "github.com/spf13/cobra" - - "github.com/larksuite/cli/errs" - "github.com/larksuite/cli/internal/httpmock" - "github.com/larksuite/cli/internal/output" - "github.com/larksuite/cli/shortcuts/common" -) - -func TestSubscribeTaskEvent(t *testing.T) { - tests := []struct { - name string - mode string - args []string - register func(*httpmock.Registry) - wantErr bool - wantParts []string - }{ - { - name: "execute json (user identity)", - mode: "execute", - args: []string{"+subscribe-event", "--as", "user", "--format", "json"}, - register: func(reg *httpmock.Registry) { - reg.Register(&httpmock.Stub{ - Method: "POST", - URL: "/open-apis/task/v2/task_v2/task_subscription", - Body: map[string]interface{}{ - "code": 0, - "msg": "success", - "data": map[string]interface{}{}, - }, - }) - }, - wantParts: []string{`"ok": true`}, - }, - { - name: "execute json (bot identity)", - mode: "execute", - args: []string{"+subscribe-event", "--as", "bot", "--format", "json"}, - register: func(reg *httpmock.Registry) { - reg.Register(&httpmock.Stub{ - Method: "POST", - URL: "/open-apis/task/v2/task_v2/task_subscription", - Body: map[string]interface{}{ - "code": 0, - "msg": "success", - "data": map[string]interface{}{}, - }, - }) - }, - wantParts: []string{`"ok": true`}, - }, - { - name: "execute api error", - mode: "execute", - args: []string{"+subscribe-event", "--as", "bot", "--format", "json"}, - register: func(reg *httpmock.Registry) { - reg.Register(&httpmock.Stub{ - Method: "POST", - URL: "/open-apis/task/v2/task_v2/task_subscription", - Body: map[string]interface{}{ - "code": 401, - "msg": "Unauthorized", - "error": map[string]interface{}{ - "log_id": "test-log-id", - }, - }, - }) - }, - wantErr: true, - wantParts: []string{"Unauthorized"}, - }, - { - name: "dry run", - mode: "dryrun", - wantParts: []string{"POST /open-apis/task/v2/task_v2/task_subscription"}, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - switch tt.mode { - case "execute": - f, stdout, _, reg := taskShortcutTestFactory(t) - warmTenantToken(t, f, reg) - if tt.register != nil { - tt.register(reg) - } - - err := runMountedTaskShortcut(t, SubscribeTaskEvent, tt.args, f, stdout) - if tt.wantErr { - if err == nil { - t.Fatal("expected error, got nil") - } - out := err.Error() - for _, want := range tt.wantParts { - if !strings.Contains(out, want) { - t.Fatalf("error missing %q: %s", want, out) - } - } - return - } - if err != nil { - t.Fatalf("runMountedTaskShortcut() error = %v", err) - } - - out := stdout.String() - outNorm := strings.ReplaceAll(out, `":"`, `": "`) - for _, want := range tt.wantParts { - if !strings.Contains(out, want) && !strings.Contains(outNorm, want) { - t.Fatalf("output missing %q: %s", want, out) - } - } - case "dryrun": - runtime := common.TestNewRuntimeContextWithIdentity(&cobra.Command{Use: "test"}, taskTestConfig(t), "user") - out := SubscribeTaskEvent.DryRun(nil, runtime).Format() - for _, want := range tt.wantParts { - if !strings.Contains(out, want) { - t.Fatalf("dry run output missing %q: %s", want, out) - } - } - } - }) - } -} - -// TestSubscribeTaskEvent_MalformedResponse covers the parse-response arm: a 200 -// with an unparseable body surfaces a typed internal invalid_response error -// (exit 5). -func TestSubscribeTaskEvent_MalformedResponse(t *testing.T) { - f, stdout, _, reg := taskShortcutTestFactory(t) - warmTenantToken(t, f, reg) - - reg.Register(&httpmock.Stub{ - Method: "POST", - URL: "/open-apis/task/v2/task_v2/task_subscription", - Status: 200, - RawBody: []byte("{not-json"), - }) - - args := []string{"+subscribe-event", "--as", "bot", "--format", "json"} - err := runMountedTaskShortcut(t, SubscribeTaskEvent, args, f, stdout) - - var ie *errs.InternalError - if !errors.As(err, &ie) { - t.Fatalf("err = %T, want *errs.InternalError; err = %v", err, err) - } - if ie.Subtype != errs.SubtypeInvalidResponse { - t.Errorf("subtype = %q, want %q", ie.Subtype, errs.SubtypeInvalidResponse) - } - if got := output.ExitCodeOf(err); got != output.ExitInternal { - t.Errorf("exit code = %d, want %d", got, output.ExitInternal) - } -} diff --git a/skills/lark-event/SKILL.md b/skills/lark-event/SKILL.md index 62a5dfbfd..434d5478e 100644 --- a/skills/lark-event/SKILL.md +++ b/skills/lark-event/SKILL.md @@ -1,7 +1,7 @@ --- name: lark-event version: 1.0.0 -description: "Lark/Feishu real-time event listening / subscribing / consuming: stream events as NDJSON via `lark-cli event consume ` (covers IM messages/reactions/chat changes, VC meeting ended, Minutes generated, Whiteboard updated, etc.). Use for Lark bots, real-time message processing, long-running subscribers, streaming webhook/push handlers. Supports `--max-events` / `--timeout` bounded runs and a stderr ready-marker contract — designed for AI agents running as subprocesses." +description: "Lark/Feishu real-time event listening / subscribing / consuming: stream events as NDJSON via `lark-cli event consume ` (covers IM messages/reactions/chat changes, Task updates, VC meeting ended, Minutes generated, Whiteboard updated, etc.). Use for Lark bots, real-time message processing, long-running subscribers, streaming webhook/push handlers. Supports `--max-events` / `--timeout` bounded runs and a stderr ready-marker contract — designed for AI agents running as subprocesses." metadata: requires: bins: ["lark-cli"] @@ -148,6 +148,7 @@ Lark-defined semantic tags (**not** JSON Schema's standard `format`). Common val | Topic | Reference | Coverage | |------------|------------------------------------------------------------------------------|---| | IM | [`references/lark-event-im.md`](references/lark-event-im.md) | Catalog of 11 IM EventKeys + shape notes (flat vs V2 envelope) + `im.message.receive_v1` field gotchas (`sender_id` is open_id only; `.content` is plain text except for `interactive` cards) + common jq recipes (filter by chat_type / message_type / sender) | +| Task | [`references/lark-event-task.md`](references/lark-event-task.md) | Catalog of 1 Task EventKey (`task.task.update_user_access_v2`) + Native V2 envelope shape + task commit types + user/bot subscription notes | | VC | [`references/lark-event-vc.md`](references/lark-event-vc.md) | Catalog of 2 VC EventKeys (`vc.meeting.participant_meeting_ended_v1`, `vc.note.generated_v1`) + field reference + source type semantics (meeting only) | | Minutes | [`references/lark-event-minutes.md`](references/lark-event-minutes.md) | Catalog of 1 Minutes EventKey (`minutes.minute.generated_v1`) + field reference + source type semantics (meeting only) | | Whiteboard | [`references/lark-event-whiteboard.md`](references/lark-event-whiteboard.md) | Catalog of 1 Board EventKey (`board.whiteboard.updated_v1`) + per-whiteboard subscription model (requires `-p whiteboard_id=`) + payload field reference (whiteboard_id / operator_ids triple-id) | diff --git a/skills/lark-event/references/lark-event-task.md b/skills/lark-event/references/lark-event-task.md new file mode 100644 index 000000000..57fd6bb76 --- /dev/null +++ b/skills/lark-event/references/lark-event-task.md @@ -0,0 +1,78 @@ +# Task Events + +> **Prerequisite:** Read [`../SKILL.md`](../SKILL.md) first for the `event consume` essentials (commands, subprocess contract, jq usage). + +## Key catalog (1) + +| EventKey | Purpose | +|---|---| +| `task.task.update_user_access_v2` | A visible task has been created, deleted, or updated | + +This key uses a **Native schema** (V2 envelope; output rooted at `.event`) and carries a **PreConsume hook** that calls the Task event subscription API before listening. + +## Scopes & auth + +| EventKey | Scope | Auth | +|---|---|---| +| `task.task.update_user_access_v2` | `task:task:read` | user, bot | + +Supports `--as user` or `--as bot`. + +- `--as user`: receive task updates visible to the current user through authorship, assignment, following, or other access. +- `--as bot`: receive task updates for tasks the application is responsible for. + +## `task.task.update_user_access_v2` + +### Subscription behavior + +On startup, `event consume` calls: + +```text +POST /open-apis/task/v2/task_v2/task_subscription?user_id_type=open_id +``` + +The Task subscription API has no matching unsubscribe endpoint in the current CLI metadata, so graceful exit has no cleanup call for this EventKey. Re-running the consumer repeats the subscribe call for the selected identity. + +This EventKey is single-consumer per local bus subscription: start one `event consume task.task.update_user_access_v2` process for a given app/profile/identity at a time. + +### Output fields (V2 envelope; root path `.event`) + +| Field | Type | Description | +|---|---|---| +| `.header.event_id` | string | Globally unique event ID; safe for deduplication | +| `.header.create_time` | string (timestamp_ms) | Event creation time in milliseconds | +| `.event.event_types[]` | string enum | Task commit types included in this event | +| `.event.task_guid` | string (kind=task_guid) | Task GUID that changed | + +Commit types: + +```text +task_assignees_update +task_completed_update +task_create +task_deleted +task_desc_update +task_followers_update +task_reminders_update +task_start_due_update +task_summary_update +``` + +### Example + +```bash +# Stream task update events for the current user +lark-cli event consume task.task.update_user_access_v2 --as user + +# Sample one event for payload inspection +lark-cli event consume task.task.update_user_access_v2 \ + --as user --max-events 1 --timeout 2m + +# Project to a compact task-update record +lark-cli event consume task.task.update_user_access_v2 \ + --as user \ + --jq '{event_id: .header.event_id, task_guid: .event.task_guid, event_types: .event.event_types, timestamp: .header.create_time}' + +# Consume as the app identity +lark-cli event consume task.task.update_user_access_v2 --as bot +``` diff --git a/skills/lark-task/SKILL.md b/skills/lark-task/SKILL.md index bde2a9362..d2f8b180c 100644 --- a/skills/lark-task/SKILL.md +++ b/skills/lark-task/SKILL.md @@ -51,7 +51,6 @@ metadata: | [`+get-my-tasks`](references/lark-task-get-my-tasks.md) | List tasks assigned to me | | [`+get-related-tasks`](references/lark-task-get-related-tasks.md) | list tasks related to me | | [`+search`](references/lark-task-search.md) | search tasks | -| [`+subscribe-event`](references/lark-task-subscribe-event.md) | subscribe to task events | | [`+upload-attachment`](references/lark-task-upload-attachment.md) | upload a local file as an attachment to a task | | [`+tasklist-create`](references/lark-task-tasklist-create.md) | create a tasklist and optionally add tasks | | [`+tasklist-search`](references/lark-task-tasklist-search.md) | search tasklists | diff --git a/skills/lark-task/references/lark-task-subscribe-event.md b/skills/lark-task/references/lark-task-subscribe-event.md deleted file mode 100644 index e23a4d80d..000000000 --- a/skills/lark-task/references/lark-task-subscribe-event.md +++ /dev/null @@ -1,86 +0,0 @@ -# task +subscribe-event - -> **Prerequisites:** Please read `../lark-shared/SKILL.md` to understand authentication, global parameters, and security rules. -> -> **⚠️ Note:** This API supports both `user` and `bot` identities. Use `user` to subscribe the current user's accessible tasks; use `bot` to subscribe tasks the **application is responsible for**. - -Subscribe task update events with the current identity. - -This shortcut is different from `event +subscribe`: -- `task +subscribe-event` registers task-event access for the **current identity** -- with `--as user`, it subscribes the **current user** to task events for tasks they created, are responsible for, or follow -- with `--as bot`, it subscribes using the **application identity** for tasks the application is responsible for - -The task event type is: - -```text -task.task.update_user_access_v2 -``` - -Within this event, task changes are represented by commit types (string values). Deduped list: - -```text -task_assignees_update -task_completed_update -task_create -task_deleted -task_desc_update -task_followers_update -task_reminders_update -task_start_due_update -task_summary_update -``` - -Event payload shape (example): - -```json -{ - "event_id": "evt_xxx", - "event_types": ["task_summary_update"], - "task_guid": "task_guid_xxx", - "timestamp": "1775793266152", - "type": "task.task.update_user_access_v2" -} -``` - -- `type`: event type, should be `task.task.update_user_access_v2` -- `event_id`: unique event id (useful for dedup) -- `event_types`: list of commit types (see the deduped list above) -- `task_guid`: the task GUID that changed -- `timestamp`: event timestamp (ms) - -In practice, this means: -- with `--as user`, the subscribed user can receive updates for tasks visible to them through authorship, assignment, or following -- with `--as bot`, the subscription covers tasks the application is responsible for - -To actually receive the subscribed events, use the standard event WebSocket receiver: - -```bash -lark-cli event +subscribe --event-types task.task.update_user_access_v2 --compact --quiet -``` - -The full flow is: -1. Register the subscription with `lark-cli task +subscribe-event [--as user|bot]` -2. Receive those events with `lark-cli event +subscribe --event-types task.task.update_user_access_v2 ...` - -## Recommended Commands - -```bash -lark-cli task +subscribe-event -``` -# Subscribe with app identity -lark-cli task +subscribe-event --as bot - - -## Parameters - -This shortcut has no additional parameters. - -## Workflow - -1. Confirm whether the user wants to subscribe with `user` identity or `bot` identity. -2. Execute `lark-cli task +subscribe-event` -3. Report whether the subscription succeeded, and clarify which identity the subscription applies to. - -> [!CAUTION] -> This is a **Write Operation** -- You must confirm the user's intent before executing.