okr

package
v1.0.65 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 3, 2026 License: MIT Imports: 17 Imported by: 0

Documentation

Index

Constants

This section is empty.

Variables

View Source
var OKRBatchCreate = common.Shortcut{
	Service:     "okr",
	Command:     "+batch-create",
	Description: "Batch create OKR objectives and key results with rollback on failure",
	Risk:        "write",
	Scopes:      []string{"okr:okr.content:writeonly"},
	AuthTypes:   []string{"user", "bot"},
	HasFormat:   true,
	Flags: []common.Flag{
		{Name: "cycle-id", Desc: "OKR cycle ID (int64)", Required: true},
		{Name: "input", Desc: "JSON array of objectives: [{\"text\":\"...\",\"mention\":[\"...\"],\"krs\":[{\"text\":\"...\",\"mention\":[\"...\"]}]}]", Input: []string{common.File, common.Stdin}, Required: true},
		{Name: "user-id-type", Default: "open_id", Desc: "user ID type: open_id | union_id | user_id"},
	},
	Validate: func(ctx context.Context, runtime *common.RuntimeContext) error {
		cycleID := runtime.Str("cycle-id")
		if id, err := strconv.ParseInt(cycleID, 10, 64); err != nil || id <= 0 {
			return errs.NewValidationError(errs.SubtypeInvalidArgument, "--cycle-id must be a positive int64").WithParam("--cycle-id")
		}

		input := runtime.Str("input")
		if err := common.RejectDangerousCharsTyped("--input", input); err != nil {
			return err
		}
		if _, err := parseBatchCreateInput(input); err != nil {
			return err
		}

		idType := runtime.Str("user-id-type")
		if idType != "open_id" && idType != "union_id" && idType != "user_id" {
			return errs.NewValidationError(errs.SubtypeInvalidArgument, "--user-id-type must be one of: open_id | union_id | user_id").WithParam("--user-id-type")
		}

		return nil
	},
	DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI {
		cycleID := runtime.Str("cycle-id")
		userIDType := runtime.Str("user-id-type")
		objectives, _ := parseBatchCreateInput(runtime.Str("input"))

		apis := common.NewDryRunAPI()

		for i, obj := range objectives {

			objContent := BuildContentBlock(obj.Text, obj.Mention)
			objBody := map[string]interface{}{
				"content": objContent,
			}
			objParams := map[string]interface{}{
				"cycle_id":     cycleID,
				"user_id_type": userIDType,
			}
			objPath := fmt.Sprintf("/open-apis/okr/v2/cycles/%s/objectives", cycleID)
			apis = apis.
				POST(objPath).
				Params(objParams).
				Body(objBody).
				Desc(fmt.Sprintf("Create objective[%d]: %s", i, obj.Text))

			for j, kr := range obj.KRs {
				krContent := BuildContentBlock(kr.Text, kr.Mention)
				krBody := map[string]interface{}{
					"content": krContent,
				}
				krParams := map[string]interface{}{
					"objective_id": "<objective_id_from_previous_call>",
					"user_id_type": userIDType,
				}
				krPath := "/open-apis/okr/v2/objectives/<objective_id>/key_results"
				apis = apis.
					POST(krPath).
					Params(krParams).
					Body(krBody).
					Desc(fmt.Sprintf("Create objective[%d].krs[%d]: %s", i, j, kr.Text))
			}
		}

		return apis
	},
	Execute: func(ctx context.Context, runtime *common.RuntimeContext) error {
		cycleID := runtime.Str("cycle-id")
		userIDType := runtime.Str("user-id-type")
		objectives, err := parseBatchCreateInput(runtime.Str("input"))
		if err != nil {
			return err
		}

		var created []createdObjective

		for i, obj := range objectives {

			if i > 0 {
				time.Sleep(500 * time.Millisecond)
			}

			objectiveID, err := createObjective(ctx, runtime, cycleID, userIDType, obj)
			if err != nil {
				if len(created) == 0 {
					return err
				}
				rollbackErrs := rollback(ctx, runtime, created)
				return buildRollbackError(err, rollbackErrs, created)
			}

			createdObj := createdObjective{
				ObjectiveID: objectiveID,
			}

			for j, kr := range obj.KRs {

				if j > 0 {
					time.Sleep(500 * time.Millisecond)
				}

				krID, err := createKR(ctx, runtime, objectiveID, userIDType, kr)
				if err != nil {
					created = append(created, createdObj)
					rollbackErrs := rollback(ctx, runtime, created)
					return buildRollbackError(err, rollbackErrs, created)
				}

				createdObj.KRIDs = append(createdObj.KRIDs, krID)
			}

			created = append(created, createdObj)
		}

		respCreated := make([]map[string]interface{}, 0, len(created))
		for _, obj := range created {
			respCreated = append(respCreated, map[string]interface{}{
				"objective_id": obj.ObjectiveID,
				"krs":          obj.KRIDs,
			})
		}

		result := map[string]interface{}{
			"ok":   true,
			"data": map[string]interface{}{"created": respCreated},
		}

		runtime.OutFormat(result, nil, func(w io.Writer) {
			fmt.Fprintf(w, "Successfully created %d objective(s)\n", len(created))
			for i, obj := range created {
				fmt.Fprintf(w, "Objective[%d] ID: %s (%d KR(s))\n", i, obj.ObjectiveID, len(obj.KRIDs))
				for j, krID := range obj.KRIDs {
					fmt.Fprintf(w, "  KR[%d] ID: %s\n", j, krID)
				}
			}
		})

		return nil
	},
}

OKRBatchCreate batch creates objectives and their key results.

View Source
var OKRCreateProgressRecord = common.Shortcut{
	Service:     "okr",
	Command:     "+progress-create",
	Description: "Create an OKR progress",
	Risk:        "write",
	Scopes:      []string{"okr:okr.progress:writeonly"},
	AuthTypes:   []string{"user", "bot"},
	HasFormat:   true,
	Flags: []common.Flag{
		{Name: "content", Desc: "progress content: semi-plain JSON {\"text\":\"...\",\"mention\":[\"...\"]} (simple style) or ContentBlock JSON (richtext style)", Required: true, Input: []string{common.File, common.Stdin}},
		{Name: "target-id", Desc: "target ID (objective or key result ID)", Required: true},
		{Name: "target-type", Desc: "target type: objective | key_result", Required: true, Enum: []string{"objective", "key_result"}},
		{Name: "progress-percent", Desc: "progress percentage"},
		{Name: "progress-status", Desc: "progress status: normal | overdue | done. must provided with --progress-percent", Enum: []string{"normal", "overdue", "done"}},
		{Name: "source-title", Default: "created by lark-cli", Desc: "source title for display"},
		{Name: "source-url", Desc: "source URL for display (defaults to open platform URL based on brand)"},
		{Name: "user-id-type", Default: "open_id", Desc: "user ID type: open_id | union_id | user_id"},
		{Name: "style", Default: "simple", Desc: "input style: simple (semi-plain text JSON) | richtext (ContentBlock JSON)", Enum: []string{"simple", "richtext"}},
	},
	Validate: func(ctx context.Context, runtime *common.RuntimeContext) error {
		content := runtime.Str("content")
		if content == "" {
			return errs.NewValidationError(errs.SubtypeInvalidArgument, "--content is required").WithParam("--content")
		}
		if err := common.RejectDangerousCharsTyped("--content", content); err != nil {
			return err
		}

		style := runtime.Str("style")
		if style != "simple" && style != "richtext" {
			return errs.NewValidationError(errs.SubtypeInvalidArgument, "--style must be one of: simple | richtext").WithParam("--style")
		}

		if style == "simple" {
			var sp SemiPlainContent
			if err := json.Unmarshal([]byte(content), &sp); err != nil {
				return errs.NewValidationError(errs.SubtypeInvalidArgument, "--content must be valid semi-plain JSON: {\"text\":\"...\",\"mention\":[\"...\"]}: %s", err).WithParam("--content").WithCause(err)
			}
			if strings.TrimSpace(sp.Text) == "" {
				return errs.NewValidationError(errs.SubtypeInvalidArgument, "--content text is required and cannot be empty").WithParam("--content")
			}
			for i, m := range sp.Mention {
				if strings.TrimSpace(m) == "" {
					return errs.NewValidationError(errs.SubtypeInvalidArgument, "--content mention[%d] cannot be empty", i).WithParam("--content")
				}
			}

			if len(sp.Docs) > 0 || len(sp.Images) > 0 {
				return errs.NewValidationError(errs.SubtypeInvalidArgument, "--content docs and images are not supported in simple style input; use richtext style or remove these fields").WithParam("--content")
			}
		} else {
			// richtext mode
			var cb ContentBlock
			if err := json.Unmarshal([]byte(content), &cb); err != nil {
				return errs.NewValidationError(errs.SubtypeInvalidArgument, "--content must be valid ContentBlock JSON: %s", err).WithParam("--content").WithCause(err)
			}
		}

		targetID := runtime.Str("target-id")
		if targetID == "" {
			return errs.NewValidationError(errs.SubtypeInvalidArgument, "--target-id is required").WithParam("--target-id")
		}
		if err := common.RejectDangerousCharsTyped("--target-id", targetID); err != nil {
			return err
		}
		if id, err := strconv.ParseInt(targetID, 10, 64); err != nil || id <= 0 {
			return errs.NewValidationError(errs.SubtypeInvalidArgument, "--target-id must be a positive int64").WithParam("--target-id")
		}

		targetType := runtime.Str("target-type")
		if _, ok := targetTypeAllowed[targetType]; !ok {
			return errs.NewValidationError(errs.SubtypeInvalidArgument, "--target-type must be one of: objective | key_result").WithParam("--target-type")
		}

		if v := runtime.Str("source-title"); v != "" {
			if err := common.RejectDangerousCharsTyped("--source-title", v); err != nil {
				return err
			}
		}
		if v := runtime.Str("source-url"); v != "" {
			if err := common.RejectDangerousCharsTyped("--source-url", v); err != nil {
				return err
			}
		}

		if v := runtime.Str("progress-percent"); v != "" {
			percent, err := strconv.ParseFloat(v, 64)
			if err != nil || math.IsNaN(percent) || math.IsInf(percent, 0) || percent < -99999999999 || percent > 99999999999 {
				return errs.NewValidationError(errs.SubtypeInvalidArgument, "--progress-percent must be a number between -99999999999 and 99999999999").WithParam("--progress-percent")
			}
		}
		if v := runtime.Str("progress-status"); v != "" {
			if _, ok := ParseProgressStatus(v); !ok {
				return errs.NewValidationError(errs.SubtypeInvalidArgument, "--progress-status must be one of: normal | overdue | done").WithParam("--progress-status")
			}
			if v := runtime.Str("progress-percent"); v == "" {
				return errs.NewValidationError(errs.SubtypeInvalidArgument, "--progress-percent must provided with --progress-status").WithParam("--progress-percent")
			}
		}

		idType := runtime.Str("user-id-type")
		if idType != "open_id" && idType != "union_id" && idType != "user_id" {
			return errs.NewValidationError(errs.SubtypeInvalidArgument, "--user-id-type must be one of: open_id | union_id | user_id").WithParam("--user-id-type")
		}
		return nil
	},
	DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI {
		p, _ := parseCreateProgressRecordParams(runtime)
		params := map[string]interface{}{
			"user_id_type": p.UserIDType,
		}
		body := map[string]interface{}{
			"content":      p.ContentV1,
			"target_id":    p.TargetID,
			"target_type":  p.TargetType,
			"source_title": p.SourceTitle,
			"source_url":   p.SourceURL,
		}
		if p.ProgressRate != nil {
			body["progress_rate"] = p.ProgressRate
		}
		return common.NewDryRunAPI().
			POST("/open-apis/okr/v1/progress_records/").
			Params(params).
			Body(body).
			Desc(fmt.Sprintf("Create OKR progress for %s", runtime.Str("target-type")))
	},
	Execute: func(ctx context.Context, runtime *common.RuntimeContext) error {
		p, err := parseCreateProgressRecordParams(runtime)
		if err != nil {
			return err
		}

		body := map[string]interface{}{
			"content":      p.ContentV1,
			"target_id":    p.TargetID,
			"target_type":  p.TargetType,
			"source_title": p.SourceTitle,
			"source_url":   p.SourceURL,
		}
		if p.ProgressRate != nil {
			body["progress_rate"] = p.ProgressRate
		}

		queryParams := map[string]interface{}{"user_id_type": p.UserIDType}

		data, err := runtime.CallAPITyped("POST", "/open-apis/okr/v1/progress_records/", queryParams, body)
		if err != nil {
			return err
		}

		record, err := parseProgressRecord(data)
		if err != nil {
			return err
		}

		style := runtime.Str("style")
		var result map[string]interface{}
		if style == "simple" {
			resp := record.ToSimple()
			result = map[string]interface{}{
				"progress": resp,
				"style":    style,
			}

			runtime.OutFormat(result, nil, func(w io.Writer) {
				fmt.Fprintf(w, "Created Progress [%s] (style: %s)\n", resp.ID, style)
				fmt.Fprintf(w, "  ModifyTime: %s\n", resp.ModifyTime)
				if resp.ProgressRate != nil && resp.ProgressRate.Percent != nil {
					fmt.Fprintf(w, "  ProgressRate: %.1f%%\n", *resp.ProgressRate.Percent)
				}
				if resp.Content != nil {
					fmt.Fprintf(w, "  Content: %s\n", resp.Content.Text)
				}
			})
		} else {
			resp := record.ToResp()
			result = map[string]interface{}{
				"progress": resp,
				"style":    style,
			}

			runtime.OutFormat(result, nil, func(w io.Writer) {
				fmt.Fprintf(w, "Created Progress [%s] (style: %s)\n", resp.ID, style)
				fmt.Fprintf(w, "  ModifyTime: %s\n", resp.ModifyTime)
				if resp.ProgressRate != nil && resp.ProgressRate.Percent != nil {
					fmt.Fprintf(w, "  ProgressRate: %.1f%%\n", *resp.ProgressRate.Percent)
				}
				if resp.Content != nil {
					fmt.Fprintf(w, "  Content: %s\n", *resp.Content)
				}
			})
		}
		return nil
	},
}

