Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,14 +30,15 @@ The official [Lark/Feishu](https://www.larksuite.com/) CLI tool, maintained by t
| 📁 Drive | Upload and download files, search docs & wiki, manage comments |
| 📊 Base | Create and manage tables, fields, records, views, dashboards, workflows, forms, roles & permissions, data aggregation & analytics |
| 📈 Sheets | Create, read, write, append, find, and export spreadsheet data |
| 🖼️ Slides | Create and manage presentations, read presentation content, and add or remove slides |
| 🖼️ Slides | Create and manage presentations, read presentation content, and add or remove slides |
| ✅ Tasks | Create, query, update, and complete tasks; manage task lists, subtasks, comments & reminders |
| 📚 Wiki | Create and manage knowledge spaces, nodes, and documents |
| 👤 Contact | Search users by name/email/phone, get user profiles |
| 📧 Mail | Browse, search, read emails, send, reply, forward, manage drafts, watch new mail |
| 🎥 Meetings | Search meeting records, query meeting minutes & recordings |
| 🕐 Attendance | Query personal attendance check-in records |
| ✍️ Approval | Query approval tasks, approve/reject/transfer tasks, cancel and CC instances |
| 🎯 OKR | Query, create, update OKRs; manage objective & key results, alignments and indicators. |

## Installation & Quick Start

Expand Down
1 change: 1 addition & 0 deletions README.zh.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
| 🎥 视频会议 | 搜索会议记录、查询会议纪要与录制 |
| 🕐 考勤打卡 | 查询个人考勤打卡记录 |
| ✍️ 审批 | 查询审批任务、同意/拒绝/转交审批任务、撤回与抄送审批实例 |
| 🎯 OKR | 查询、创建、更新 OKR,管理目标、关键结果、对齐和指标 |

## 安装与快速开始

Expand Down
4 changes: 4 additions & 0 deletions internal/registry/service_descriptions.json
Original file line number Diff line number Diff line change
Expand Up @@ -63,5 +63,9 @@
"wiki": {
"en": { "title": "Wiki", "description": "Wiki space and node management" },
"zh": { "title": "知识库", "description": "知识空间、节点管理" }
},
"okr": {
"en": { "title": "OKR", "description": "Lark OKR objectives, key results, alignments, indicators" },
"zh": { "title": "OKR", "description": "飞书 OKR 目标、关键结果、对齐、量化指标" }
}
}
101 changes: 101 additions & 0 deletions shortcuts/okr/okr_cli_resp.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
// SPDX-License-Identifier: MIT

package okr

// RespAlignment 对齐关系
type RespAlignment struct {
ID string `json:"id"`
CreateTime string `json:"create_time"`
UpdateTime string `json:"update_time"`
FromOwner RespOwner `json:"from_owner"`
ToOwner RespOwner `json:"to_owner"`
FromEntityType string `json:"from_entity_type"`
FromEntityID string `json:"from_entity_id"`
ToEntityType string `json:"to_entity_type"`
ToEntityID string `json:"to_entity_id"`
}

// RespCategory 分类
type RespCategory struct {
ID string `json:"id"`
CreateTime string `json:"create_time"`
UpdateTime string `json:"update_time"`
CategoryType string `json:"category_type"`
Enabled *bool `json:"enabled,omitempty"`
Color *string `json:"color,omitempty"`
Name CategoryName `json:"name"`
}

