// Copyright (c) 2026 Lark Technologies Pte. Ltd. // SPDX-License-Identifier: MIT package okr import ( "encoding/json" "regexp" "strconv" "strings" "time" ) // CycleStatus 周期状态 type CycleStatus int32 const ( CycleStatusDefault CycleStatus = 0 CycleStatusNormal CycleStatus = 1 CycleStatusInvalid CycleStatus = 2 CycleStatusHidden CycleStatus = 3 ) func (t CycleStatus) Ptr() *CycleStatus { return &t } // StatusCalculateType 状态计算类型 type StatusCalculateType int32 const ( StatusCalculateTypeManualUpdate StatusCalculateType = 0 StatusCalculateTypeAutomaticallyUpdatesBasedOnProgressAndCurrentTime StatusCalculateType = 1 StatusCalculateTypeStatusUpdatesBasedOnTheHighestRiskKeyResults StatusCalculateType = 2 ) // BlockElementType 块元素类型 type BlockElementType string const ( BlockElementTypeGallery BlockElementType = "gallery" BlockElementTypeParagraph BlockElementType = "paragraph" ) func (t BlockElementType) Ptr() *BlockElementType { return &t } // CategoryName 分类名称 type CategoryName struct { Zh *string `json:"zh,omitempty"` En *string `json:"en,omitempty"` Ja *string `json:"ja,omitempty"` } // ListType 列表类型 type ListType string const ( ListTypeBullet ListType = "bullet" ListTypeCheckBox ListType = "checkBox" ListTypeCheckedBox ListType = "checkedBox" ListTypeIndent ListType = "indent" ListTypeNumber ListType = "number" ) // OwnerType 所有者类型 type OwnerType string const ( OwnerTypeDepartment OwnerType = "department" OwnerTypeUser OwnerType = "user" ) // ParagraphElementType 段落元素类型 type ParagraphElementType string const ( ParagraphElementTypeDocsLink ParagraphElementType = "docsLink" ParagraphElementTypeMention ParagraphElementType = "mention" ParagraphElementTypeTextRun ParagraphElementType = "textRun" ) func (t ParagraphElementType) Ptr() *ParagraphElementType { return &t } type ParagraphElementTypeV1 string const ( ParagraphElementTypeV1DocsLink ParagraphElementTypeV1 = "docsLink" ParagraphElementTypeV1Mention ParagraphElementTypeV1 = "person" ParagraphElementTypeV1TextRun ParagraphElementTypeV1 = "textRun" ) func (t ParagraphElementTypeV1) Ptr() *ParagraphElementTypeV1 { return &t } // ContentBlock 内容块 type ContentBlock struct { Blocks []ContentBlockElement `json:"blocks,omitempty"` } // ContentBlockElement 内容块元素 type ContentBlockElement struct { BlockElementType *BlockElementType `json:"block_element_type,omitempty"` Paragraph *ContentParagraph `json:"paragraph,omitempty"` Gallery *ContentGallery `json:"gallery,omitempty"` } // ContentColor 颜色 type ContentColor struct { Red *int32 `json:"red,omitempty"` Green *int32 `json:"green,omitempty"` Blue *int32 `json:"blue,omitempty"` Alpha *float64 `json:"alpha,omitempty"` } // ContentDocsLink 文档链接 type ContentDocsLink struct { URL *string `json:"url,omitempty"` Title *string `json:"title,omitempty"` } // ContentGallery 图库 type ContentGallery struct { Images []ContentImageItem `json:"images,omitempty"` } // 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"` } // ContentLink 链接 type ContentLink struct { URL *string `json:"url,omitempty"` } // ContentList 列表 type ContentList struct { ListType *ListType `json:"list_type,omitempty"` IndentLevel *int32 `json:"indent_level,omitempty"` Number *int32 `json:"number,omitempty"` } // ContentMention 提及 type ContentMention struct { UserID *string `json:"user_id,omitempty"` } // ContentParagraph 段落 type ContentParagraph struct { Style *ContentParagraphStyle `json:"style,omitempty"` Elements []ContentParagraphElement `json:"elements,omitempty"` } // 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"` } // ContentParagraphStyle 段落样式 type ContentParagraphStyle struct { List *ContentList `json:"list,omitempty"` } // ContentTextRun 文本块 type ContentTextRun struct { Text *string `json:"text,omitempty"` Style *ContentTextStyle `json:"style,omitempty"` } // 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"` } // 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"` } // 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"` } // 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"` } // Owner OKR 所有者 type Owner struct { OwnerType OwnerType `json:"owner_type"` UserID *string `json:"user_id,omitempty"` } // ToString CycleStatus to string func (t CycleStatus) ToString() string { switch t { case CycleStatusDefault: return "default" case CycleStatusNormal: return "normal" case CycleStatusInvalid: return "invalid" case CycleStatusHidden: return "hidden" default: return "" } } // formatTimestamp 格式化毫秒级时间戳为 DateTime 格式 func formatTimestamp(ts string) string { if ts == "" { return "" } millis, err := strconv.ParseInt(ts, 10, 64) if err != nil { return ts } t := time.UnixMilli(millis) return t.Format("2006-01-02 15:04:05") } // ToResp converts Cycle to RespCycle func (c *Cycle) ToResp() *RespCycle { if c == nil { return nil } resp := &RespCycle{ ID: c.ID, StartTime: formatTimestamp(c.StartTime), EndTime: formatTimestamp(c.EndTime), } if c.CycleStatus != nil { s := c.CycleStatus.ToString() resp.CycleStatus = &s } return resp } // ToResp converts KeyResult to RespKeyResult func (k *KeyResult) ToResp() *RespKeyResult { if k == nil { return nil } result := &RespKeyResult{ ID: k.ID, CreateTime: formatTimestamp(k.CreateTime), UpdateTime: formatTimestamp(k.UpdateTime), Owner: *k.Owner.ToResp(), ObjectiveID: k.ObjectiveID, Position: k.Position, Score: k.Score, Weight: k.Weight, } if k.Deadline != nil { d := formatTimestamp(*k.Deadline) result.Deadline = &d } // Serialize ContentBlock to JSON string (only if Content is not nil and has blocks) if k.Content != nil && len(k.Content.Blocks) > 0 { if bytes, err := json.Marshal(k.Content); err == nil { s := string(bytes) result.Content = &s } } return result } // ToResp converts Objective to RespObjective func (o *Objective) ToResp() *RespObjective { if o == nil { return nil } result := &RespObjective{ ID: o.ID, CreateTime: formatTimestamp(o.CreateTime), UpdateTime: formatTimestamp(o.UpdateTime), Owner: *o.Owner.ToResp(), CycleID: o.CycleID, Position: o.Position, Score: o.Score, Weight: o.Weight, CategoryID: o.CategoryID, } if o.Deadline != nil { d := formatTimestamp(*o.Deadline) result.Deadline = &d } // Serialize Content to JSON string if o.Content != nil && len(o.Content.Blocks) > 0 { if bytes, err := json.Marshal(o.Content); err == nil { s := string(bytes) result.Content = &s } } // Serialize Notes to JSON string if o.Notes != nil && len(o.Notes.Blocks) > 0 { if bytes, err := json.Marshal(o.Notes); err == nil { s := string(bytes) result.Notes = &s } } return result } // ToResp converts Owner to RespOwner func (o *Owner) ToResp() *RespOwner { if o == nil { return nil } return &RespOwner{ OwnerType: string(o.OwnerType), UserID: o.UserID, } } // ptrStr dereferences a string pointer, returning "" for nil. func ptrStr(p *string) string { if p == nil { return "" } return *p } // ptrFloat64 dereferences a float64 pointer, returning 0 for nil. func ptrFloat64(p *float64) float64 { if p == nil { return 0 } return *p } // ========== ContentBlockV1 (for OKR v1 API ContentBlock) ========== // ContentBlockV1 是 OKR v1 API 使用的内容块 type ContentBlockV1 struct { Blocks []ContentBlockElementV1 `json:"blocks,omitempty"` } // ContentBlockElementV1 内容块元素 type ContentBlockElementV1 struct { Type *BlockElementType `json:"type,omitempty"` Paragraph *ContentParagraphV1 `json:"paragraph,omitempty"` Gallery *ContentGalleryV1 `json:"gallery,omitempty"` } // ContentGalleryV1 图库 type ContentGalleryV1 struct { ImageList []ContentImageItemV1 `json:"imageList,omitempty"` } // ContentImageItemV1 图片项 type ContentImageItemV1 struct { FileToken *string `json:"fileToken,omitempty"` Src *string `json:"src,omitempty"` Width *float64 `json:"width,omitempty"` Height *float64 `json:"height,omitempty"` } // ContentParagraphV1 段落 type ContentParagraphV1 struct { Style *ContentParagraphStyleV1 `json:"style,omitempty"` Elements []ContentParagraphElementV1 `json:"elements,omitempty"` } // ContentParagraphElementV1 段落元素 type ContentParagraphElementV1 struct { Type *ParagraphElementTypeV1 `json:"type,omitempty"` TextRun *ContentTextRunV1 `json:"textRun,omitempty"` DocsLink *ContentDocsLink `json:"docsLink,omitempty"` Person *ContentPersonV1 `json:"person,omitempty"` } // ContentParagraphStyleV1 段落样式 type ContentParagraphStyleV1 struct { List *ContentListV1 `json:"list,omitempty"` } // ContentListV1 列表 type ContentListV1 struct { Type *ListType `json:"type,omitempty"` IndentLevel *int32 `json:"indentLevel,omitempty"` Number *int32 `json:"number,omitempty"` } // ContentPersonV1 提及的人 type ContentPersonV1 struct { OpenID *string `json:"openId,omitempty"` } // ContentTextRunV1 文本块 type ContentTextRunV1 struct { Text *string `json:"text,omitempty"` Style *ContentTextStyleV1 `json:"style,omitempty"` } // ContentTextStyleV1 文本样式 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"` } // ToV1 将 ContentBlock 转换为 ContentBlockV1 func (c *ContentBlock) ToV1() *ContentBlockV1 { if c == nil { return nil } result := &ContentBlockV1{} for _, block := range c.Blocks { result.Blocks = append(result.Blocks, block.ToV1()) } return result } // ToV1 将 ContentBlockElement 转换为 ContentBlockElementV1 func (e *ContentBlockElement) ToV1() ContentBlockElementV1 { return ContentBlockElementV1{ Type: e.BlockElementType, Paragraph: e.Paragraph.ToV1(), Gallery: e.Gallery.ToV1(), } } // ToV1 将 ContentGallery 转换为 ContentGalleryV1 func (g *ContentGallery) ToV1() *ContentGalleryV1 { if g == nil { return nil } imageList := make([]ContentImageItemV1, 0, len(g.Images)) for _, img := range g.Images { imageList = append(imageList, img.ToV1()) } return &ContentGalleryV1{ ImageList: imageList, } } // ToV1 将 ContentImageItem 转换为 ContentImageItemV1 func (i *ContentImageItem) ToV1() ContentImageItemV1 { return ContentImageItemV1{ FileToken: i.FileToken, Src: i.Src, Width: i.Width, Height: i.Height, } } // ToV1 将 ContentParagraph 转换为 ContentParagraphV1 func (p *ContentParagraph) ToV1() *ContentParagraphV1 { if p == nil { return nil } result := &ContentParagraphV1{ Style: p.Style.ToV1(), } for _, elem := range p.Elements { result.Elements = append(result.Elements, elem.ToV1()) } return result } // ToV1 将 ParagraphElementType 转换为 ParagraphElementTypeV1 func (t ParagraphElementType) ToV1() ParagraphElementTypeV1 { switch t { case ParagraphElementTypeDocsLink: return ParagraphElementTypeV1DocsLink case ParagraphElementTypeMention: return ParagraphElementTypeV1Mention // "person" case ParagraphElementTypeTextRun: return ParagraphElementTypeV1TextRun default: return ParagraphElementTypeV1(t) } } // ToV2 将 ParagraphElementTypeV1 转换为 ParagraphElementType func (t ParagraphElementTypeV1) ToV2() ParagraphElementType { switch t { case ParagraphElementTypeV1DocsLink: return ParagraphElementTypeDocsLink case ParagraphElementTypeV1Mention: // "person" return ParagraphElementTypeMention case ParagraphElementTypeV1TextRun: return ParagraphElementTypeTextRun default: return ParagraphElementType(t) } } // ToV1 将 ContentParagraphElement 转换为 ContentParagraphElementV1 func (e *ContentParagraphElement) ToV1() ContentParagraphElementV1 { t := ParagraphElementTypeV1TextRun if e.ParagraphElementType != nil { t = e.ParagraphElementType.ToV1() } return ContentParagraphElementV1{ Type: t.Ptr(), TextRun: e.TextRun.ToV1(), DocsLink: e.DocsLink, Person: e.Mention.ToV1(), } } // ToV1 将 ContentParagraphStyle 转换为 ContentParagraphStyleV1 func (s *ContentParagraphStyle) ToV1() *ContentParagraphStyleV1 { if s == nil { return nil } return &ContentParagraphStyleV1{ List: s.List.ToV1(), } } // ToV1 将 ContentList 转换为 ContentListV1 func (l *ContentList) ToV1() *ContentListV1 { if l == nil { return nil } return &ContentListV1{ Type: l.ListType, IndentLevel: l.IndentLevel, Number: l.Number, } } // ToV1 将 ContentTextStyle 转换为 ContentTextStyleV1 func (s *ContentTextStyle) ToV1() *ContentTextStyleV1 { if s == nil { return nil } return &ContentTextStyleV1{ Bold: s.Bold, StrikeThrough: s.StrikeThrough, BackColor: s.BackColor, TextColor: s.TextColor, Link: s.Link, } } // ToV1 将 ContentTextRun 转换为 ContentTextRunV1 func (t *ContentTextRun) ToV1() *ContentTextRunV1 { if t == nil { return nil } return &ContentTextRunV1{ Text: t.Text, Style: t.Style.ToV1(), } } // ToV1 将 ContentMention 转换为 ContentPersonV1 func (m *ContentMention) ToV1() *ContentPersonV1 { if m == nil { return nil } return &ContentPersonV1{ OpenID: m.UserID, } } // ========== ContentBlockV1 转 ContentBlock ========== // ToV2 将 ContentBlockV1 转换为 ContentBlock func (c *ContentBlockV1) ToV2() *ContentBlock { if c == nil { return nil } result := &ContentBlock{} for _, block := range c.Blocks { result.Blocks = append(result.Blocks, block.ToV2()) } return result } // ToV2 将 ContentBlockElementV1 转换为 ContentBlockElement func (e *ContentBlockElementV1) ToV2() ContentBlockElement { return ContentBlockElement{ BlockElementType: e.Type, Paragraph: e.Paragraph.ToV2(), Gallery: e.Gallery.ToV2(), } } // ToV2 将 ContentGalleryV1 转换为 ContentGallery func (g *ContentGalleryV1) ToV2() *ContentGallery { if g == nil { return nil } images := make([]ContentImageItem, 0, len(g.ImageList)) for _, img := range g.ImageList { images = append(images, img.ToV2()) } return &ContentGallery{ Images: images, } } // ToV2 将 ContentImageItemV1 转换为 ContentImageItem func (i *ContentImageItemV1) ToV2() ContentImageItem { return ContentImageItem{ FileToken: i.FileToken, Src: i.Src, Width: i.Width, Height: i.Height, } } // ToV2 将 ContentParagraphV1 转换为 ContentParagraph func (p *ContentParagraphV1) ToV2() *ContentParagraph { if p == nil { return nil } result := &ContentParagraph{ Style: p.Style.ToV2(), } for _, elem := range p.Elements { result.Elements = append(result.Elements, elem.ToV2()) } return result } // ToV2 将 ContentParagraphElementV1 转换为 ContentParagraphElement func (e *ContentParagraphElementV1) ToV2() ContentParagraphElement { t := ParagraphElementTypeTextRun if e.Type != nil { t = e.Type.ToV2() } return ContentParagraphElement{ ParagraphElementType: t.Ptr(), TextRun: e.TextRun.ToV2(), DocsLink: e.DocsLink, Mention: e.Person.ToV2(), } } // ToV2 将 ContentParagraphStyleV1 转换为 ContentParagraphStyle func (s *ContentParagraphStyleV1) ToV2() *ContentParagraphStyle { if s == nil { return nil } return &ContentParagraphStyle{ List: s.List.ToV2(), } } // ToV2 将 ContentListV1 转换为 ContentList func (l *ContentListV1) ToV2() *ContentList { if l == nil { return nil } return &ContentList{ ListType: l.Type, IndentLevel: l.IndentLevel, Number: l.Number, } } // ToV2 将 ContentTextStyleV1 转换为 ContentTextStyle func (s *ContentTextStyleV1) ToV2() *ContentTextStyle { if s == nil { return nil } return &ContentTextStyle{ Bold: s.Bold, StrikeThrough: s.StrikeThrough, BackColor: s.BackColor, TextColor: s.TextColor, Link: s.Link, } } // ToV2 将 ContentTextRunV1 转换为 ContentTextRun func (t *ContentTextRunV1) ToV2() *ContentTextRun { if t == nil { return nil } return &ContentTextRun{ Text: t.Text, Style: t.Style.ToV2(), } } // ToV2 将 ContentPersonV1 转换为 ContentMention func (p *ContentPersonV1) ToV2() *ContentMention { if p == nil { return nil } return &ContentMention{ UserID: p.OpenID, } } // ========== SemiPlainContent (半纯文本格式) ========== // Regex patterns for semi-plain text processing (pre-compiled for performance). var ( placeholderRE = regexp.MustCompile(`\s*@\{[^}]+\}\s*`) multiSpaceRE = regexp.MustCompile(`\s+`) ) // SemiPlainDoc represents a document link in semi-plain content. type SemiPlainDoc struct { Title string `json:"title"` URL string `json:"url"` } // SemiPlainContent is a simplified, lossy representation of ContentBlock. // It contains plain text, mentions, docs, and images without rich formatting or position info. type SemiPlainContent struct { Text string `json:"text"` Mention []string `json:"mention,omitempty"` Docs []SemiPlainDoc `json:"docs,omitempty"` Images []string `json:"images,omitempty"` } // ToSemiPlain converts ContentBlock to SemiPlainContent (lossy conversion). // Position information and formatting are discarded; only text, mentions, docs, and images are extracted. func (c *ContentBlock) ToSemiPlain() *SemiPlainContent { if c == nil { return nil } result := &SemiPlainContent{} var textParts []string for _, block := range c.Blocks { if block.Paragraph != nil { for _, elem := range block.Paragraph.Elements { switch { case elem.TextRun != nil && elem.TextRun.Text != nil: textParts = append(textParts, *elem.TextRun.Text) case elem.Mention != nil && elem.Mention.UserID != nil: textParts = append(textParts, " @{"+*elem.Mention.UserID+"} ") result.Mention = append(result.Mention, *elem.Mention.UserID) case elem.DocsLink != nil: doc := SemiPlainDoc{} if elem.DocsLink.Title != nil { doc.Title = *elem.DocsLink.Title } if elem.DocsLink.URL != nil { doc.URL = *elem.DocsLink.URL } result.Docs = append(result.Docs, doc) } } } if block.Gallery != nil { for _, img := range block.Gallery.Images { if img.Src != nil { result.Images = append(result.Images, *img.Src) } } } } result.Text = strings.Join(textParts, "") return result } // 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). func (s *SemiPlainContent) ToContentBlock() *ContentBlock { if s == nil { return nil } elements := make([]ContentParagraphElement, 0, len(s.Mention)+1) // Strip @{userID} placeholders from text to avoid duplicate mentions // (these placeholders are only for readability in the output format) strippedText := placeholderRE.ReplaceAllString(s.Text, " ") // Collapse multiple spaces and trim strippedText = multiSpaceRE.ReplaceAllString(strippedText, " ") strippedText = strings.TrimSpace(strippedText) // Add text element if stripped text is not empty if strippedText != "" { text := strippedText elements = append(elements, ContentParagraphElement{ ParagraphElementType: ParagraphElementTypeTextRun.Ptr(), TextRun: &ContentTextRun{ Text: &text, }, }) } // Add mention elements for _, mention := range s.Mention { m := mention elements = append(elements, ContentParagraphElement{ ParagraphElementType: ParagraphElementTypeMention.Ptr(), Mention: &ContentMention{ UserID: &m, }, }) } return &ContentBlock{ Blocks: []ContentBlockElement{ { BlockElementType: BlockElementTypeParagraph.Ptr(), Paragraph: &ContentParagraph{ Elements: elements, }, }, }, } } // BuildContentBlock converts text and mentions to a ContentBlock. // This is a convenience wrapper around SemiPlainContent.ToContentBlock(). func BuildContentBlock(text string, mentions []string) *ContentBlock { return (&SemiPlainContent{ Text: text, Mention: mentions, }).ToContentBlock() } // ProgressRateV1 进度率 type ProgressRateV1 struct { Percent *float64 `json:"percent,omitempty"` Status *int32 `json:"status,omitempty"` } // ProgressV1 进展记录 type ProgressV1 struct { ID string `json:"progress_id"` ModifyTime string `json:"modify_time"` Content *ContentBlockV1 `json:"content,omitempty"` ProgressRate *ProgressRateV1 `json:"progress_rate,omitempty"` } // ToResp converts ProgressV1 to RespProgress func (p *ProgressV1) ToResp() *RespProgress { if p == nil { return nil } resp := &RespProgress{ ID: p.ID, ModifyTime: formatTimestamp(p.ModifyTime), } if p.ProgressRate != nil { resp.ProgressRate = &RespProgressRate{ Percent: p.ProgressRate.Percent, } if p.ProgressRate.Status != nil { s := ProgressStatus(*p.ProgressRate.Status).String() if s != "" { resp.ProgressRate.Status = &s } } } // Convert ContentBlockV1 to ContentBlock, then serialize to JSON string if p.Content != nil && len(p.Content.Blocks) > 0 { if v2 := p.Content.ToV2(); v2 != nil && len(v2.Blocks) > 0 { if bytes, err := json.Marshal(v2); err == nil { s := string(bytes) resp.Content = &s } } } return resp } // int32Ptr returns a pointer to the given int32 value. func int32Ptr(v int32) *int32 { return &v } // ========== Progress (for OKR v2 API ListOkrObjectiveProgress/ListOkrKeyResultProgress) ========== // ProgressRate 进度率(v2 API) type ProgressRate struct { ProgressPercent *float64 `json:"progress_percent,omitempty"` ProgressStatus *int32 `json:"progress_status,omitempty"` } // Progress 进展记录(v2 API) 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"` } // ToResp converts Progress to RespProgress func (p *Progress) ToResp() *RespProgress { if p == nil { return nil } cteateTime := formatTimestamp(p.CreateTime) resp := &RespProgress{ ID: p.ID, ModifyTime: formatTimestamp(p.UpdateTime), // Use UpdateTime as ModifyTime CreateTime: &cteateTime, } if p.ProgressRate != nil { resp.ProgressRate = &RespProgressRate{ Percent: p.ProgressRate.ProgressPercent, } if p.ProgressRate.ProgressStatus != nil { s := ProgressStatus(*p.ProgressRate.ProgressStatus).String() if s != "" { resp.ProgressRate.Status = &s } } } // Serialize ContentBlock to JSON string if p.Content != nil && len(p.Content.Blocks) > 0 { if bytes, err := json.Marshal(p.Content); err == nil { s := string(bytes) resp.Content = &s } } return resp }