OKRCreateProgressRecord creates a progress.

View Source
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},
		{Name: "style", Default: "simple", Desc: "output style: simple (semi-plain text JSON) | richtext (ContentBlock JSON)", Enum: []string{"simple", "richtext"}},
	},
	Validate: func(ctx context.Context, runtime *common.RuntimeContext) error {
		cycleID := runtime.Str("cycle-id")
		if cycleID == "" {
			return errs.NewValidationError(errs.SubtypeInvalidArgument, "--cycle-id is required").WithParam("--cycle-id")
		}
		if id, err := strconv.ParseInt(cycleID, 10, 64); err != nil || id <= 0 {
			return errs.NewValidationError(errs.SubtypeInvalidArgument, "--cycle-id must be a positive int64").WithParam("--cycle-id")
		}
		style := runtime.Str("style")
		if style != "simple" && style != "richtext" {
			return errs.NewValidationError(errs.SubtypeInvalidArgument, "--style must be one of: simple | richtext").WithParam("--style")
		}
		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")
		style := runtime.Str("style")

		queryParams := map[string]interface{}{"page_size": "100"}

		var objectives []Objective
		page := 0
		for {
			if err := ctx.Err(); err != nil {
				return err
			}
			if page > 0 {
				select {
				case <-ctx.Done():
					return ctx.Err()
				case <-time.After(500 * time.Millisecond):
				}
			}
			page++

			path := fmt.Sprintf("/open-apis/okr/v2/cycles/%s/objectives", cycleID)
			data, err := runtime.CallAPITyped("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
				}
				var obj Objective
				if err := json.Unmarshal(raw, &obj); err != nil {
					continue
				}
				objectives = append(objectives, obj)
			}

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

		if style == "simple" {
			respObjectives := make([]*RespObjectiveSimple, 0, len(objectives))
			for i := range objectives {
				if err := ctx.Err(); err != nil {
					return err
				}
				obj := &objectives[i]

				keyResults, err := fetchKeyResults(ctx, runtime, obj.ID)
				if err != nil {
					return err
				}

				respObj := obj.ToSimple()
				if respObj == nil {
					continue
				}
				respKRs := make([]RespKeyResultSimple, 0, len(keyResults))
				for j := range keyResults {
					if r := keyResults[j].ToSimple(); 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),
				"style":      style,
			}

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

			respObjectives := make([]*RespObjective, 0, len(objectives))
			for i := range objectives {
				if err := ctx.Err(); err != nil {
					return err
				}
				obj := &objectives[i]

				keyResults, err := fetchKeyResults(ctx, runtime, obj.ID)
				if err != nil {
					return err
				}

				respObj := obj.ToResp()
				if respObj == nil {
					continue
				}
				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),
				"style":      style,
			}

			runtime.OutFormat(result, nil, func(w io.Writer) {
				fmt.Fprintf(w, "Cycle %s: %d objective(s) (style: %s)\n", cycleID, len(respObjectives), style)
				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))
					}
				}
			})
		}
		return nil
	},
}

OKRCycleDetail lists all objectives and their key results under a given OKR cycle.

View Source
var OKRDeleteProgressRecord = common.Shortcut{
	Service:     "okr",
	Command:     "+progress-delete",
	Description: "Delete an OKR progress by ID",
	Risk:        "high-risk-write",
	Scopes:      []string{"okr:okr.progress:delete"},
	AuthTypes:   []string{"user", "bot"},
	HasFormat:   true,
	Flags: []common.Flag{
		{Name: "progress-id", Desc: "progress ID (int64)", Required: true},
	},
	Validate: func(ctx context.Context, runtime *common.RuntimeContext) error {
		progressID := runtime.Str("progress-id")
		if progressID == "" {
			return errs.NewValidationError(errs.SubtypeInvalidArgument, "--progress-id is required").WithParam("--progress-id")
		}
		if id, err := strconv.ParseInt(progressID, 10, 64); err != nil || id <= 0 {
			return errs.NewValidationError(errs.SubtypeInvalidArgument, "--progress-id must be a positive int64").WithParam("--progress-id")
		}
		return nil
	},
	DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI {
		progressID := runtime.Str("progress-id")
		return common.NewDryRunAPI().
			DELETE("/open-apis/okr/v1/progress_records/:progress_id").
			Set("progress_id", progressID).
			Desc("Delete OKR progress")
	},
	Execute: func(ctx context.Context, runtime *common.RuntimeContext) error {
		progressID := runtime.Str("progress-id")

		path := fmt.Sprintf("/open-apis/okr/v1/progress_records/%s", progressID)
		_, err := runtime.CallAPITyped("DELETE", path, nil, nil)
		if err != nil {
			return err
		}

		result := map[string]interface{}{
			"deleted":     true,
			"progress_id": progressID,
		}

		runtime.OutFormat(result, nil, func(w io.Writer) {
			fmt.Fprintf(w, "Deleted progress record %s\n", progressID)
		})
		return nil
	},
}

OKRDeleteProgressRecord deletes a progress by ID.

View Source
var OKRGetProgressRecord = common.Shortcut{
	Service:     "okr",
	Command:     "+progress-get",
	Description: "Get an OKR progress by ID",
	Risk:        "read",
	Scopes:      []string{"okr:okr.progress:readonly"},
	AuthTypes:   []string{"user", "bot"},
	HasFormat:   true,
	Flags: []common.Flag{
		{Name: "progress-id", Desc: "progress ID (int64)", Required: true},
		{Name: "user-id-type", Default: "open_id", Desc: "user ID type: open_id | union_id | user_id"},
		{Name: "style", Default: "simple", Desc: "output style: simple (semi-plain text JSON) | richtext (ContentBlock JSON)", Enum: []string{"simple", "richtext"}},
	},
	Validate: func(ctx context.Context, runtime *common.RuntimeContext) error {
		progressID := runtime.Str("progress-id")
		if progressID == "" {
			return errs.NewValidationError(errs.SubtypeInvalidArgument, "--progress-id is required").WithParam("--progress-id")
		}
		if id, err := strconv.ParseInt(progressID, 10, 64); err != nil || id <= 0 {
			return errs.NewValidationError(errs.SubtypeInvalidArgument, "--progress-id must be a positive int64").WithParam("--progress-id")
		}
		idType := runtime.Str("user-id-type")
		if idType != "open_id" && idType != "union_id" && idType != "user_id" {
			return errs.NewValidationError(errs.SubtypeInvalidArgument, "--user-id-type must be one of: open_id | union_id | user_id").WithParam("--user-id-type")
		}
		style := runtime.Str("style")
		if style != "simple" && style != "richtext" {
			return errs.NewValidationError(errs.SubtypeInvalidArgument, "--style must be one of: simple | richtext").WithParam("--style")
		}
		return nil
	},
	DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI {
		progressID := runtime.Str("progress-id")
		params := map[string]interface{}{
			"user_id_type": runtime.Str("user-id-type"),
		}
		return common.NewDryRunAPI().
			GET("/open-apis/okr/v1/progress_records/:progress_id").
			Params(params).
			Set("progress_id", progressID).
			Desc("Get OKR progress")
	},
	Execute: func(ctx context.Context, runtime *common.RuntimeContext) error {
		progressID := runtime.Str("progress-id")
		userIDType := runtime.Str("user-id-type")
		style := runtime.Str("style")

		queryParams := map[string]interface{}{"user_id_type": userIDType}

		path := fmt.Sprintf("/open-apis/okr/v1/progress_records/%s", progressID)
		data, err := runtime.CallAPITyped("GET", path, queryParams, nil)
		if err != nil {
			return err
		}

		record, err := parseProgressRecord(data)
		if err != nil {
			return err
		}

		var result map[string]interface{}
		if style == "simple" {
			resp := record.ToSimple()
			result = map[string]interface{}{
				"progress": resp,
				"style":    style,
			}

			runtime.OutFormat(result, nil, func(w io.Writer) {
				fmt.Fprintf(w, "Progress [%s] (style: %s)\n", resp.ID, style)
				fmt.Fprintf(w, "  ModifyTime: %s\n", resp.ModifyTime)
				if resp.ProgressRate != nil && resp.ProgressRate.Percent != nil {
					fmt.Fprintf(w, "  ProgressRate: %.1f%%\n", *resp.ProgressRate.Percent)
				}
				if resp.Content != nil {
					fmt.Fprintf(w, "  Content: %s\n", resp.Content.Text)
					if len(resp.Content.Mention) > 0 {
						fmt.Fprintf(w, "  Mentions: %v\n", resp.Content.Mention)
					}
				}
			})
		} else {
			resp := record.ToResp()
			result = map[string]interface{}{
				"progress": resp,
				"style":    style,
			}

			runtime.OutFormat(result, nil, func(w io.Writer) {
				fmt.Fprintf(w, "Progress [%s] (style: %s)\n", resp.ID, style)
				fmt.Fprintf(w, "  ModifyTime: %s\n", resp.ModifyTime)
				if resp.ProgressRate != nil && resp.ProgressRate.Percent != nil {
					fmt.Fprintf(w, "  ProgressRate: %.1f%%\n", *resp.ProgressRate.Percent)
				}
				if resp.Content != nil {
					fmt.Fprintf(w, "  Content: %s\n", *resp.Content)
				}
			})
		}
		return nil
	},
}

OKRGetProgressRecord gets a progress by ID.