// RespCycle 周期
type RespCycle struct {
ID string `json:"id"`
CreateTime string `json:"create_time"`
UpdateTime string `json:"update_time"`
TenantCycleID string `json:"tenant_cycle_id"`
Owner RespOwner `json:"owner"`
StartTime string `json:"start_time"`
EndTime string `json:"end_time"`
CycleStatus *string `json:"cycle_status,omitempty"`
Score *float64 `json:"score,omitempty"`
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

// RespIndicator 指标
type RespIndicator struct {
ID string `json:"id"`
CreateTime string `json:"create_time"`
UpdateTime string `json:"update_time"`
Owner RespOwner `json:"owner"`
EntityType *string `json:"entity_type,omitempty"`
EntityID *string `json:"entity_id,omitempty"`
IndicatorStatus *string `json:"indicator_status,omitempty"`
StatusCalculateType *string `json:"status_calculate_type,omitempty"`
StartValue *float64 `json:"start_value,omitempty"`
TargetValue *float64 `json:"target_value,omitempty"`
CurrentValue *float64 `json:"current_value,omitempty"`
CurrentValueCalculateType *string `json:"current_value_calculate_type,omitempty"`
Unit *RespIndicatorUnit `json:"unit,omitempty"`
}

// RespIndicatorUnit 指标单位
type RespIndicatorUnit struct {
UnitType *string `json:"unit_type,omitempty"`
UnitValue *string `json:"unit_value,omitempty"`
}

// RespKeyResult 关键结果
type RespKeyResult struct {
ID string `json:"id"`
CreateTime string `json:"create_time"`
UpdateTime string `json:"update_time"`
Owner RespOwner `json:"owner"`
ObjectiveID string `json:"objective_id"`
Position *int32 `json:"position,omitempty"`
Content *string `json:"content,omitempty"`
Score *float64 `json:"score,omitempty"`
Weight *float64 `json:"weight,omitempty"`
Deadline *string `json:"deadline,omitempty"`
}

// RespObjective 目标
type RespObjective struct {
ID string `json:"id"`
CreateTime string `json:"create_time"`
UpdateTime string `json:"update_time"`
Owner RespOwner `json:"owner"`
CycleID string `json:"cycle_id"`
Position *int32 `json:"position,omitempty"`
Content *string `json:"content,omitempty"`
Score *float64 `json:"score,omitempty"`
Notes *string `json:"notes,omitempty"`
Weight *float64 `json:"weight,omitempty"`
Deadline *string `json:"deadline,omitempty"`
CategoryID *string `json:"category_id,omitempty"`
KeyResults []RespKeyResult `json:"key_results,omitempty"`
}

// RespOwner OKR 所有者
type RespOwner struct {
OwnerType string `json:"owner_type"`
UserID *string `json:"user_id,omitempty"`
}
182 changes: 182 additions & 0 deletions shortcuts/okr/okr_cycle_detail.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
// SPDX-License-Identifier: MIT

package okr

import (
"context"
"encoding/json"
"fmt"
"io"
"strconv"
"time"

"github.com/larksuite/cli/shortcuts/common"
larkcore "github.com/larksuite/oapi-sdk-go/v3/core"
)

// OKRCycleDetail lists all objectives and their key results under a given OKR cycle.
var OKRCycleDetail = common.Shortcut{
Service: "okr",
Command: "+cycle-detail",
Description: "List objectives and key results under an OKR cycle",
Risk: "read",
Scopes: []string{"okr:okr.content:readonly"},
AuthTypes: []string{"user", "bot"},
HasFormat: true,
Flags: []common.Flag{
{Name: "cycle-id", Desc: "OKR cycle id (int64)", Required: true},
},
Validate: func(ctx context.Context, runtime *common.RuntimeContext) error {
cycleID := runtime.Str("cycle-id")
if cycleID == "" {
return common.FlagErrorf("--cycle-id is required")

Check warning on line 33 in shortcuts/okr/okr_cycle_detail.go

View check run for this annotation

Codecov / codecov/patch

shortcuts/okr/okr_cycle_detail.go#L33

Added line #L33 was not covered by tests
}
if id, err := strconv.ParseInt(cycleID, 10, 64); err != nil || id <= 0 {
return common.FlagErrorf("--cycle-id must be a positive int64")
}
return nil
},
DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI {
cycleID := runtime.Str("cycle-id")
params := map[string]interface{}{
"page_size": 100,
}
return common.NewDryRunAPI().
GET("/open-apis/okr/v2/cycles/:cycle_id/objectives").
Params(params).
Set("cycle_id", cycleID).
Desc("Auto-paginates objectives in the cycle, then calls GET /open-apis/okr/v2/objectives/:objective_id/key_results for each objective to fetch key results")
},
Execute: func(ctx context.Context, runtime *common.RuntimeContext) error {
cycleID := runtime.Str("cycle-id")

// Paginate objectives under the cycle.
queryParams := make(larkcore.QueryParams)
queryParams.Set("page_size", "100")

var objectives []Objective
page := 0
for {
if err := ctx.Err(); err != nil {
return err

Check warning on line 62 in shortcuts/okr/okr_cycle_detail.go

View check run for this annotation

Codecov / codecov/patch

shortcuts/okr/okr_cycle_detail.go#L62

Added line #L62 was not covered by tests
}
if page > 0 {
select {
case <-ctx.Done():
return ctx.Err()

Check warning on line 67 in shortcuts/okr/okr_cycle_detail.go

View check run for this annotation

Codecov / codecov/patch

shortcuts/okr/okr_cycle_detail.go#L66-L67

Added lines #L66 - L67 were not covered by tests
case <-time.After(500 * time.Millisecond):
}
}
page++
Comment thread
coderabbitai[bot] marked this conversation as resolved.

path := fmt.Sprintf("/open-apis/okr/v2/cycles/%s/objectives", cycleID)
data, err := runtime.DoAPIJSON("GET", path, queryParams, nil)
if err != nil {
return err
}

itemsRaw, _ := data["items"].([]interface{})
for _, item := range itemsRaw {
raw, err := json.Marshal(item)
if err != nil {
continue

Check warning on line 83 in shortcuts/okr/okr_cycle_detail.go

View check run for this annotation

Codecov / codecov/patch

shortcuts/okr/okr_cycle_detail.go#L83

Added line #L83 was not covered by tests
}
var obj Objective
if err := json.Unmarshal(raw, &obj); err != nil {
continue

Check warning on line 87 in shortcuts/okr/okr_cycle_detail.go

View check run for this annotation

Codecov / codecov/patch

shortcuts/okr/okr_cycle_detail.go#L87

Added line #L87 was not covered by tests
}
objectives = append(objectives, obj)
}

hasMore, pageToken := common.PaginationMeta(data)
if !hasMore || pageToken == "" {
break
}
queryParams.Set("page_token", pageToken)
}

// For each objective, paginate key results and convert to response format.
respObjectives := make([]*RespObjective, 0, len(objectives))
for i := range objectives {
if err := ctx.Err(); err != nil {
return err

Check warning on line 103 in shortcuts/okr/okr_cycle_detail.go

View check run for this annotation

Codecov / codecov/patch

shortcuts/okr/okr_cycle_detail.go#L103

Added line #L103 was not covered by tests
}
obj := &objectives[i]

krQuery := make(larkcore.QueryParams)
krQuery.Set("page_size", "100")

var keyResults []KeyResult
krPage := 0
for {
if err := ctx.Err(); err != nil {
return err

Check warning on line 114 in shortcuts/okr/okr_cycle_detail.go

View check run for this annotation

Codecov / codecov/patch

shortcuts/okr/okr_cycle_detail.go#L114

Added line #L114 was not covered by tests
}
if krPage > 0 {
select {
case <-ctx.Done():
return ctx.Err()

Check warning on line 119 in shortcuts/okr/okr_cycle_detail.go

View check run for this annotation

Codecov / codecov/patch

shortcuts/okr/okr_cycle_detail.go#L118-L119

Added lines #L118 - L119 were not covered by tests
case <-time.After(500 * time.Millisecond):
}
}
krPage++

path := fmt.Sprintf("/open-apis/okr/v2/objectives/%s/key_results", obj.ID)
data, err := runtime.DoAPIJSON("GET", path, krQuery, nil)
if err != nil {
return err

Check warning on line 128 in shortcuts/okr/okr_cycle_detail.go

View check run for this annotation

Codecov / codecov/patch

shortcuts/okr/okr_cycle_detail.go#L128

Added line #L128 was not covered by tests
}

itemsRaw, _ := data["items"].([]interface{})
for _, item := range itemsRaw {
raw, err := json.Marshal(item)
if err != nil {
continue

Check warning on line 135 in shortcuts/okr/okr_cycle_detail.go

View check run for this annotation

Codecov / codecov/patch

shortcuts/okr/okr_cycle_detail.go#L135

Added line #L135 was not covered by tests
}
var kr KeyResult
if err := json.Unmarshal(raw, &kr); err != nil {
continue

Check warning on line 139 in shortcuts/okr/okr_cycle_detail.go

View check run for this annotation

Codecov / codecov/patch

shortcuts/okr/okr_cycle_detail.go#L139

Added line #L139 was not covered by tests
}
keyResults = append(keyResults, kr)
}

hasMore, pageToken := common.PaginationMeta(data)
if !hasMore || pageToken == "" {
break
}
krQuery.Set("page_token", pageToken)
}

respObj := obj.ToResp()
if respObj == nil {
continue

Check warning on line 153 in shortcuts/okr/okr_cycle_detail.go

View check run for this annotation

Codecov / codecov/patch

shortcuts/okr/okr_cycle_detail.go#L153

Added line #L153 was not covered by tests
}
respKRs := make([]RespKeyResult, 0, len(keyResults))
for j := range keyResults {
if r := keyResults[j].ToResp(); r != nil {
respKRs = append(respKRs, *r)
}
}
respObj.KeyResults = respKRs
respObjectives = append(respObjectives, respObj)
}

result := map[string]interface{}{
"cycle_id": cycleID,
"objectives": respObjectives,
"total": len(respObjectives),
}

runtime.OutFormat(result, nil, func(w io.Writer) {
fmt.Fprintf(w, "Cycle %s: %d objective(s)\n", cycleID, len(respObjectives))
for _, o := range respObjectives {
fmt.Fprintf(w, "Objective [%s]: %s \n Notes: %s \n score=%.2f weight=%.2f\n", o.ID, ptrStr(o.Content), ptrStr(o.Notes), ptrFloat64(o.Score), ptrFloat64(o.Weight))
for _, kr := range o.KeyResults {
fmt.Fprintf(w, " - KR [%s]: %s \n score=%.2f weight=%.2f\n", kr.ID, ptrStr(kr.Content), ptrFloat64(kr.Score), ptrFloat64(kr.Weight))

Check warning on line 176 in shortcuts/okr/okr_cycle_detail.go

View check run for this annotation

Codecov / codecov/patch

shortcuts/okr/okr_cycle_detail.go#L172-L176

Added lines #L172 - L176 were not covered by tests
}
}
})
return nil
},
}
Loading
Loading