View Source
var OKRIndicatorUpdate = common.Shortcut{
	Service:     "okr",
	Command:     "+indicator-update",
	Description: "Update the indicator current value for an objective or key result",
	Risk:        "write",
	Scopes:      []string{"okr:okr.content:writeonly"},
	AuthTypes:   []string{"user", "bot"},
	HasFormat:   true,
	Flags: []common.Flag{
		{Name: "level", Desc: "level to update: objective | key-result, Required.", Enum: []string{"objective", "key-result"}},
		{Name: "id", Desc: "objective or key result ID (int64), Required."},
		{Name: "value", Desc: "new current value for the indicator (number), Required."},
	},
	Validate: func(ctx context.Context, runtime *common.RuntimeContext) error {
		level := runtime.Str("level")
		if level == "" {
			return errs.NewValidationError(errs.SubtypeInvalidArgument, "--level is required").WithParam("--level")
		}
		if level != "objective" && level != "key-result" {
			return errs.NewValidationError(errs.SubtypeInvalidArgument, "--level must be one of: objective | key-result").WithParam("--level")
		}

		id := runtime.Str("id")
		if id == "" {
			return errs.NewValidationError(errs.SubtypeInvalidArgument, "--id is required").WithParam("--id")
		}
		if _, err := strconv.ParseInt(id, 10, 64); err != nil {
			return errs.NewValidationError(errs.SubtypeInvalidArgument, "--id must be a valid int64").WithParam("--id")
		}
		if runtime.Str("value") == "" {
			return errs.NewValidationError(errs.SubtypeInvalidArgument, "--value is required").WithParam("--value")
		}
		if _, err := parseIndicatorValue(runtime.Str("value")); err != nil {
			return err
		}

		return nil
	},
	DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI {
		level := runtime.Str("level")
		id := runtime.Str("id")
		value, _ := parseIndicatorValue(runtime.Str("value"))

		apis := common.NewDryRunAPI()

		var listPath string
		if level == "objective" {
			listPath = fmt.Sprintf("/open-apis/okr/v2/objectives/%s/indicators", id)
		} else {
			listPath = fmt.Sprintf("/open-apis/okr/v2/key_results/%s/indicators", id)
		}

		apis = apis.
			GET(listPath).
			Params(map[string]interface{}{"page_size": 100}).
			Desc(fmt.Sprintf("Fetch indicators for the %s to get indicator ID", level))

		patchPath := "/open-apis/okr/v2/indicators/:indicator_id"
		patchBody := map[string]interface{}{
			"current_value": value,
		}
		apis = apis.
			PATCH(patchPath).
			Body(patchBody).
			Set("indicator_id", "<indicator_id_from_list>").
			Desc("Update indicator current value")

		return apis
	},
	Execute: func(ctx context.Context, runtime *common.RuntimeContext) error {
		level := runtime.Str("level")
		id := runtime.Str("id")
		value, err := parseIndicatorValue(runtime.Str("value"))
		if err != nil {
			return err
		}

		indicatorID, err := fetchIndicatorID(ctx, runtime, level, id)
		if err != nil {
			return err
		}

		patchPath := fmt.Sprintf("/open-apis/okr/v2/indicators/%s", indicatorID)
		patchBody := map[string]interface{}{
			"current_value": value,
		}

		_, err = runtime.CallAPITyped("PATCH", patchPath, nil, patchBody)
		if err != nil {
			return wrapOkrNetworkErr(err, "failed to update indicator value")
		}

		result := map[string]interface{}{
			"indicator_id":  indicatorID,
			"current_value": value,
			"level":         level,
			"target_id":     id,
		}

		runtime.OutFormat(result, nil, func(w io.Writer) {
			fmt.Fprintf(w, "Updated Indicator [%s]\n", indicatorID)
			fmt.Fprintf(w, "  Level: %s\n", level)
			fmt.Fprintf(w, "  Target ID: %s\n", id)
			fmt.Fprintf(w, "  Current Value: %v\n", value)
		})

		return nil
	},
}

OKRIndicatorUpdate updates the current value of an indicator for an objective or key result.

View Source
var OKRListCycles = common.Shortcut{
	Service:     "okr",
	Command:     "+cycle-list",
	Description: "List okr cycles of a certain user",
	Risk:        "read",
	Scopes:      []string{"okr:okr.period:readonly"},
	AuthTypes:   []string{"user", "bot"},
	HasFormat:   true,
	Flags: []common.Flag{
		{Name: "user-id", Desc: "user ID", Required: true},
		{Name: "user-id-type", Default: "open_id", Desc: "user ID type: open_id | union_id | user_id"},
		{Name: "time-range", Desc: "specify time range. Use Format as YYYY-MM--YYYY-MM. leave empty to fetch all user cycles."},
	},
	Validate: func(ctx context.Context, runtime *common.RuntimeContext) error {
		idType := runtime.Str("user-id-type")
		if idType != "open_id" && idType != "union_id" && idType != "user_id" {
			return errs.NewValidationError(errs.SubtypeInvalidArgument, "--user-id-type must be one of: open_id | union_id | user_id").WithParam("--user-id-type")
		}
		userID := runtime.Str("user-id")
		if err := common.RejectDangerousCharsTyped("--user-id", userID); err != nil {
			return err
		}

		tr := runtime.Str("time-range")
		if tr != "" {
			if err := common.RejectDangerousCharsTyped("--time-range", tr); err != nil {
				return err
			}
			if _, _, err := parseTimeRange(tr); err != nil {
				return err
			}
		}
		return nil
	},
	DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI {
		params := map[string]interface{}{
			"user_id":      runtime.Str("user-id"),
			"user_id_type": runtime.Str("user-id-type"),
			"page_size":    100,
		}
		return common.NewDryRunAPI().
			GET("/open-apis/okr/v2/cycles").
			Params(params).
			Desc("List OKR cycles for user, paginated at 100 per page, filtered by time-range")
	},
	Execute: func(ctx context.Context, runtime *common.RuntimeContext) error {
		userID := runtime.Str("user-id")
		userIDType := runtime.Str("user-id-type")
		timeRange := runtime.Str("time-range")

		// Parse time range for filtering
		var rangeStart, rangeEnd time.Time
		var hasRange bool
		if timeRange != "" {
			var err error
			rangeStart, rangeEnd, err = parseTimeRange(timeRange)
			if err != nil {
				return err
			}
			hasRange = true
		}

		queryParams := map[string]interface{}{
			"user_id":      userID,
			"user_id_type": userIDType,
			"page_size":    "100",
		}

		var allCycles []Cycle
		page := 0
		for {
			if err := ctx.Err(); err != nil {
				return err
			}
			if page > 0 {
				select {
				case <-ctx.Done():
					return ctx.Err()
				case <-time.After(500 * time.Millisecond):
				}
			}
			page++

			data, err := runtime.CallAPITyped("GET", "/open-apis/okr/v2/cycles", queryParams, nil)
			if err != nil {
				return err
			}

			itemsRaw, _ := data["items"].([]interface{})
			for _, item := range itemsRaw {
				raw, err := json.Marshal(item)
				if err != nil {
					continue
				}
				var cycle Cycle
				if err := json.Unmarshal(raw, &cycle); err != nil {
					continue
				}
				allCycles = append(allCycles, cycle)
			}

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

		// Filter by time-range overlap
		var filtered []Cycle
		for i := range allCycles {
			if !hasRange || cycleOverlaps(&allCycles[i], rangeStart, rangeEnd) {
				filtered = append(filtered, allCycles[i])
			}
		}

		respCycles := make([]*RespCycle, 0, len(filtered))
		for i := range filtered {
			respCycles = append(respCycles, filtered[i].ToResp())
		}

		now := time.Now()
		currentActiveCycles := make([]*RespCycle, 0)
		for i := range filtered {
			if isCurrentActiveCycle(&filtered[i], now) {
				currentActiveCycles = append(currentActiveCycles, filtered[i].ToResp())
			}
		}

		runtime.OutFormat(map[string]interface{}{
			"cycles":                respCycles,
			"total":                 len(respCycles),
			"current_active_cycles": currentActiveCycles,
		}, nil, func(w io.Writer) {
			fmt.Fprintf(w, "Found %d cycle(s)\n", len(respCycles))
			for _, c := range respCycles {
				fmt.Fprintf(w, "  [%s] %s ~ %s (status: %s)\n", c.ID, c.StartTime, c.EndTime, ptrStr(c.CycleStatus))
			}
			if len(currentActiveCycles) > 0 {
				fmt.Fprintf(w, "\nCurrent active cycle(s):\n")
				for _, c := range currentActiveCycles {
					fmt.Fprintf(w, "  [%s] %s ~ %s\n", c.ID, c.StartTime, c.EndTime)
				}
			}
		})
		return nil
	},
}
View Source
var OKRListProgress = common.Shortcut{
	Service:     "okr",
	Command:     "+progress-list",
	Description: "List progress for an objective or key result",
	Risk:        "read",
	Scopes:      []string{"okr:okr.progress:readonly"},
	AuthTypes:   []string{"user", "bot"},
	HasFormat:   true,
	Flags: []common.Flag{
		{Name: "target-id", Desc: "target ID (objective or key result ID)", Required: true},
		{Name: "target-type", Desc: "target type: objective | key_result", Required: true, Enum: []string{"objective", "key_result"}},
		{Name: "user-id-type", Default: "open_id", Desc: "user ID type: open_id | union_id | user_id"},
		{Name: "department-id-type", Default: "open_department_id", Desc: "department ID type: department_id | open_department_id"},
	},
	Validate: func(ctx context.Context, runtime *common.RuntimeContext) error {
		targetID := runtime.Str("target-id")
		if targetID == "" {
			return errs.NewValidationError(errs.SubtypeInvalidArgument, "--target-id is required").WithParam("--target-id")
		}
		if err := common.RejectDangerousCharsTyped("--target-id", targetID); err != nil {
			return err
		}
		if id, err := strconv.ParseInt(targetID, 10, 64); err != nil || id <= 0 {
			return errs.NewValidationError(errs.SubtypeInvalidArgument, "--target-id must be a positive int64").WithParam("--target-id")
		}

		targetType := runtime.Str("target-type")
		if _, ok := targetTypeAllowed[targetType]; !ok {
			return errs.NewValidationError(errs.SubtypeInvalidArgument, "--target-type must be one of: objective | key_result").WithParam("--target-type")
		}

		idType := runtime.Str("user-id-type")
		if idType != "open_id" && idType != "union_id" && idType != "user_id" {
			return errs.NewValidationError(errs.SubtypeInvalidArgument, "--user-id-type must be one of: open_id | union_id | user_id").WithParam("--user-id-type")
		}

		deptIDType := runtime.Str("department-id-type")
		if deptIDType != "department_id" && deptIDType != "open_department_id" {
			return errs.NewValidationError(errs.SubtypeInvalidArgument, "--department-id-type must be one of: department_id | open_department_id").WithParam("--department-id-type")
		}
		return nil
	},
	DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI {
		targetID := runtime.Str("target-id")
		targetType := runtime.Str("target-type")
		params := map[string]interface{}{
			"user_id_type":       runtime.Str("user-id-type"),
			"department_id_type": runtime.Str("department-id-type"),
			"page_size":          100,
		}

		switch targetType {
		case "objective":
			return common.NewDryRunAPI().
				GET("/open-apis/okr/v2/objectives/:objective_id/progresses").
				Params(params).
				Set("objective_id", targetID).
				Desc("List progresses for objective")
		case "key_result":
			return common.NewDryRunAPI().
				GET("/open-apis/okr/v2/key_results/:key_result_id/progresses").
				Params(params).
				Set("key_result_id", targetID).
				Desc("List progresses for key result")
		}
		return nil
	},
	Execute: func(ctx context.Context, runtime *common.RuntimeContext) error {
		targetID := runtime.Str("target-id")
		targetType := runtime.Str("target-type")
		userIDType := runtime.Str("user-id-type")
		deptIDType := runtime.Str("department-id-type")

		queryParams := map[string]interface{}{
			"user_id_type":       userIDType,
			"department_id_type": deptIDType,
			"page_size":          "100",
		}

		var apiPath string
		switch targetType {
		case "objective":
			apiPath = fmt.Sprintf("/open-apis/okr/v2/objectives/%s/progresses", targetID)
		case "key_result":
			apiPath = fmt.Sprintf("/open-apis/okr/v2/key_results/%s/progresses", targetID)
		}

		var allProgress []*Progress
		for {
			if err := ctx.Err(); err != nil {
				return err
			}

			data, err := runtime.CallAPITyped("GET", apiPath, queryParams, nil)
			if err != nil {
				return err
			}

			itemsRaw, _ := data["items"].([]interface{})
			for _, item := range itemsRaw {
				raw, err := json.Marshal(item)
				if err != nil {
					continue
				}
				var progress Progress
				if err := json.Unmarshal(raw, &progress); err != nil {
					continue
				}
				allProgress = append(allProgress, &progress)
			}

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

		respProgress := make([]*RespProgress, 0, len(allProgress))
		for _, p := range allProgress {
			respProgress = append(respProgress, p.ToResp())
		}

		runtime.OutFormat(map[string]interface{}{
			"progress_list": respProgress,
			"total":         len(respProgress),
		}, nil, func(w io.Writer) {
			fmt.Fprintf(w, "Found %d progress(es)\n", len(respProgress))
			for _, p := range respProgress {
				fmt.Fprintf(w, "  [%s] , %s", p.ID, p.ModifyTime)
				if p.ProgressRate != nil && p.ProgressRate.Percent != nil {
					fmt.Fprintf(w, " (%.2f%%", *p.ProgressRate.Percent)
					if p.ProgressRate.Status != nil {
						fmt.Fprintf(w, ", %s", *p.ProgressRate.Status)
					}
					fmt.Fprintf(w, ")\n")
					if p.Content != nil {
						fmt.Fprintf(w, "  Content: %s\n", *p.Content)
					}
				}
				fmt.Fprintln(w)
			}
		})
		return nil
	},
}

OKRListProgress lists progress for an objective or key result.

View Source
var OKRPatch = common.Shortcut{
	Service:     "okr",
	Command:     "+patch",
	Description: "Patch an OKR objective or key result (content, notes, score, deadline)",
	Risk:        "write",
	Scopes:      []string{"okr:okr.content:writeonly"},
	AuthTypes:   []string{"user", "bot"},
	HasFormat:   true,
	Flags: []common.Flag{
		{Name: "level", Desc: "patch level: objective | key-result", Required: true, Enum: []string{"objective", "key-result"}},
		{Name: "target-id", Desc: "target ID (objective or key result ID)", Required: true},
		{Name: "style", Default: "simple", Desc: "input style for content/notes: simple (semi-plain text JSON) | richtext (ContentBlock JSON)", Enum: []string{"simple", "richtext"}},
		{Name: "content", Desc: "content: semi-plain JSON {\"text\":\"...\",\"mention\":[\"...\"]} (simple) or ContentBlock JSON (richtext)", Input: []string{common.File, common.Stdin}},
		{Name: "notes", Desc: "notes (objective only): semi-plain JSON {\"text\":\"...\",\"mention\":[\"...\"]} (simple) or ContentBlock JSON (richtext)", Input: []string{common.File, common.Stdin}},
		{Name: "score", Desc: "score value between 0 and 1, with at most one decimal place (e.g., 0.5)"},
		{Name: "deadline", Desc: "deadline as millisecond timestamp"},
		{Name: "user-id-type", Default: "open_id", Desc: "user ID type: open_id | union_id | user_id"},
	},
	Validate: func(ctx context.Context, runtime *common.RuntimeContext) error {
		level := runtime.Str("level")
		if level != "objective" && level != "key-result" {
			return errs.NewValidationError(errs.SubtypeInvalidArgument, "--level must be one of: objective | key-result").WithParam("--level")
		}

		targetID := runtime.Str("target-id")
		if targetID == "" {
			return errs.NewValidationError(errs.SubtypeInvalidArgument, "--target-id is required").WithParam("--target-id")
		}
		if err := common.RejectDangerousCharsTyped("--target-id", targetID); err != nil {
			return err
		}
		if id, err := strconv.ParseInt(targetID, 10, 64); err != nil || id <= 0 {
			return errs.NewValidationError(errs.SubtypeInvalidArgument, "--target-id must be a positive int64").WithParam("--target-id")
		}

		style := runtime.Str("style")
		if style != "simple" && style != "richtext" {
			return errs.NewValidationError(errs.SubtypeInvalidArgument, "--style must be one of: simple | richtext").WithParam("--style")
		}

		idType := runtime.Str("user-id-type")
		if idType != "open_id" && idType != "union_id" && idType != "user_id" {
			return errs.NewValidationError(errs.SubtypeInvalidArgument, "--user-id-type must be one of: open_id | union_id | user_id").WithParam("--user-id-type")
		}

		if _, err := parsePatchParams(runtime); err != nil {
			return err
		}

		return nil
	},
	DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI {
		p, err := parsePatchParams(runtime)
		if err != nil {
			return common.NewDryRunAPI().
				PATCH("").
				Desc(fmt.Sprintf("Dry-run skipped: %s", err.Error()))
		}

		body := make(map[string]interface{})
		if p.Content != nil {
			body["content"] = p.Content
		}
		if p.Notes != nil {
			body["notes"] = p.Notes
		}
		if p.Score != nil {
			body["score"] = *p.Score
		}
		if p.Deadline != nil {
			body["deadline"] = *p.Deadline
		}

		params := map[string]interface{}{
			"user_id_type": p.UserIDType,
		}

		api := common.NewDryRunAPI()
		if p.Level == "objective" {
			api = api.PATCH("/open-apis/okr/v2/objectives/:objective_id").
				Set("objective_id", p.TargetID)
		} else {
			api = api.PATCH("/open-apis/okr/v2/key_results/:key_result_id").
				Set("key_result_id", p.TargetID)
		}
		return api.Params(params).Body(body).
			Desc(fmt.Sprintf("Patch OKR %s: content=%v, notes=%v, score=%v, deadline=%v",
				p.Level, p.Content != nil, p.Notes != nil, p.Score != nil, p.Deadline != nil))
	},
	Execute: func(ctx context.Context, runtime *common.RuntimeContext) error {
		p, err := parsePatchParams(runtime)
		if err != nil {
			return err
		}

		body := make(map[string]interface{})
		if p.Content != nil {
			body["content"] = p.Content
		}
		if p.Notes != nil {
			body["notes"] = p.Notes
		}
		if p.Score != nil {
			body["score"] = *p.Score
		}
		if p.Deadline != nil {
			body["deadline"] = *p.Deadline
		}

		queryParams := map[string]interface{}{
			"user_id_type": p.UserIDType,
		}

		var path string
		if p.Level == "objective" {
			path = fmt.Sprintf("/open-apis/okr/v2/objectives/%s", p.TargetID)
		} else {
			path = fmt.Sprintf("/open-apis/okr/v2/key_results/%s", p.TargetID)
		}

		_, err = runtime.CallAPITyped("PATCH", path, queryParams, body)
		if err != nil {
			return wrapOkrNetworkErr(err, "failed to patch OKR %s", p.Level)
		}

		result := map[string]interface{}{
			"level":     p.Level,
			"target_id": p.TargetID,
			"patched": map[string]bool{
				"content":  p.Content != nil,
				"notes":    p.Notes != nil,
				"score":    p.Score != nil,
				"deadline": p.Deadline != nil,
			},
		}

		runtime.OutFormat(result, nil, func(w io.Writer) {
			fmt.Fprintf(w, "Patched OKR %s [%s]\n", p.Level, p.TargetID)
			if p.Content != nil {
				fmt.Fprintf(w, "  - content: updated\n")
			}
			if p.Notes != nil {
				fmt.Fprintf(w, "  - notes: updated\n")
			}
			if p.Score != nil {
				fmt.Fprintf(w, "  - score: %.1f\n", *p.Score)
			}
			if p.Deadline != nil {
				fmt.Fprintf(w, "  - deadline: %s\n", formatTimestamp(*p.Deadline))
			}
		})

		return nil
	},
}

OKRPatch patches an objective or key result.

View Source
var OKRReorder = common.Shortcut{
	Service:     "okr",
	Command:     "+reorder",
	Description: "Adjust the position (order) of OKR objectives or key results",
	Risk:        "write",
	Scopes:      []string{"okr:okr.content:writeonly"},
	AuthTypes:   []string{"user", "bot"},
	HasFormat:   true,
	Flags: []common.Flag{
		{Name: "level", Desc: "level to reorder: objective | key-result, Required.", Enum: []string{"objective", "key-result"}},
		{Name: "cycle-id", Desc: "OKR cycle ID (int64), Required."},
		{Name: "objective-id", Desc: "objective ID (required when --level=key-result)"},
		{Name: "ops", Desc: "JSON array of reorder operations: [{\"id\":\"...\",\"position\":1}], Required.", Input: []string{common.File, common.Stdin}},
	},
	Validate: func(ctx context.Context, runtime *common.RuntimeContext) error {
		level := runtime.Str("level")
		if strings.TrimSpace(level) == "" {
			return errs.NewValidationError(errs.SubtypeInvalidArgument, "--level is required").WithParam("--level")
		}
		if level != "objective" && level != "key-result" {
			return errs.NewValidationError(errs.SubtypeInvalidArgument, "--level must be one of: objective | key-result").WithParam("--level")
		}

		cycleID := runtime.Str("cycle-id")
		if strings.TrimSpace(cycleID) == "" {
			return errs.NewValidationError(errs.SubtypeInvalidArgument, "--cycle-id is required").WithParam("--cycle-id")
		}
		if id, err := strconv.ParseInt(cycleID, 10, 64); err != nil || id <= 0 {
			return errs.NewValidationError(errs.SubtypeInvalidArgument, "--cycle-id must be a positive int64").WithParam("--cycle-id")
		}

		if level == "key-result" {
			objID := runtime.Str("objective-id")
			if objID == "" {
				return errs.NewValidationError(errs.SubtypeInvalidArgument, "--objective-id is required when --level=key-result").WithParam("--objective-id")
			}
			if id, err := strconv.ParseInt(objID, 10, 64); err != nil || id <= 0 {
				return errs.NewValidationError(errs.SubtypeInvalidArgument, "--objective-id must be a positive int64").WithParam("--objective-id")
			}
		}

		opsStr := runtime.Str("ops")
		if strings.TrimSpace(opsStr) == "" {
			return errs.NewValidationError(errs.SubtypeInvalidArgument, "--ops is required").WithParam("--ops")
		}
		if err := common.RejectDangerousCharsTyped("--ops", opsStr); err != nil {
			return err
		}
		if _, err := parseReorderOps(opsStr); err != nil {
			return err
		}

		return nil
	},
	DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI {
		level := runtime.Str("level")
		cycleID := runtime.Str("cycle-id")
		objectiveID := runtime.Str("objective-id")
		ops, _ := parseReorderOps(runtime.Str("ops"))

		apis := common.NewDryRunAPI()

		if level == "objective" {

			listParams := map[string]interface{}{
				"page_size": 100,
			}
			listPath := fmt.Sprintf("/open-apis/okr/v2/cycles/%s/objectives", cycleID)
			apis = apis.
				GET(listPath).
				Params(listParams).
				Desc("Fetch all objectives in the cycle to determine current order")

			reorderParams := map[string]interface{}{
				"cycle_id": cycleID,
			}

			objectiveIDs := make([]string, 0, len(ops))
			for _, op := range ops {
				objectiveIDs = append(objectiveIDs, op.ID)
			}
			reorderBody := map[string]interface{}{
				"objective_ids": objectiveIDs,
			}
			reorderPath := fmt.Sprintf("/open-apis/okr/v2/cycles/%s/objectives_position", cycleID)
			apis = apis.
				PUT(reorderPath).
				Params(reorderParams).
				Body(reorderBody).
				Desc("Update objective positions (full list sent, not just changes)")
		} else {

			listParams := map[string]interface{}{
				"page_size": 100,
			}
			listPath := fmt.Sprintf("/open-apis/okr/v2/objectives/%s/key_results", objectiveID)
			apis = apis.
				GET(listPath).
				Params(listParams).
				Desc("Fetch all key results for the objective to determine current order")

			reorderParams := map[string]interface{}{
				"objective_id": objectiveID,
			}

			keyResultIDs := make([]string, 0, len(ops))
			for _, op := range ops {
				keyResultIDs = append(keyResultIDs, op.ID)
			}
			reorderBody := map[string]interface{}{
				"key_result_ids": keyResultIDs,
			}
			reorderPath := fmt.Sprintf("/open-apis/okr/v2/objectives/%s/key_results_position", objectiveID)
			apis = apis.
				PUT(reorderPath).
				Params(reorderParams).
				Body(reorderBody).
				Desc("Update key result positions (full list sent, not just changes)")
		}

		return apis
	},
	Execute: func(ctx context.Context, runtime *common.RuntimeContext) error {
		level := runtime.Str("level")
		cycleID := runtime.Str("cycle-id")
		objectiveID := runtime.Str("objective-id")
		ops, err := parseReorderOps(runtime.Str("ops"))
		if err != nil {
			return err
		}

		var reorderedIDs []string
		var total int

		if level == "objective" {
			objectives, err := fetchObjectives(ctx, runtime, cycleID)
			if err != nil {
				return err
			}
			total = len(objectives)

			reorderedIDs, err = buildReorderedIDs(objectives, ops, total)
			if err != nil {
				return err
			}

			params := map[string]interface{}{
				"cycle_id": cycleID,
			}
			body := map[string]interface{}{
				"objective_ids": reorderedIDs,
			}
			path := fmt.Sprintf("/open-apis/okr/v2/cycles/%s/objectives_position", cycleID)
			_, err = runtime.CallAPITyped("PUT", path, params, body)
			if err != nil {
				return wrapOkrNetworkErr(err, "failed to update objective positions")
			}
		} else {

			keyResults, err := fetchKeyResults(ctx, runtime, objectiveID)
			if err != nil {
				return err
			}
			total = len(keyResults)

			reorderedIDs, err = buildReorderedIDs(keyResults, ops, total)
			if err != nil {
				return err
			}

			params := map[string]interface{}{
				"objective_id": objectiveID,
			}
			body := map[string]interface{}{
				"key_result_ids": reorderedIDs,
			}
			path := fmt.Sprintf("/open-apis/okr/v2/objectives/%s/key_results_position", objectiveID)
			_, err = runtime.CallAPITyped("PUT", path, params, body)
			if err != nil {
				return wrapOkrNetworkErr(err, "failed to update key result positions")
			}
		}

		result := map[string]interface{}{
			"level":    level,
			"cycle_id": cycleID,
			"total":    total,
			"ordered":  reorderedIDs,
		}

		runtime.OutFormat(result, nil, func(w io.Writer) {
			fmt.Fprintf(w, "Successfully reordered %d %s(s)\n", total, level)
			fmt.Fprintln(w, "New order:")
			for i, id := range reorderedIDs {
				fmt.Fprintf(w, "  Position %d: %s\n", i+1, id)
			}
		})

		return nil
	},
}

OKRReorder adjusts the position of objectives or key results.

View Source
var OKRUpdateProgressRecord = common.Shortcut{
	Service:     "okr",
	Command:     "+progress-update",
	Description: "Update an OKR progress",
	Risk:        "write",
	Scopes:      []string{"okr:okr.progress:writeonly"},
	AuthTypes:   []string{"user", "bot"},
	HasFormat:   true,
	Flags: []common.Flag{
		{Name: "progress-id", Desc: "progress ID (int64)", Required: true},
		{Name: "content", Desc: "progress content: semi-plain JSON {\"text\":\"...\",\"mention\":[\"...\"]} (simple style) or ContentBlock JSON (richtext style)", Required: true, Input: []string{common.File, common.Stdin}},
		{Name: "progress-percent", Desc: "progress percentage"},
		{Name: "progress-status", Desc: "progress status: normal | overdue | done", Enum: []string{"normal", "overdue", "done"}},
		{Name: "user-id-type", Default: "open_id", Desc: "user ID type: open_id | union_id | user_id"},
		{Name: "style", Default: "simple", Desc: "input style: simple (semi-plain text JSON) | richtext (ContentBlock JSON)", Enum: []string{"simple", "richtext"}},
	},
	Validate: func(ctx context.Context, runtime *common.RuntimeContext) error {
		progressID := runtime.Str("progress-id")
		if progressID == "" {
			return errs.NewValidationError(errs.SubtypeInvalidArgument, "--progress-id is required").WithParam("--progress-id")
		}
		if id, err := strconv.ParseInt(progressID, 10, 64); err != nil || id <= 0 {
			return errs.NewValidationError(errs.SubtypeInvalidArgument, "--progress-id must be a positive int64").WithParam("--progress-id")
		}

		content := runtime.Str("content")
		if content == "" {
			return errs.NewValidationError(errs.SubtypeInvalidArgument, "--content is required").WithParam("--content")
		}
		if err := common.RejectDangerousCharsTyped("--content", content); err != nil {
			return err
		}

		style := runtime.Str("style")
		if style != "simple" && style != "richtext" {
			return errs.NewValidationError(errs.SubtypeInvalidArgument, "--style must be one of: simple | richtext").WithParam("--style")
		}

		if style == "simple" {
			var sp SemiPlainContent
			if err := json.Unmarshal([]byte(content), &sp); err != nil {
				return errs.NewValidationError(errs.SubtypeInvalidArgument, "--content must be valid semi-plain JSON: {\"text\":\"...\",\"mention\":[\"...\"]}: %s", err).WithParam("--content").WithCause(err)
			}
			if strings.TrimSpace(sp.Text) == "" {
				return errs.NewValidationError(errs.SubtypeInvalidArgument, "--content text is required and cannot be empty").WithParam("--content")
			}
			for i, m := range sp.Mention {
				if strings.TrimSpace(m) == "" {
					return errs.NewValidationError(errs.SubtypeInvalidArgument, "--content mention[%d] cannot be empty", i).WithParam("--content")
				}
			}
			if len(sp.Docs) > 0 || len(sp.Images) > 0 {
				return errs.NewValidationError(errs.SubtypeInvalidArgument, "--content docs and images are not supported in simple style input; use richtext style or remove these fields").WithParam("--content")
			}
		} else {
			// richtext mode
			var cb ContentBlock
			if err := json.Unmarshal([]byte(content), &cb); err != nil {
				return errs.NewValidationError(errs.SubtypeInvalidArgument, "--content must be valid ContentBlock JSON: %s", err).WithParam("--content").WithCause(err)
			}
		}

		if v := runtime.Str("progress-percent"); v != "" {
			percent, err := strconv.ParseFloat(v, 64)
			if err != nil || math.IsNaN(percent) || math.IsInf(percent, 0) || percent < -99999999999 || percent > 99999999999 {
				return errs.NewValidationError(errs.SubtypeInvalidArgument, "--progress-percent must be a number between -99999999999 and 99999999999").WithParam("--progress-percent")
			}
		}
		if v := runtime.Str("progress-status"); v != "" {
			if _, ok := ParseProgressStatus(v); !ok {
				return errs.NewValidationError(errs.SubtypeInvalidArgument, "--progress-status must be one of: normal | overdue | done").WithParam("--progress-status")
			}
			if v := runtime.Str("progress-percent"); v == "" {
				return errs.NewValidationError(errs.SubtypeInvalidArgument, "--progress-percent must provided with --progress-status").WithParam("--progress-percent")
			}
		}

		idType := runtime.Str("user-id-type")
		if idType != "open_id" && idType != "union_id" && idType != "user_id" {
			return errs.NewValidationError(errs.SubtypeInvalidArgument, "--user-id-type must be one of: open_id | union_id | user_id").WithParam("--user-id-type")
		}
		return nil
	},
	DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI {
		p, _ := parseUpdateProgressRecordParams(runtime)
		params := map[string]interface{}{
			"user_id_type": p.UserIDType,
		}
		body := map[string]interface{}{
			"content": p.ContentV1,
		}
		if p.ProgressRate != nil {
			body["progress_rate"] = p.ProgressRate
		}
		return common.NewDryRunAPI().
			PUT("/open-apis/okr/v1/progress_records/:progress_id").
			Params(params).
			Body(body).
			Set("progress_id", p.ProgressID).
			Desc("Update OKR progress")
	},
	Execute: func(ctx context.Context, runtime *common.RuntimeContext) error {
		p, err := parseUpdateProgressRecordParams(runtime)
		if err != nil {
			return err
		}

		body := map[string]interface{}{
			"content": p.ContentV1,
		}
		if p.ProgressRate != nil {
			body["progress_rate"] = p.ProgressRate
		}

		queryParams := map[string]interface{}{"user_id_type": p.UserIDType}

		path := fmt.Sprintf("/open-apis/okr/v1/progress_records/%s", p.ProgressID)
		data, err := runtime.CallAPITyped("PUT", path, queryParams, body)
		if err != nil {
			return err
		}

		record, err := parseProgressRecord(data)
		if err != nil {
			return err
		}

		style := runtime.Str("style")
		var result map[string]interface{}
		if style == "simple" {
			resp := record.ToSimple()
			result = map[string]interface{}{
				"progress": resp,
				"style":    style,
			}

			runtime.OutFormat(result, nil, func(w io.Writer) {
				fmt.Fprintf(w, "Updated Progress [%s] (style: %s)\n", resp.ID, style)
				fmt.Fprintf(w, "  ModifyTime: %s\n", resp.ModifyTime)
				if resp.ProgressRate != nil && resp.ProgressRate.Percent != nil {
					fmt.Fprintf(w, "  Progress: %.1f%%\n", *resp.ProgressRate.Percent)
				}
				if resp.Content != nil {
					fmt.Fprintf(w, "  Content: %s\n", resp.Content.Text)
				}
			})
		} else {
			resp := record.ToResp()
			result = map[string]interface{}{
				"progress": resp,
				"style":    style,
			}

			runtime.OutFormat(result, nil, func(w io.Writer) {
				fmt.Fprintf(w, "Updated Progress [%s] (style: %s)\n", resp.ID, style)
				fmt.Fprintf(w, "  ModifyTime: %s\n", resp.ModifyTime)
				if resp.ProgressRate != nil && resp.ProgressRate.Percent != nil {
					fmt.Fprintf(w, "  Progress: %.1f%%\n", *resp.ProgressRate.Percent)
				}
				if resp.Content != nil {
					fmt.Fprintf(w, "  Content: %s\n", *resp.Content)
				}
			})
		}
		return nil
	},
}

OKRUpdateProgressRecord updates a progress.

View Source
var OKRUploadImage = common.Shortcut{
	Service:     "okr",
	Command:     "+upload-image",
	Description: "Upload an image for use in OKR progress rich text",
	Risk:        "write",
	Scopes:      []string{"okr:okr.progress.file:upload"},
	AuthTypes:   []string{"user", "bot"},
	Flags: []common.Flag{
		{Name: "file", Desc: "local image path (supports JPG, JPEG, PNG, GIF, BMP)", Required: true},
		{Name: "target-id", Desc: "target ID (objective or key result ID) for the progress", Required: true},
		{Name: "target-type", Desc: "target type: objective | key_result", Required: true, Enum: []string{"objective", "key_result"}},
	},
	Validate: func(ctx context.Context, runtime *common.RuntimeContext) error {
		filePath := runtime.Str("file")
		if filePath == "" {
			return errs.NewValidationError(errs.SubtypeInvalidArgument, "--file is required").WithParam("--file")
		}
		ext := strings.ToLower(filepath.Ext(filePath))
		if !allowedImageExts[ext] {
			return errs.NewValidationError(errs.SubtypeInvalidArgument, "--file must be an image (supported: JPG, JPEG, PNG, GIF, BMP), got %q", ext).WithParam("--file")
		}

		targetID := runtime.Str("target-id")
		if targetID == "" {
			return errs.NewValidationError(errs.SubtypeInvalidArgument, "--target-id is required").WithParam("--target-id")
		}
		if id, err := strconv.ParseInt(targetID, 10, 64); err != nil || id <= 0 {
			return errs.NewValidationError(errs.SubtypeInvalidArgument, "--target-id must be a positive int64").WithParam("--target-id")
		}

		targetType := runtime.Str("target-type")
		if _, ok := targetTypeAllowed[targetType]; !ok {
			return errs.NewValidationError(errs.SubtypeInvalidArgument, "--target-type must be one of: objective | key_result").WithParam("--target-type")
		}
		return nil
	},
	DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI {
		filePath := runtime.Str("file")
		targetID := runtime.Str("target-id")
		targetType := runtime.Str("target-type")
		targetTypeVal := targetTypeAllowed[targetType]

		return common.NewDryRunAPI().
			POST("/open-apis/okr/v1/images/upload").
			Body(map[string]interface{}{
				"file":        "@" + filePath,
				"target_id":   targetID,
				"target_type": targetTypeVal,
			}).
			Desc(fmt.Sprintf("Upload image for OKR %s %s", targetType, targetID))
	},
	Execute: func(ctx context.Context, runtime *common.RuntimeContext) error {
		filePath := runtime.Str("file")
		targetID := runtime.Str("target-id")
		targetType := runtime.Str("target-type")
		targetTypeVal := targetTypeAllowed[targetType]

		info, err := runtime.FileIO().Stat(filePath)
		if err != nil {
			return okrInputStatError(err)
		}

		f, err := runtime.FileIO().Open(filePath)
		if err != nil {
			return okrInputStatError(err)
		}
		defer f.Close()

		fileName := filepath.Base(filePath)
		fmt.Fprintf(runtime.IO().ErrOut, "Uploading: %s (%s)\n", fileName, common.FormatSize(info.Size()))

		fd := larkcore.NewFormdata()
		fd.AddField("target_id", targetID)
		fd.AddField("target_type", fmt.Sprintf("%d", targetTypeVal))
		fd.AddFile("data", f)

		apiResp, err := runtime.DoAPI(&larkcore.ApiReq{
			HttpMethod: "POST",
			ApiPath:    "/open-apis/okr/v1/images/upload",
			Body:       fd,
		}, larkcore.WithFileUpload())
		if err != nil {

			return wrapOkrNetworkErr(err, "upload failed: %v", err)
		}

		data, err := runtime.ClassifyAPIResponse(apiResp)
		if err != nil {
			return err
		}

		fileToken := common.GetString(data, "file_token")
		if fileToken == "" {
			return errs.NewInternalError(errs.SubtypeInvalidResponse, "upload failed: no file_token returned")
		}
		url := common.GetString(data, "url")

		runtime.Out(map[string]interface{}{
			"file_token": fileToken,
			"url":        url,
			"file_name":  fileName,
			"size":       info.Size(),
		}, nil)
		return nil
	},
}

OKRUploadImage uploads an image for use in OKR progress rich text.

View Source
var OKRWeight = common.Shortcut{
	Service:     "okr",
	Command:     "+weight",
	Description: "Adjust the weight of OKR objectives or key results",
	Risk:        "write",
	Scopes:      []string{"okr:okr.content:writeonly"},
	AuthTypes:   []string{"user", "bot"},
	HasFormat:   true,
	Flags: []common.Flag{
		{Name: "level", Desc: "level to adjust: objective | key-result", Enum: []string{"objective", "key-result"}, Required: true},
		{Name: "cycle-id", Desc: "OKR cycle ID (int64)", Required: true},
		{Name: "objective-id", Desc: "objective ID (required when --level=key-result)"},
		{Name: "weights", Desc: "JSON array of weight assignments: [{\"id\":\"...\",\"weight\":0.5}]", Input: []string{common.File, common.Stdin}, Required: true},
	},
	Validate: func(ctx context.Context, runtime *common.RuntimeContext) error {
		level := runtime.Str("level")
		if level != "objective" && level != "key-result" {
			return errs.NewValidationError(errs.SubtypeInvalidArgument, "--level must be one of: objective | key-result").WithParam("--level")
		}

		cycleID := runtime.Str("cycle-id")
		if id, err := strconv.ParseInt(cycleID, 10, 64); err != nil || id <= 0 {
			return errs.NewValidationError(errs.SubtypeInvalidArgument, "--cycle-id must be a positive int64").WithParam("--cycle-id")
		}

		if level == "key-result" {
			objID := runtime.Str("objective-id")
			if objID == "" {
				return errs.NewValidationError(errs.SubtypeInvalidArgument, "--objective-id is required when --level=key-result").WithParam("--objective-id")
			}
			if id, err := strconv.ParseInt(objID, 10, 64); err != nil || id <= 0 {
				return errs.NewValidationError(errs.SubtypeInvalidArgument, "--objective-id must be a positive int64").WithParam("--objective-id")
			}
		}

		weightsStr := runtime.Str("weights")
		if err := common.RejectDangerousCharsTyped("--weights", weightsStr); err != nil {
			return err
		}
		if _, err := parseWeightOps(weightsStr); err != nil {
			return err
		}

		return nil
	},
	DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI {
		level := runtime.Str("level")
		cycleID := runtime.Str("cycle-id")
		objectiveID := runtime.Str("objective-id")
		ops, _ := parseWeightOps(runtime.Str("weights"))

		apis := common.NewDryRunAPI()

		if level == "objective" {

			listParams := map[string]interface{}{
				"page_size": 100,
			}
			listPath := fmt.Sprintf("/open-apis/okr/v2/cycles/%s/objectives", cycleID)
			apis = apis.
				GET(listPath).
				Params(listParams).
				Desc("Fetch all objectives in the cycle to get current weights for normalization")

			weightParams := map[string]interface{}{
				"cycle_id": cycleID,
			}

			objectiveWeights := make([]map[string]interface{}, 0, len(ops))
			for _, op := range ops {
				objectiveWeights = append(objectiveWeights, map[string]interface{}{
					"objective_id": op.ID,
					"weight":       op.Weight,
				})
			}
			weightBody := map[string]interface{}{
				"objective_weights": objectiveWeights,
			}
			weightPath := fmt.Sprintf("/open-apis/okr/v2/cycles/%s/objectives_weight", cycleID)
			apis = apis.
				PUT(weightPath).
				Params(weightParams).
				Body(weightBody).
				Desc("Update objective weights (full list sent after normalization)")
		} else {

			listParams := map[string]interface{}{
				"page_size": 100,
			}
			listPath := fmt.Sprintf("/open-apis/okr/v2/objectives/%s/key_results", objectiveID)
			apis = apis.
				GET(listPath).
				Params(listParams).
				Desc("Fetch all key results for the objective to get current weights for normalization")

			weightParams := map[string]interface{}{
				"objective_id": objectiveID,
			}

			keyResultWeights := make([]map[string]interface{}, 0, len(ops))
			for _, op := range ops {
				keyResultWeights = append(keyResultWeights, map[string]interface{}{
					"key_result_id": op.ID,
					"weight":        op.Weight,
				})
			}
			weightBody := map[string]interface{}{
				"key_result_weights": keyResultWeights,
			}
			weightPath := fmt.Sprintf("/open-apis/okr/v2/objectives/%s/key_results_weight", objectiveID)
			apis = apis.
				PUT(weightPath).
				Params(weightParams).
				Body(weightBody).
				Desc("Update key result weights (full list sent after normalization)")
		}

		return apis
	},
	Execute: func(ctx context.Context, runtime *common.RuntimeContext) error {
		level := runtime.Str("level")
		cycleID := runtime.Str("cycle-id")
		objectiveID := runtime.Str("objective-id")
		ops, err := parseWeightOps(runtime.Str("weights"))
		if err != nil {
			return err
		}

		var normalizedWeights []map[string]interface{}
		var total int

		if level == "objective" {
			objectives, err := fetchObjectives(ctx, runtime, cycleID)
			if err != nil {
				return err
			}
			total = len(objectives)

			objIDs := make(map[string]bool)
			for _, obj := range objectives {
				objIDs[obj.ID] = true
			}
			for _, op := range ops {
				if !objIDs[op.ID] {
					return errs.NewValidationError(errs.SubtypeInvalidArgument, "objective id %q not found in cycle", op.ID).WithParam("--weights")
				}
			}

			normalizedWeights, err = normalizeWeights(objectives, ops)
			if err != nil {
				return err
			}

			posMap := make(map[string]int32)
			for _, obj := range objectives {
				if obj.Position != nil {
					posMap[obj.ID] = *obj.Position
				}
			}

			params := map[string]interface{}{
				"cycle_id": cycleID,
			}
			objectiveWeights := make([]map[string]interface{}, 0, len(normalizedWeights))
			for _, w := range normalizedWeights {
				objectiveWeights = append(objectiveWeights, map[string]interface{}{
					"objective_id": w["id"],
					"weight":       w["weight"],
				})
			}

			sort.Slice(objectiveWeights, func(i, j int) bool {
				idI := objectiveWeights[i]["objective_id"].(string)
				idJ := objectiveWeights[j]["objective_id"].(string)
				return posMap[idI] < posMap[idJ]
			})
			body := map[string]interface{}{
				"objective_weights": objectiveWeights,
			}
			path := fmt.Sprintf("/open-apis/okr/v2/cycles/%s/objectives_weight", cycleID)
			_, err = runtime.CallAPITyped("PUT", path, params, body)
			if err != nil {
				return wrapOkrNetworkErr(err, "failed to update objective weights")
			}
		} else {

			keyResults, err := fetchKeyResults(ctx, runtime, objectiveID)
			if err != nil {
				return err
			}
			total = len(keyResults)

			krIDs := make(map[string]bool)
			for _, kr := range keyResults {
				krIDs[kr.ID] = true
			}
			for _, op := range ops {
				if !krIDs[op.ID] {
					return errs.NewValidationError(errs.SubtypeInvalidArgument, "key_result id %q not found in objective", op.ID).WithParam("--weights")
				}
			}

			normalizedWeights, err = normalizeWeights(keyResults, ops)
			if err != nil {
				return err
			}

			posMap := make(map[string]int32)
			for _, kr := range keyResults {
				if kr.Position != nil {
					posMap[kr.ID] = *kr.Position
				}
			}

			params := map[string]interface{}{
				"objective_id": objectiveID,
			}
			keyResultWeights := make([]map[string]interface{}, 0, len(normalizedWeights))
			for _, w := range normalizedWeights {
				keyResultWeights = append(keyResultWeights, map[string]interface{}{
					"key_result_id": w["id"],
					"weight":        w["weight"],
				})
			}

			sort.Slice(keyResultWeights, func(i, j int) bool {
				idI := keyResultWeights[i]["key_result_id"].(string)
				idJ := keyResultWeights[j]["key_result_id"].(string)
				return posMap[idI] < posMap[idJ]
			})
			body := map[string]interface{}{
				"key_result_weights": keyResultWeights,
			}
			path := fmt.Sprintf("/open-apis/okr/v2/objectives/%s/key_results_weight", objectiveID)
			_, err = runtime.CallAPITyped("PUT", path, params, body)
			if err != nil {
				return wrapOkrNetworkErr(err, "failed to update key result weights")
			}
		}

		result := map[string]interface{}{
			"level":    level,
			"cycle_id": cycleID,
			"total":    total,
			"weights":  normalizedWeights,
		}

		runtime.OutFormat(result, nil, func(w io.Writer) {
			fmt.Fprintf(w, "Successfully updated weights for %d %s(s)\n", total, level)
			fmt.Fprintln(w, "Weights:")
			for _, weightEntry := range normalizedWeights {
				fmt.Fprintf(w, "  %s: %v\n", weightEntry["id"], weightEntry["weight"])
			}
		})

		return nil
	},
}

OKRWeight adjusts the weight of objectives or key results.

Functions

func Shortcuts

func Shortcuts() []common.Shortcut

Shortcuts returns all okr shortcuts.

Types

type BlockElementType

type BlockElementType string

BlockElementType 块元素类型

const (
	BlockElementTypeGallery   BlockElementType = "gallery"
	BlockElementTypeParagraph BlockElementType = "paragraph"
)

func (BlockElementType) Ptr

type CategoryName

type CategoryName struct {
	Zh *string `json:"zh,omitempty"`
	En *string `json:"en,omitempty"`
	Ja *string `json:"ja,omitempty"`
}

CategoryName 分类名称

type ContentBlock

type ContentBlock struct {
	Blocks []ContentBlockElement `json:"blocks,omitempty"`
}

ContentBlock 内容块

func BuildContentBlock added in v1.0.64

func BuildContentBlock(text string, mentions []string) *ContentBlock

BuildContentBlock converts text and mentions to a ContentBlock. This is a convenience wrapper around SemiPlainContent.ToContentBlock().

func (*ContentBlock) ToSemiPlain added in v1.0.64

func (c *ContentBlock) ToSemiPlain() *SemiPlainContent

ToSemiPlain converts ContentBlock to SemiPlainContent (lossy conversion). Position information and formatting are discarded; only text, mentions, docs, and images are extracted.

func (*ContentBlock) ToV1 added in v1.0.21

func (c *ContentBlock) ToV1() *ContentBlockV1

ToV1 将 ContentBlock 转换为 ContentBlockV1

type ContentBlockElement

type ContentBlockElement struct {
	BlockElementType *BlockElementType `json:"block_element_type,omitempty"`
	Paragraph        *ContentParagraph `json:"paragraph,omitempty"`
	Gallery          *ContentGallery   `json:"gallery,omitempty"`
}

ContentBlockElement 内容块元素

func (*ContentBlockElement) ToV1 added in v1.0.21

ToV1 将 ContentBlockElement 转换为 ContentBlockElementV1

type ContentBlockElementV1 added in v1.0.21

type ContentBlockElementV1 struct {
	Type      *BlockElementType   `json:"type,omitempty"`
	Paragraph *ContentParagraphV1 `json:"paragraph,omitempty"`
	Gallery   *ContentGalleryV1   `json:"gallery,omitempty"`
}

ContentBlockElementV1 内容块元素

func (*ContentBlockElementV1) ToV2 added in v1.0.21

ToV2 将 ContentBlockElementV1 转换为 ContentBlockElement

type ContentBlockV1 added in v1.0.21

type ContentBlockV1 struct {
	Blocks []ContentBlockElementV1 `json:"blocks,omitempty"`
}

ContentBlockV1 是 OKR v1 API 使用的内容块

func (*ContentBlockV1) ToV2 added in v1.0.21

func (c *ContentBlockV1) ToV2() *ContentBlock

ToV2 将 ContentBlockV1 转换为 ContentBlock

type ContentColor

type ContentColor struct {
	Red   *int32   `json:"red,omitempty"`
	Green *int32   `json:"green,omitempty"`
	Blue  *int32   `json:"blue,omitempty"`
	Alpha *float64 `json:"alpha,omitempty"`
}

ContentColor 颜色

type ContentDocsLink struct {
	URL   *string `json:"url,omitempty"`
	Title *string `json:"title,omitempty"`
}

ContentDocsLink 文档链接

type ContentGallery

type ContentGallery struct {
	Images []ContentImageItem `json:"images,omitempty"`
}

ContentGallery 图库

func (*ContentGallery) ToV1 added in v1.0.21

func (g *ContentGallery) ToV1() *ContentGalleryV1

ToV1 将 ContentGallery 转换为 ContentGalleryV1

type ContentGalleryV1 added in v1.0.21

type ContentGalleryV1 struct {
	ImageList []ContentImageItemV1 `json:"imageList,omitempty"`
}

ContentGalleryV1 图库

func (*ContentGalleryV1) ToV2 added in v1.0.21

func (g *ContentGalleryV1) ToV2() *ContentGallery

ToV2 将 ContentGalleryV1 转换为 ContentGallery

type ContentImageItem

type ContentImageItem struct {
	FileToken *string  `json:"file_token,omitempty"`
	Src       *string  `json:"src,omitempty"`
	Width     *float64 `json:"width,omitempty"`
	Height    *float64 `json:"height,omitempty"`
}

ContentImageItem 图片项

func (*ContentImageItem) ToV1 added in v1.0.21

ToV1 将 ContentImageItem 转换为 ContentImageItemV1

type ContentImageItemV1 added in v1.0.21

type ContentImageItemV1 struct {
	FileToken *string  `json:"fileToken,omitempty"`
	Src       *string  `json:"src,omitempty"`
	Width     *float64 `json:"width,omitempty"`
	Height    *float64 `json:"height,omitempty"`
}

ContentImageItemV1 图片项

func (*ContentImageItemV1) ToV2 added in v1.0.21

ToV2 将 ContentImageItemV1 转换为 ContentImageItem

type ContentLink struct {
	URL *string `json:"url,omitempty"`
}

ContentLink 链接

type ContentList

type ContentList struct {
	ListType    *ListType `json:"list_type,omitempty"`
	IndentLevel *int32    `json:"indent_level,omitempty"`
	Number      *int32    `json:"number,omitempty"`
}

ContentList 列表

func (*ContentList) ToV1 added in v1.0.21

func (l *ContentList) ToV1() *ContentListV1

ToV1 将 ContentList 转换为 ContentListV1

type ContentListV1 added in v1.0.21

type ContentListV1 struct {
	Type        *ListType `json:"type,omitempty"`
	IndentLevel *int32    `json:"indentLevel,omitempty"`
	Number      *int32    `json:"number,omitempty"`
}

ContentListV1 列表

func (*ContentListV1) ToV2 added in v1.0.21

func (l *ContentListV1) ToV2() *ContentList

ToV2 将 ContentListV1 转换为 ContentList

type ContentMention

type ContentMention struct {
	UserID *string `json:"user_id,omitempty"`
}

ContentMention 提及

func (*ContentMention) ToV1 added in v1.0.21

func (m *ContentMention) ToV1() *ContentPersonV1

ToV1 将 ContentMention 转换为 ContentPersonV1

type ContentParagraph

type ContentParagraph struct {
	Style    *ContentParagraphStyle    `json:"style,omitempty"`
	Elements []ContentParagraphElement `json:"elements,omitempty"`
}

ContentParagraph 段落

func (*ContentParagraph) ToV1 added in v1.0.21

ToV1 将 ContentParagraph 转换为 ContentParagraphV1

type ContentParagraphElement

type ContentParagraphElement struct {
	ParagraphElementType *ParagraphElementType `json:"paragraph_element_type,omitempty"`
	TextRun              *ContentTextRun       `json:"text_run,omitempty"`
	DocsLink             *ContentDocsLink      `json:"docs_link,omitempty"`
	Mention              *ContentMention       `json:"mention,omitempty"`
}

ContentParagraphElement 段落元素

func (*ContentParagraphElement) ToV1 added in v1.0.21

ToV1 将 ContentParagraphElement 转换为 ContentParagraphElementV1

type ContentParagraphElementV1 added in v1.0.21

type ContentParagraphElementV1 struct {
	Type     *ParagraphElementTypeV1 `json:"type,omitempty"`
	TextRun  *ContentTextRunV1       `json:"textRun,omitempty"`
	DocsLink *ContentDocsLink        `json:"docsLink,omitempty"`
	Person   *ContentPersonV1        `json:"person,omitempty"`
}

ContentParagraphElementV1 段落元素

func (*ContentParagraphElementV1) ToV2 added in v1.0.21

ToV2 将 ContentParagraphElementV1 转换为 ContentParagraphElement

type ContentParagraphStyle

type ContentParagraphStyle struct {
	List *ContentList `json:"list,omitempty"`
}

ContentParagraphStyle 段落样式

func (*ContentParagraphStyle) ToV1 added in v1.0.21

ToV1 将 ContentParagraphStyle 转换为 ContentParagraphStyleV1

type ContentParagraphStyleV1 added in v1.0.21

type ContentParagraphStyleV1 struct {
	List *ContentListV1 `json:"list,omitempty"`
}

ContentParagraphStyleV1 段落样式

func (*ContentParagraphStyleV1) ToV2 added in v1.0.21

ToV2 将 ContentParagraphStyleV1 转换为 ContentParagraphStyle

type ContentParagraphV1 added in v1.0.21

type ContentParagraphV1 struct {
	Style    *ContentParagraphStyleV1    `json:"style,omitempty"`
	Elements []ContentParagraphElementV1 `json:"elements,omitempty"`
}

ContentParagraphV1 段落

func (*ContentParagraphV1) ToV2 added in v1.0.21

ToV2 将 ContentParagraphV1 转换为 ContentParagraph

type ContentPersonV1 added in v1.0.21

type ContentPersonV1 struct {
	OpenID *string `json:"openId,omitempty"`
}

ContentPersonV1 提及的人

func (*ContentPersonV1) ToV2 added in v1.0.21

func (p *ContentPersonV1) ToV2() *ContentMention

ToV2 将 ContentPersonV1 转换为 ContentMention

type ContentTextRun

type ContentTextRun struct {
	Text  *string           `json:"text,omitempty"`
	Style *ContentTextStyle `json:"style,omitempty"`
}

ContentTextRun 文本块

func (*ContentTextRun) ToV1 added in v1.0.21

func (t *ContentTextRun) ToV1() *ContentTextRunV1

ToV1 将 ContentTextRun 转换为 ContentTextRunV1

type ContentTextRunV1 added in v1.0.21

type ContentTextRunV1 struct {
	Text  *string             `json:"text,omitempty"`
	Style *ContentTextStyleV1 `json:"style,omitempty"`
}

ContentTextRunV1 文本块

func (*ContentTextRunV1) ToV2 added in v1.0.21

func (t *ContentTextRunV1) ToV2() *ContentTextRun

ToV2 将 ContentTextRunV1 转换为 ContentTextRun

type ContentTextStyle

type ContentTextStyle struct {
	Bold          *bool         `json:"bold,omitempty"`
	StrikeThrough *bool         `json:"strike_through,omitempty"`
	BackColor     *ContentColor `json:"back_color,omitempty"`
	TextColor     *ContentColor `json:"text_color,omitempty"`
	Link          *ContentLink  `json:"link,omitempty"`
}

ContentTextStyle 文本样式

func (*ContentTextStyle) ToV1 added in v1.0.21

ToV1 将 ContentTextStyle 转换为 ContentTextStyleV1

type ContentTextStyleV1 added in v1.0.21

type ContentTextStyleV1 struct {
	Bold          *bool         `json:"bold,omitempty"`
	StrikeThrough *bool         `json:"strikeThrough,omitempty"`
	BackColor     *ContentColor `json:"backColor,omitempty"`
	TextColor     *ContentColor `json:"textColor,omitempty"`
	Link          *ContentLink  `json:"link,omitempty"`
}

ContentTextStyleV1 文本样式

func (*ContentTextStyleV1) ToV2 added in v1.0.21

ToV2 将 ContentTextStyleV1 转换为 ContentTextStyle

type Cycle

type Cycle struct {
	ID            string       `json:"id"`
	CreateTime    string       `json:"create_time"`
	UpdateTime    string       `json:"update_time"`
	TenantCycleID string       `json:"tenant_cycle_id"`
	Owner         Owner        `json:"owner"`
	StartTime     string       `json:"start_time"`
	EndTime       string       `json:"end_time"`
	CycleStatus   *CycleStatus `json:"cycle_status,omitempty"`
	Score         *float64     `json:"score,omitempty"`
}

Cycle 周期

func (*Cycle) ToResp

func (c *Cycle) ToResp() *RespCycle

ToResp converts Cycle to RespCycle

type CycleStatus

type CycleStatus int32

CycleStatus 周期状态

const (
	CycleStatusDefault CycleStatus = 0
	CycleStatusNormal  CycleStatus = 1
	CycleStatusInvalid CycleStatus = 2
	CycleStatusHidden  CycleStatus = 3
)

func (CycleStatus) Ptr

func (t CycleStatus) Ptr() *CycleStatus

func (CycleStatus) ToString

func (t CycleStatus) ToString() string

ToString CycleStatus to string

type KeyResult

type KeyResult struct {
	ID          string        `json:"id"`
	CreateTime  string        `json:"create_time"`
	UpdateTime  string        `json:"update_time"`
	Owner       Owner         `json:"owner"`
	ObjectiveID string        `json:"objective_id"`
	Position    *int32        `json:"position,omitempty"`
	Content     *ContentBlock `json:"content,omitempty"`
	Score       *float64      `json:"score,omitempty"`
	Weight      *float64      `json:"weight,omitempty"`
	Deadline    *string       `json:"deadline,omitempty"`
}

KeyResult 关键结果

func (KeyResult) GetID added in v1.0.56

func (k KeyResult) GetID() string

GetID implements the interface for KeyResult.

func (KeyResult) GetWeight added in v1.0.56

func (k KeyResult) GetWeight() float64

GetWeight implements the interface for KeyResult.

func (*KeyResult) ToResp

func (k *KeyResult) ToResp() *RespKeyResult

ToResp converts KeyResult to RespKeyResult

func (*KeyResult) ToSimple added in v1.0.64

func (k *KeyResult) ToSimple() *RespKeyResultSimple

ToSimple converts KeyResult to RespKeyResultSimple.

type ListType

type ListType string

ListType 列表类型

const (
	ListTypeBullet     ListType = "bullet"
	ListTypeCheckBox   ListType = "checkBox"
	ListTypeCheckedBox ListType = "checkedBox"
	ListTypeIndent     ListType = "indent"
	ListTypeNumber     ListType = "number"
)

type Objective

type Objective struct {
	ID         string        `json:"id"`
	CreateTime string        `json:"create_time"`
	UpdateTime string        `json:"update_time"`
	Owner      Owner         `json:"owner"`
	CycleID    string        `json:"cycle_id"`
	Position   *int32        `json:"position,omitempty"`
	Content    *ContentBlock `json:"content,omitempty"`
	Score      *float64      `json:"score,omitempty"`
	Notes      *ContentBlock `json:"notes,omitempty"`
	Weight     *float64      `json:"weight,omitempty"`
	Deadline   *string       `json:"deadline,omitempty"`
	CategoryID *string       `json:"category_id,omitempty"`
}

Objective 目标

func (Objective) GetID added in v1.0.56

func (o Objective) GetID() string

GetID implements the interface for Objective.

func (Objective) GetWeight added in v1.0.56

func (o Objective) GetWeight() float64

GetWeight implements the interface for Objective.

func (*Objective) ToResp

func (o *Objective) ToResp() *RespObjective

ToResp converts Objective to RespObjective

func (*Objective) ToSimple added in v1.0.64

func (o *Objective) ToSimple() *RespObjectiveSimple

ToSimple converts Objective to RespObjectiveSimple.

type Owner

type Owner struct {
	OwnerType OwnerType `json:"owner_type"`
	UserID    *string   `json:"user_id,omitempty"`
}

Owner OKR 所有者

func (*Owner) ToResp

func (o *Owner) ToResp() *RespOwner

ToResp converts Owner to RespOwner

type OwnerType

type OwnerType string

OwnerType 所有者类型

const (
	OwnerTypeDepartment OwnerType = "department"
	OwnerTypeUser       OwnerType = "user"
)

type ParagraphElementType

type ParagraphElementType string

ParagraphElementType 段落元素类型

const (
	ParagraphElementTypeDocsLink ParagraphElementType = "docsLink"
	ParagraphElementTypeMention  ParagraphElementType = "mention"
	ParagraphElementTypeTextRun  ParagraphElementType = "textRun"
)

func (ParagraphElementType) Ptr

func (ParagraphElementType) ToV1 added in v1.0.21

ToV1 将 ParagraphElementType 转换为 ParagraphElementTypeV1

type ParagraphElementTypeV1 added in v1.0.21

type ParagraphElementTypeV1 string
const (
	ParagraphElementTypeV1DocsLink ParagraphElementTypeV1 = "docsLink"
	ParagraphElementTypeV1Mention  ParagraphElementTypeV1 = "person"
	ParagraphElementTypeV1TextRun  ParagraphElementTypeV1 = "textRun"
)

func (ParagraphElementTypeV1) Ptr added in v1.0.21

func (ParagraphElementTypeV1) ToV2 added in v1.0.21

ToV2 将 ParagraphElementTypeV1 转换为 ParagraphElementType

type Progress added in v1.0.21

type Progress struct {
	ID           string        `json:"id"`
	CreateTime   string        `json:"create_time"`
	UpdateTime   string        `json:"update_time"`
	Owner        Owner         `json:"owner"`
	EntityType   *int32        `json:"entity_type,omitempty"`
	EntityID     string        `json:"entity_id"`
	Content      *ContentBlock `json:"content,omitempty"`
	ProgressRate *ProgressRate `json:"progress_rate,omitempty"`
}

Progress 进展记录(v2 API)

func (*Progress) ToResp added in v1.0.21

func (p *Progress) ToResp() *RespProgress

ToResp converts Progress to RespProgress

func (*Progress) ToSimple added in v1.0.64

func (p *Progress) ToSimple() *RespProgressSimple

ToSimple converts Progress to RespProgressSimple.

type ProgressRate added in v1.0.21

type ProgressRate struct {
	ProgressPercent *float64 `json:"progress_percent,omitempty"`
	ProgressStatus  *int32   `json:"progress_status,omitempty"`
}

ProgressRate 进度率(v2 API)

type ProgressRateV1 added in v1.0.21

type ProgressRateV1 struct {
	Percent *float64 `json:"percent,omitempty"`
	Status  *int32   `json:"status,omitempty"`
}

ProgressRateV1 进度率

type ProgressStatus added in v1.0.21

type ProgressStatus int32

ProgressStatus 进展状态

const (
	ProgressStatusNormal  ProgressStatus = 0 // 正常
	ProgressStatusOverdue ProgressStatus = 1 // 逾期
	ProgressStatusDone    ProgressStatus = 2 // 已完成
)

func ParseProgressStatus added in v1.0.21

func ParseProgressStatus(s string) (ProgressStatus, bool)

ParseProgressStatus parses a progress status string into ProgressStatus. Accepts "normal", "overdue", "done" or their numeric values "0", "1", "2".

func (ProgressStatus) String added in v1.0.21

func (s ProgressStatus) String() string

String returns a human-readable name for ProgressStatus.

type ProgressV1 added in v1.0.21

type ProgressV1 struct {
	ID           string          `json:"progress_id"`
	ModifyTime   string          `json:"modify_time"`
	Content      *ContentBlockV1 `json:"content,omitempty"`
	ProgressRate *ProgressRateV1 `json:"progress_rate,omitempty"`
}

ProgressV1 进展记录

func (*ProgressV1) ToResp added in v1.0.21

func (p *ProgressV1) ToResp() *RespProgress

ToResp converts ProgressV1 to RespProgress

func (*ProgressV1) ToSimple added in v1.0.64

func (p *ProgressV1) ToSimple() *RespProgressSimple

ToSimple converts ProgressV1 to RespProgressSimple.

type 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"`
}

RespAlignment 对齐关系

type 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"`
}

RespCategory 分类

type RespCycle

type RespCycle struct {
	ID          string  `json:"id"`
	StartTime   string  `json:"start_time"`
	EndTime     string  `json:"end_time"`
	CycleStatus *string `json:"cycle_status,omitempty"`
}

RespCycle 周期

type 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"`
}

RespIndicator 指标

type RespIndicatorUnit

type RespIndicatorUnit struct {
	UnitType  *string `json:"unit_type,omitempty"`
	UnitValue *string `json:"unit_value,omitempty"`
}

RespIndicatorUnit 指标单位

type 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"`
}

RespKeyResult 关键结果

type RespKeyResultSimple added in v1.0.64

type RespKeyResultSimple 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     *SemiPlainContent `json:"content,omitempty"`
	Score       *float64          `json:"score,omitempty"`
	Weight      *float64          `json:"weight,omitempty"`
	Deadline    *string           `json:"deadline,omitempty"`
}

RespKeyResultSimple is KeyResult response with SemiPlainContent instead of ContentBlock JSON string.

type 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"`
}

RespObjective 目标

type RespObjectiveSimple added in v1.0.64

type RespObjectiveSimple 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    *SemiPlainContent     `json:"content,omitempty"`
	Score      *float64              `json:"score,omitempty"`
	Notes      *SemiPlainContent     `json:"notes,omitempty"`
	Weight     *float64              `json:"weight,omitempty"`
	Deadline   *string               `json:"deadline,omitempty"`
	CategoryID *string               `json:"category_id,omitempty"`
	KeyResults []RespKeyResultSimple `json:"key_results,omitempty"`
}

RespObjectiveSimple is Objective response with SemiPlainContent instead of ContentBlock JSON string.

type RespOwner

type RespOwner struct {
	OwnerType string  `json:"owner_type"`
	UserID    *string `json:"user_id,omitempty"`
}

RespOwner OKR 所有者

type RespProgress added in v1.0.21

type RespProgress struct {
	ID           string            `json:"progress_id"`
	ModifyTime   string            `json:"modify_time"`
	CreateTime   *string           `json:"create_time,omitempty"`
	Content      *string           `json:"content,omitempty"`
	ProgressRate *RespProgressRate `json:"progress_rate,omitempty"`
}

RespProgress 进展记录

type RespProgressRate added in v1.0.21

type RespProgressRate struct {
	Percent *float64 `json:"percent,omitempty"`
	Status  *string  `json:"status,omitempty"`
}

RespProgressRate 进度率(面向用户的响应格式,Status 为可读字符串)

type RespProgressSimple added in v1.0.64

type RespProgressSimple struct {
	ID           string            `json:"progress_id"`
	ModifyTime   string            `json:"modify_time"`
	CreateTime   *string           `json:"create_time,omitempty"`
	Content      *SemiPlainContent `json:"content,omitempty"`
	ProgressRate *RespProgressRate `json:"progress_rate,omitempty"`
}

RespProgressSimple is Progress response with SemiPlainContent instead of ContentBlock JSON string.

type SemiPlainContent added in v1.0.64

type SemiPlainContent struct {
	Text    string         `json:"text"`
	Mention []string       `json:"mention,omitempty"`
	Docs    []SemiPlainDoc `json:"docs,omitempty"`
	Images  []string       `json:"images,omitempty"`
}

SemiPlainContent is a simplified, lossy representation of ContentBlock. It contains plain text, mentions, docs, and images without rich formatting or position info.

func (*SemiPlainContent) ToContentBlock added in v1.0.64

func (s *SemiPlainContent) ToContentBlock() *ContentBlock

ToContentBlock converts SemiPlainContent to ContentBlock. Text and mentions are placed in a single paragraph (text first, then mentions). Docs and images are NOT converted (input semi-plain format only supports text+mention).

type SemiPlainDoc added in v1.0.64

type SemiPlainDoc struct {
	Title string `json:"title"`
	URL   string `json:"url"`
}

SemiPlainDoc represents a document link in semi-plain content.

type StatusCalculateType

type StatusCalculateType int32

StatusCalculateType 状态计算类型

const (
	StatusCalculateTypeManualUpdate                                      StatusCalculateType = 0
	StatusCalculateTypeAutomaticallyUpdatesBasedOnProgressAndCurrentTime StatusCalculateType = 1
	StatusCalculateTypeStatusUpdatesBasedOnTheHighestRiskKeyResults      StatusCalculateType = 2
)

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL