2026-08-14 07:52:01 +08:00
|
|
|
|
package main
|
|
|
|
|
|
|
2026-08-15 17:18:00 +08:00
|
|
|
|
// team.go 团队协作:团队/成员/角色、团队任务、日报与 AI 摘要、团队通知。
|
|
|
|
|
|
// 团队数据经 nl-pms-api(JWT);通知在同步循环里增量拉取后转本地消息。
|
2026-08-14 07:52:01 +08:00
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
|
"context"
|
|
|
|
|
|
"errors"
|
|
|
|
|
|
"fmt"
|
2026-08-15 17:18:00 +08:00
|
|
|
|
"net/http"
|
|
|
|
|
|
"net/url"
|
2026-08-14 07:52:01 +08:00
|
|
|
|
"regexp"
|
|
|
|
|
|
"strings"
|
|
|
|
|
|
"time"
|
|
|
|
|
|
|
|
|
|
|
|
"view/service"
|
|
|
|
|
|
"view/service/ai"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
// ---------- 结构 ----------
|
|
|
|
|
|
|
|
|
|
|
|
type TeamInfo struct {
|
|
|
|
|
|
ID int64 `json:"id"`
|
|
|
|
|
|
Name string `json:"name"`
|
|
|
|
|
|
OwnerID int64 `json:"ownerId"`
|
|
|
|
|
|
DigestTime string `json:"digestTime"`
|
|
|
|
|
|
Role string `json:"role"`
|
|
|
|
|
|
Members int `json:"members"`
|
|
|
|
|
|
Current bool `json:"current"`
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
type TeamMember struct {
|
|
|
|
|
|
UserID int64 `json:"userId"`
|
|
|
|
|
|
Username string `json:"username"`
|
|
|
|
|
|
Nickname string `json:"nickname"`
|
|
|
|
|
|
Title string `json:"title"`
|
|
|
|
|
|
Bio string `json:"bio"`
|
|
|
|
|
|
TechTags []string `json:"techTags"`
|
|
|
|
|
|
Avatar string `json:"avatar"`
|
|
|
|
|
|
Role string `json:"role"`
|
|
|
|
|
|
JoinedAt string `json:"joinedAt"`
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
type TeamTask struct {
|
|
|
|
|
|
ID int64 `json:"id"`
|
|
|
|
|
|
TeamID int64 `json:"teamId"`
|
|
|
|
|
|
Kind string `json:"kind"` // todo | ticket
|
|
|
|
|
|
Title string `json:"title"`
|
|
|
|
|
|
Description string `json:"description"`
|
|
|
|
|
|
Priority string `json:"priority"`
|
|
|
|
|
|
Status string `json:"status"` // open | doing | done | closed
|
|
|
|
|
|
CreatorID int64 `json:"creatorId"`
|
|
|
|
|
|
Creator string `json:"creator"`
|
|
|
|
|
|
AssigneeID int64 `json:"assigneeId"`
|
|
|
|
|
|
Assignee string `json:"assignee"`
|
|
|
|
|
|
StartAt string `json:"startAt"`
|
|
|
|
|
|
DueAt string `json:"dueAt"`
|
|
|
|
|
|
UrgedAt string `json:"urgedAt"`
|
|
|
|
|
|
History string `json:"history"`
|
|
|
|
|
|
UpdatedAt string `json:"updatedAt"`
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
type TeamSharedItem struct {
|
|
|
|
|
|
Kind string `json:"kind"` // todo | ticket
|
|
|
|
|
|
UUID string `json:"uuid"`
|
|
|
|
|
|
UserID int64 `json:"userId"`
|
|
|
|
|
|
Owner string `json:"owner"`
|
|
|
|
|
|
Title string `json:"title"`
|
|
|
|
|
|
Status string `json:"status"`
|
|
|
|
|
|
Priority string `json:"priority"`
|
|
|
|
|
|
DueAt string `json:"dueAt"`
|
|
|
|
|
|
UpdatedAt string `json:"updatedAt"`
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
type TeamReport struct {
|
|
|
|
|
|
UserID int64 `json:"userId"`
|
|
|
|
|
|
User string `json:"user"`
|
|
|
|
|
|
Date string `json:"date"`
|
|
|
|
|
|
Content string `json:"content"`
|
|
|
|
|
|
SubmittedAt string `json:"submittedAt"`
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
type TeamDigest struct {
|
|
|
|
|
|
Date string `json:"date"`
|
|
|
|
|
|
Content string `json:"content"`
|
|
|
|
|
|
Provider string `json:"provider"`
|
|
|
|
|
|
GeneratedAt string `json:"generatedAt"`
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// TeamReportBoard 一天的日报全景:成员提交内容 + 未交名单 + AI 摘要。
|
|
|
|
|
|
type TeamReportBoard struct {
|
|
|
|
|
|
Date string `json:"date"`
|
|
|
|
|
|
Role string `json:"role"`
|
|
|
|
|
|
Reports []TeamReport `json:"reports"`
|
|
|
|
|
|
Missing []TeamMember `json:"missing"`
|
|
|
|
|
|
Digest *TeamDigest `json:"digest"`
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ---------- 基础设施 ----------
|
|
|
|
|
|
|
|
|
|
|
|
var teamDateRe = regexp.MustCompile(`^\d{4}-\d{2}-\d{2}$`)
|
|
|
|
|
|
|
2026-08-15 17:18:00 +08:00
|
|
|
|
// requireSyncLogin 校验本地登录态与 API 基址。
|
|
|
|
|
|
func (a *App) requireSyncLogin() (int64, error) {
|
2026-08-14 07:52:01 +08:00
|
|
|
|
if e := a.ready(); e != nil {
|
2026-08-15 17:18:00 +08:00
|
|
|
|
return 0, e
|
2026-08-14 07:52:01 +08:00
|
|
|
|
}
|
|
|
|
|
|
uid := a.syncUserID()
|
2026-08-15 17:18:00 +08:00
|
|
|
|
if uid <= 0 || a.store.Meta("sync_access_token") == "" {
|
|
|
|
|
|
return 0, errors.New("SYNC_NOT_LOGGED_IN")
|
2026-08-14 07:52:01 +08:00
|
|
|
|
}
|
2026-08-15 17:18:00 +08:00
|
|
|
|
if a.apiBaseURL() == "" {
|
|
|
|
|
|
return 0, errors.New("SYNC_NOT_CONFIGURED")
|
2026-08-14 07:52:01 +08:00
|
|
|
|
}
|
2026-08-15 17:18:00 +08:00
|
|
|
|
return uid, nil
|
2026-08-14 07:52:01 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func teamRoleRank(role string) int {
|
|
|
|
|
|
switch role {
|
|
|
|
|
|
case "owner":
|
|
|
|
|
|
return 3
|
|
|
|
|
|
case "admin":
|
|
|
|
|
|
return 2
|
|
|
|
|
|
case "member":
|
|
|
|
|
|
return 1
|
|
|
|
|
|
}
|
|
|
|
|
|
return 0
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-15 17:18:00 +08:00
|
|
|
|
func teamPath(teamID int64, suffix string) string {
|
|
|
|
|
|
p := fmt.Sprintf("/api/v1/teams/%d", teamID)
|
|
|
|
|
|
if suffix != "" {
|
|
|
|
|
|
p += suffix
|
2026-08-14 07:52:01 +08:00
|
|
|
|
}
|
2026-08-15 17:18:00 +08:00
|
|
|
|
return p
|
2026-08-14 07:52:01 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ---------- 团队 CRUD ----------
|
|
|
|
|
|
|
|
|
|
|
|
func (a *App) TeamCreate(name string) (TeamInfo, error) {
|
|
|
|
|
|
name = strings.TrimSpace(name)
|
|
|
|
|
|
if name == "" || len([]rune(name)) > 64 {
|
|
|
|
|
|
return TeamInfo{}, errors.New("TEAM_NAME_INVALID")
|
|
|
|
|
|
}
|
2026-08-15 17:18:00 +08:00
|
|
|
|
if _, e := a.requireSyncLogin(); e != nil {
|
2026-08-14 07:52:01 +08:00
|
|
|
|
return TeamInfo{}, e
|
|
|
|
|
|
}
|
2026-08-15 17:18:00 +08:00
|
|
|
|
var out TeamInfo
|
|
|
|
|
|
if e := a.apiDecode(http.MethodPost, "/api/v1/teams", map[string]string{"name": name}, &out, true); e != nil {
|
|
|
|
|
|
return TeamInfo{}, e
|
2026-08-14 07:52:01 +08:00
|
|
|
|
}
|
2026-08-15 17:18:00 +08:00
|
|
|
|
out.Current = true
|
|
|
|
|
|
_ = a.store.SetMeta("current_team_id", fmt.Sprint(out.ID))
|
2026-08-14 07:52:01 +08:00
|
|
|
|
a.store.Log("info", "团队", "团队创建成功", name)
|
2026-08-15 17:18:00 +08:00
|
|
|
|
return out, nil
|
2026-08-14 07:52:01 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func (a *App) TeamList() ([]TeamInfo, error) {
|
2026-08-15 17:18:00 +08:00
|
|
|
|
if _, e := a.requireSyncLogin(); e != nil {
|
2026-08-14 07:52:01 +08:00
|
|
|
|
return nil, e
|
|
|
|
|
|
}
|
2026-08-15 17:18:00 +08:00
|
|
|
|
var resp struct {
|
|
|
|
|
|
Items []TeamInfo `json:"items"`
|
2026-08-14 07:52:01 +08:00
|
|
|
|
}
|
2026-08-15 17:18:00 +08:00
|
|
|
|
if e := a.apiDecode(http.MethodGet, "/api/v1/teams", nil, &resp, true); e != nil {
|
2026-08-14 07:52:01 +08:00
|
|
|
|
return nil, e
|
|
|
|
|
|
}
|
2026-08-15 17:18:00 +08:00
|
|
|
|
out := resp.Items
|
|
|
|
|
|
if out == nil {
|
|
|
|
|
|
out = []TeamInfo{}
|
|
|
|
|
|
}
|
2026-08-14 07:52:01 +08:00
|
|
|
|
cur := a.store.Meta("current_team_id")
|
|
|
|
|
|
found := false
|
|
|
|
|
|
for i := range out {
|
|
|
|
|
|
if fmt.Sprint(out[i].ID) == cur {
|
|
|
|
|
|
out[i].Current = true
|
|
|
|
|
|
found = true
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
if !found && len(out) > 0 {
|
|
|
|
|
|
out[0].Current = true
|
|
|
|
|
|
_ = a.store.SetMeta("current_team_id", fmt.Sprint(out[0].ID))
|
|
|
|
|
|
}
|
|
|
|
|
|
return out, nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func (a *App) TeamSwitch(teamID int64) error {
|
2026-08-15 17:18:00 +08:00
|
|
|
|
uid, e := a.requireSyncLogin()
|
2026-08-14 07:52:01 +08:00
|
|
|
|
if e != nil {
|
|
|
|
|
|
return e
|
|
|
|
|
|
}
|
2026-08-15 17:18:00 +08:00
|
|
|
|
teams, e := a.TeamList()
|
|
|
|
|
|
if e != nil {
|
2026-08-14 07:52:01 +08:00
|
|
|
|
return e
|
|
|
|
|
|
}
|
2026-08-15 17:18:00 +08:00
|
|
|
|
ok := false
|
|
|
|
|
|
for _, t := range teams {
|
|
|
|
|
|
if t.ID == teamID {
|
|
|
|
|
|
ok = true
|
|
|
|
|
|
break
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
if !ok {
|
|
|
|
|
|
return errors.New("TEAM_FORBIDDEN")
|
|
|
|
|
|
}
|
2026-08-14 07:52:01 +08:00
|
|
|
|
if e := a.store.SetMeta("current_team_id", fmt.Sprint(teamID)); e != nil {
|
|
|
|
|
|
return e
|
|
|
|
|
|
}
|
2026-08-15 17:18:00 +08:00
|
|
|
|
_ = uid
|
|
|
|
|
|
a.refreshTeamAssignedCache()
|
2026-08-14 07:52:01 +08:00
|
|
|
|
return nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func (a *App) TeamRename(teamID int64, name string) error {
|
|
|
|
|
|
name = strings.TrimSpace(name)
|
|
|
|
|
|
if name == "" || len([]rune(name)) > 64 {
|
|
|
|
|
|
return errors.New("TEAM_NAME_INVALID")
|
|
|
|
|
|
}
|
2026-08-15 17:18:00 +08:00
|
|
|
|
if _, e := a.requireSyncLogin(); e != nil {
|
2026-08-14 07:52:01 +08:00
|
|
|
|
return e
|
|
|
|
|
|
}
|
2026-08-15 17:18:00 +08:00
|
|
|
|
return a.apiDecode(http.MethodPut, teamPath(teamID, ""), map[string]string{"name": name}, nil, true)
|
2026-08-14 07:52:01 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func (a *App) TeamSetDigestTime(teamID int64, at string) error {
|
|
|
|
|
|
if _, ok := parseClockOK(at); !ok {
|
|
|
|
|
|
return errors.New("TEAM_TIME_INVALID")
|
|
|
|
|
|
}
|
2026-08-15 17:18:00 +08:00
|
|
|
|
if _, e := a.requireSyncLogin(); e != nil {
|
2026-08-14 07:52:01 +08:00
|
|
|
|
return e
|
|
|
|
|
|
}
|
2026-08-15 17:18:00 +08:00
|
|
|
|
return a.apiDecode(http.MethodPut, teamPath(teamID, "/digest-time"), map[string]string{"digestTime": at}, nil, true)
|
2026-08-14 07:52:01 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func parseClockOK(at string) (string, bool) {
|
|
|
|
|
|
parts := strings.SplitN(at, ":", 2)
|
|
|
|
|
|
if len(parts) != 2 {
|
|
|
|
|
|
return "", false
|
|
|
|
|
|
}
|
|
|
|
|
|
var h, m int
|
|
|
|
|
|
if _, e := fmt.Sscanf(at, "%d:%d", &h, &m); e != nil || h < 0 || h > 23 || m < 0 || m > 59 {
|
|
|
|
|
|
return "", false
|
|
|
|
|
|
}
|
|
|
|
|
|
return at, true
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func (a *App) TeamDissolve(teamID int64) error {
|
2026-08-15 17:18:00 +08:00
|
|
|
|
uid, e := a.requireSyncLogin()
|
2026-08-14 07:52:01 +08:00
|
|
|
|
if e != nil {
|
|
|
|
|
|
return e
|
|
|
|
|
|
}
|
2026-08-15 17:18:00 +08:00
|
|
|
|
if e := a.apiDecode(http.MethodDelete, teamPath(teamID, ""), nil, nil, true); e != nil {
|
2026-08-14 07:52:01 +08:00
|
|
|
|
return e
|
|
|
|
|
|
}
|
|
|
|
|
|
if a.store.Meta("current_team_id") == fmt.Sprint(teamID) {
|
|
|
|
|
|
_ = a.store.SetMeta("current_team_id", "")
|
|
|
|
|
|
}
|
2026-08-15 17:18:00 +08:00
|
|
|
|
a.refreshTeamAssignedCache()
|
|
|
|
|
|
_ = uid
|
|
|
|
|
|
a.store.Log("warning", "团队", "团队已解散", fmt.Sprint(teamID))
|
2026-08-14 07:52:01 +08:00
|
|
|
|
return nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func (a *App) TeamLeave(teamID int64) error {
|
2026-08-15 17:18:00 +08:00
|
|
|
|
uid, e := a.requireSyncLogin()
|
2026-08-14 07:52:01 +08:00
|
|
|
|
if e != nil {
|
|
|
|
|
|
return e
|
|
|
|
|
|
}
|
2026-08-15 17:18:00 +08:00
|
|
|
|
if e := a.apiDecode(http.MethodPost, teamPath(teamID, "/leave"), nil, nil, true); e != nil {
|
2026-08-14 07:52:01 +08:00
|
|
|
|
return e
|
|
|
|
|
|
}
|
|
|
|
|
|
if a.store.Meta("current_team_id") == fmt.Sprint(teamID) {
|
|
|
|
|
|
_ = a.store.SetMeta("current_team_id", "")
|
|
|
|
|
|
}
|
2026-08-15 17:18:00 +08:00
|
|
|
|
a.refreshTeamAssignedCache()
|
|
|
|
|
|
_ = uid
|
2026-08-14 07:52:01 +08:00
|
|
|
|
return nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ---------- 成员管理 ----------
|
|
|
|
|
|
|
|
|
|
|
|
func (a *App) TeamMembers(teamID int64) ([]TeamMember, error) {
|
2026-08-15 17:18:00 +08:00
|
|
|
|
if _, e := a.requireSyncLogin(); e != nil {
|
2026-08-14 07:52:01 +08:00
|
|
|
|
return nil, e
|
|
|
|
|
|
}
|
2026-08-15 17:18:00 +08:00
|
|
|
|
var resp struct {
|
|
|
|
|
|
Items []TeamMember `json:"items"`
|
|
|
|
|
|
}
|
|
|
|
|
|
if e := a.apiDecode(http.MethodGet, teamPath(teamID, "/members"), nil, &resp, true); e != nil {
|
2026-08-14 07:52:01 +08:00
|
|
|
|
return nil, e
|
|
|
|
|
|
}
|
2026-08-15 17:18:00 +08:00
|
|
|
|
if resp.Items == nil {
|
|
|
|
|
|
return []TeamMember{}, nil
|
|
|
|
|
|
}
|
|
|
|
|
|
for i := range resp.Items {
|
|
|
|
|
|
if resp.Items[i].TechTags == nil {
|
|
|
|
|
|
resp.Items[i].TechTags = []string{}
|
2026-08-14 07:52:01 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-08-15 17:18:00 +08:00
|
|
|
|
return resp.Items, nil
|
2026-08-14 07:52:01 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func (a *App) TeamInvite(teamID int64, username, role string) error {
|
|
|
|
|
|
if role != "member" && role != "admin" {
|
|
|
|
|
|
role = "member"
|
|
|
|
|
|
}
|
|
|
|
|
|
username = strings.TrimSpace(username)
|
2026-08-15 17:18:00 +08:00
|
|
|
|
if _, e := a.requireSyncLogin(); e != nil {
|
2026-08-14 07:52:01 +08:00
|
|
|
|
return e
|
|
|
|
|
|
}
|
2026-08-15 17:18:00 +08:00
|
|
|
|
return a.apiDecode(http.MethodPost, teamPath(teamID, "/invite"), map[string]string{
|
|
|
|
|
|
"username": username,
|
|
|
|
|
|
"role": role,
|
|
|
|
|
|
}, nil, true)
|
2026-08-14 07:52:01 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func (a *App) TeamSetRole(teamID, userID int64, role string) error {
|
|
|
|
|
|
if role != "member" && role != "admin" {
|
|
|
|
|
|
return errors.New("TEAM_ROLE_INVALID")
|
|
|
|
|
|
}
|
2026-08-15 17:18:00 +08:00
|
|
|
|
if _, e := a.requireSyncLogin(); e != nil {
|
2026-08-14 07:52:01 +08:00
|
|
|
|
return e
|
|
|
|
|
|
}
|
2026-08-15 17:18:00 +08:00
|
|
|
|
return a.apiDecode(http.MethodPut, teamPath(teamID, fmt.Sprintf("/members/%d/role", userID)),
|
|
|
|
|
|
map[string]string{"role": role}, nil, true)
|
2026-08-14 07:52:01 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func (a *App) TeamRemoveMember(teamID, userID int64) error {
|
2026-08-15 17:18:00 +08:00
|
|
|
|
if _, e := a.requireSyncLogin(); e != nil {
|
2026-08-14 07:52:01 +08:00
|
|
|
|
return e
|
|
|
|
|
|
}
|
2026-08-15 17:18:00 +08:00
|
|
|
|
return a.apiDecode(http.MethodDelete, teamPath(teamID, fmt.Sprintf("/members/%d", userID)), nil, nil, true)
|
2026-08-14 07:52:01 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ---------- 团队任务 ----------
|
|
|
|
|
|
|
|
|
|
|
|
func validTeamTask(t *TeamTask) error {
|
|
|
|
|
|
t.Title = strings.TrimSpace(t.Title)
|
|
|
|
|
|
if t.Title == "" {
|
|
|
|
|
|
return errors.New("TEAM_TASK_TITLE_REQUIRED")
|
|
|
|
|
|
}
|
|
|
|
|
|
if t.Kind != "ticket" {
|
|
|
|
|
|
t.Kind = "todo"
|
|
|
|
|
|
}
|
|
|
|
|
|
switch t.Priority {
|
|
|
|
|
|
case "low", "medium", "high":
|
|
|
|
|
|
default:
|
|
|
|
|
|
t.Priority = "medium"
|
|
|
|
|
|
}
|
|
|
|
|
|
return nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-15 17:18:00 +08:00
|
|
|
|
// TeamTaskSave 创建/编辑团队任务(admin+)。
|
2026-08-14 07:52:01 +08:00
|
|
|
|
func (a *App) TeamTaskSave(t TeamTask) (TeamTask, error) {
|
|
|
|
|
|
if e := validTeamTask(&t); e != nil {
|
|
|
|
|
|
return TeamTask{}, e
|
|
|
|
|
|
}
|
2026-08-15 17:18:00 +08:00
|
|
|
|
if _, e := a.requireSyncLogin(); e != nil {
|
2026-08-14 07:52:01 +08:00
|
|
|
|
return TeamTask{}, e
|
|
|
|
|
|
}
|
2026-08-15 17:18:00 +08:00
|
|
|
|
path := teamPath(t.TeamID, "/tasks")
|
|
|
|
|
|
method := http.MethodPost
|
|
|
|
|
|
if t.ID > 0 {
|
|
|
|
|
|
path = teamPath(t.TeamID, fmt.Sprintf("/tasks/%d", t.ID))
|
|
|
|
|
|
method = http.MethodPut
|
2026-08-14 07:52:01 +08:00
|
|
|
|
}
|
2026-08-15 17:18:00 +08:00
|
|
|
|
var out TeamTask
|
|
|
|
|
|
if e := a.apiDecode(method, path, t, &out, true); e != nil {
|
|
|
|
|
|
return TeamTask{}, e
|
2026-08-14 07:52:01 +08:00
|
|
|
|
}
|
2026-08-15 17:18:00 +08:00
|
|
|
|
a.refreshTeamAssignedCache()
|
|
|
|
|
|
return out, nil
|
2026-08-14 07:52:01 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func (a *App) TeamTaskSetStatus(teamID, id int64, status string) error {
|
|
|
|
|
|
switch status {
|
|
|
|
|
|
case "open", "doing", "done", "closed":
|
|
|
|
|
|
default:
|
|
|
|
|
|
return errors.New("TEAM_STATUS_INVALID")
|
|
|
|
|
|
}
|
2026-08-15 17:18:00 +08:00
|
|
|
|
if _, e := a.requireSyncLogin(); e != nil {
|
2026-08-14 07:52:01 +08:00
|
|
|
|
return e
|
|
|
|
|
|
}
|
2026-08-15 17:18:00 +08:00
|
|
|
|
if e := a.apiDecode(http.MethodPut, teamPath(teamID, fmt.Sprintf("/tasks/%d/status", id)),
|
|
|
|
|
|
map[string]string{"status": status}, nil, true); e != nil {
|
2026-08-14 07:52:01 +08:00
|
|
|
|
return e
|
|
|
|
|
|
}
|
2026-08-15 17:18:00 +08:00
|
|
|
|
a.refreshTeamAssignedCache()
|
2026-08-14 07:52:01 +08:00
|
|
|
|
return nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-15 17:18:00 +08:00
|
|
|
|
func (a *App) TeamTaskUrge(teamID, id int64) error {
|
|
|
|
|
|
if _, e := a.requireSyncLogin(); e != nil {
|
|
|
|
|
|
return e
|
|
|
|
|
|
}
|
|
|
|
|
|
return a.apiDecode(http.MethodPost, teamPath(teamID, fmt.Sprintf("/tasks/%d/urge", id)), nil, nil, true)
|
2026-08-14 07:52:01 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-15 17:18:00 +08:00
|
|
|
|
// appendHistoryNode 追加一个生命周期节点(本地单测与兼容保留;服务端也会写 history)。
|
2026-08-14 07:52:01 +08:00
|
|
|
|
func appendHistoryNode(history, status, at string) string {
|
|
|
|
|
|
node := fmt.Sprintf(`{"status":%q,"at":%q}`, status, at)
|
|
|
|
|
|
h := strings.TrimSpace(history)
|
|
|
|
|
|
if h == "" || h == "[]" {
|
|
|
|
|
|
return "[" + node + "]"
|
|
|
|
|
|
}
|
|
|
|
|
|
if strings.HasSuffix(h, "]") {
|
|
|
|
|
|
return h[:len(h)-1] + "," + node + "]"
|
|
|
|
|
|
}
|
|
|
|
|
|
return "[" + node + "]"
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func (a *App) TeamTaskDelete(teamID, id int64) error {
|
2026-08-15 17:18:00 +08:00
|
|
|
|
if _, e := a.requireSyncLogin(); e != nil {
|
2026-08-14 07:52:01 +08:00
|
|
|
|
return e
|
|
|
|
|
|
}
|
2026-08-15 17:18:00 +08:00
|
|
|
|
if e := a.apiDecode(http.MethodDelete, teamPath(teamID, fmt.Sprintf("/tasks/%d", id)), nil, nil, true); e != nil {
|
2026-08-14 07:52:01 +08:00
|
|
|
|
return e
|
|
|
|
|
|
}
|
2026-08-15 17:18:00 +08:00
|
|
|
|
a.refreshTeamAssignedCache()
|
2026-08-14 07:52:01 +08:00
|
|
|
|
return nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// TeamTaskList 列出团队任务;filter: all | mine | created | open。
|
|
|
|
|
|
func (a *App) TeamTaskList(teamID int64, filter string) ([]TeamTask, error) {
|
2026-08-15 17:18:00 +08:00
|
|
|
|
if _, e := a.requireSyncLogin(); e != nil {
|
2026-08-14 07:52:01 +08:00
|
|
|
|
return nil, e
|
|
|
|
|
|
}
|
2026-08-15 17:18:00 +08:00
|
|
|
|
q := teamPath(teamID, "/tasks")
|
|
|
|
|
|
if filter != "" {
|
|
|
|
|
|
q += "?filter=" + url.QueryEscape(filter)
|
|
|
|
|
|
}
|
|
|
|
|
|
var resp struct {
|
|
|
|
|
|
Items []TeamTask `json:"items"`
|
|
|
|
|
|
}
|
|
|
|
|
|
if e := a.apiDecode(http.MethodGet, q, nil, &resp, true); e != nil {
|
2026-08-14 07:52:01 +08:00
|
|
|
|
return nil, e
|
|
|
|
|
|
}
|
2026-08-15 17:18:00 +08:00
|
|
|
|
if resp.Items == nil {
|
|
|
|
|
|
return []TeamTask{}, nil
|
2026-08-14 07:52:01 +08:00
|
|
|
|
}
|
2026-08-15 17:18:00 +08:00
|
|
|
|
return resp.Items, nil
|
2026-08-14 07:52:01 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ---------- 个人条目共享 ----------
|
|
|
|
|
|
|
|
|
|
|
|
func (a *App) SetTodoTeam(id, teamID int64) error {
|
|
|
|
|
|
return a.setItemTeam("todos", id, teamID)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func (a *App) SetTicketTeam(id, teamID int64) error {
|
|
|
|
|
|
return a.setItemTeam("tickets", id, teamID)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func (a *App) setItemTeam(table string, id, teamID int64) error {
|
|
|
|
|
|
if e := a.ready(); e != nil {
|
|
|
|
|
|
return e
|
|
|
|
|
|
}
|
|
|
|
|
|
if teamID < 0 {
|
|
|
|
|
|
teamID = 0
|
|
|
|
|
|
}
|
|
|
|
|
|
res, e := a.store.db.Exec(`UPDATE `+table+` SET team_id=?,dirty=1,updated_at=? WHERE id=? AND deleted=0`, teamID, nowRFC(), id)
|
|
|
|
|
|
if e != nil {
|
|
|
|
|
|
return e
|
|
|
|
|
|
}
|
|
|
|
|
|
if n, _ := res.RowsAffected(); n == 0 {
|
|
|
|
|
|
return errors.New("NOT_FOUND")
|
|
|
|
|
|
}
|
|
|
|
|
|
if a.syncUserID() > 0 {
|
|
|
|
|
|
go a.syncOnce(true)
|
|
|
|
|
|
}
|
|
|
|
|
|
return nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func (a *App) TeamSharedItems(teamID int64) ([]TeamSharedItem, error) {
|
2026-08-15 17:18:00 +08:00
|
|
|
|
if _, e := a.requireSyncLogin(); e != nil {
|
2026-08-14 07:52:01 +08:00
|
|
|
|
return nil, e
|
|
|
|
|
|
}
|
2026-08-15 17:18:00 +08:00
|
|
|
|
var resp struct {
|
|
|
|
|
|
Items []TeamSharedItem `json:"items"`
|
|
|
|
|
|
}
|
|
|
|
|
|
if e := a.apiDecode(http.MethodGet, teamPath(teamID, "/shared"), nil, &resp, true); e != nil {
|
2026-08-14 07:52:01 +08:00
|
|
|
|
return nil, e
|
|
|
|
|
|
}
|
2026-08-15 17:18:00 +08:00
|
|
|
|
if resp.Items == nil {
|
|
|
|
|
|
return []TeamSharedItem{}, nil
|
2026-08-14 07:52:01 +08:00
|
|
|
|
}
|
2026-08-15 17:18:00 +08:00
|
|
|
|
return resp.Items, nil
|
2026-08-14 07:52:01 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func (a *App) TeamUrgeShared(teamID int64, kind, uuid string) error {
|
2026-08-15 17:18:00 +08:00
|
|
|
|
if _, e := a.requireSyncLogin(); e != nil {
|
2026-08-14 07:52:01 +08:00
|
|
|
|
return e
|
|
|
|
|
|
}
|
2026-08-15 17:18:00 +08:00
|
|
|
|
return a.apiDecode(http.MethodPost, teamPath(teamID, "/shared/urge"), map[string]string{
|
|
|
|
|
|
"kind": kind,
|
|
|
|
|
|
"uuid": uuid,
|
|
|
|
|
|
}, nil, true)
|
2026-08-14 07:52:01 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ---------- 日报 ----------
|
|
|
|
|
|
|
|
|
|
|
|
func (a *App) TeamReportSubmit(teamID int64, date, content string) error {
|
|
|
|
|
|
if !teamDateRe.MatchString(date) {
|
|
|
|
|
|
return errors.New("TEAM_DATE_INVALID")
|
|
|
|
|
|
}
|
|
|
|
|
|
content = strings.TrimSpace(content)
|
|
|
|
|
|
if content == "" {
|
|
|
|
|
|
return errors.New("TEAM_REPORT_EMPTY")
|
|
|
|
|
|
}
|
2026-08-15 17:18:00 +08:00
|
|
|
|
if _, e := a.requireSyncLogin(); e != nil {
|
2026-08-14 07:52:01 +08:00
|
|
|
|
return e
|
|
|
|
|
|
}
|
2026-08-15 17:18:00 +08:00
|
|
|
|
return a.apiDecode(http.MethodPost, teamPath(teamID, "/reports"), map[string]string{
|
|
|
|
|
|
"date": date,
|
|
|
|
|
|
"content": content,
|
|
|
|
|
|
}, nil, true)
|
2026-08-14 07:52:01 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func (a *App) TeamReportBoardGet(teamID int64, date string) (TeamReportBoard, error) {
|
|
|
|
|
|
if !teamDateRe.MatchString(date) {
|
|
|
|
|
|
return TeamReportBoard{}, errors.New("TEAM_DATE_INVALID")
|
|
|
|
|
|
}
|
2026-08-15 17:18:00 +08:00
|
|
|
|
if _, e := a.requireSyncLogin(); e != nil {
|
2026-08-14 07:52:01 +08:00
|
|
|
|
return TeamReportBoard{}, e
|
|
|
|
|
|
}
|
2026-08-15 17:18:00 +08:00
|
|
|
|
var board TeamReportBoard
|
|
|
|
|
|
if e := a.apiDecode(http.MethodGet, teamPath(teamID, "/reports/"+url.PathEscape(date)), nil, &board, true); e != nil {
|
2026-08-14 07:52:01 +08:00
|
|
|
|
return TeamReportBoard{}, e
|
|
|
|
|
|
}
|
2026-08-15 17:18:00 +08:00
|
|
|
|
if board.Reports == nil {
|
|
|
|
|
|
board.Reports = []TeamReport{}
|
2026-08-14 07:52:01 +08:00
|
|
|
|
}
|
2026-08-15 17:18:00 +08:00
|
|
|
|
if board.Missing == nil {
|
|
|
|
|
|
board.Missing = []TeamMember{}
|
2026-08-14 07:52:01 +08:00
|
|
|
|
}
|
2026-08-15 17:18:00 +08:00
|
|
|
|
if board.Digest != nil {
|
|
|
|
|
|
board.Digest.Content = stripFence(board.Digest.Content)
|
2026-08-14 07:52:01 +08:00
|
|
|
|
}
|
|
|
|
|
|
return board, nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func (a *App) TeamReportUrge(teamID, userID int64, date string) error {
|
|
|
|
|
|
if !teamDateRe.MatchString(date) {
|
|
|
|
|
|
return errors.New("TEAM_DATE_INVALID")
|
|
|
|
|
|
}
|
2026-08-15 17:18:00 +08:00
|
|
|
|
if _, e := a.requireSyncLogin(); e != nil {
|
2026-08-14 07:52:01 +08:00
|
|
|
|
return e
|
|
|
|
|
|
}
|
2026-08-15 17:18:00 +08:00
|
|
|
|
return a.apiDecode(http.MethodPost, teamPath(teamID, "/reports/urge"), map[string]any{
|
|
|
|
|
|
"userId": userID,
|
|
|
|
|
|
"date": date,
|
|
|
|
|
|
}, nil, true)
|
2026-08-14 07:52:01 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ---------- 日报 AI 摘要 ----------
|
|
|
|
|
|
|
|
|
|
|
|
type teamDigestEvent struct {
|
|
|
|
|
|
TeamID int64 `json:"teamId"`
|
|
|
|
|
|
Date string `json:"date"`
|
|
|
|
|
|
Error string `json:"error,omitempty"`
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func (a *App) TeamDigestGenerate(teamID int64, date string) error {
|
|
|
|
|
|
if !teamDateRe.MatchString(date) {
|
|
|
|
|
|
return errors.New("TEAM_DATE_INVALID")
|
|
|
|
|
|
}
|
|
|
|
|
|
if _, e := a.aiProvider(); e != nil {
|
|
|
|
|
|
return e
|
|
|
|
|
|
}
|
2026-08-15 17:18:00 +08:00
|
|
|
|
board, e := a.TeamReportBoardGet(teamID, date)
|
2026-08-14 07:52:01 +08:00
|
|
|
|
if e != nil {
|
|
|
|
|
|
return e
|
|
|
|
|
|
}
|
2026-08-15 17:18:00 +08:00
|
|
|
|
if teamRoleRank(board.Role) < teamRoleRank("admin") {
|
|
|
|
|
|
return errors.New("TEAM_FORBIDDEN")
|
2026-08-14 07:52:01 +08:00
|
|
|
|
}
|
|
|
|
|
|
go a.generateTeamDigest(teamID, date)
|
|
|
|
|
|
return nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func (a *App) generateTeamDigest(teamID int64, date string) {
|
|
|
|
|
|
emitErr := func(msg string) {
|
|
|
|
|
|
a.emit("team:digest", teamDigestEvent{TeamID: teamID, Date: date, Error: msg})
|
|
|
|
|
|
}
|
|
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Minute)
|
|
|
|
|
|
defer cancel()
|
2026-08-15 17:18:00 +08:00
|
|
|
|
board, e := a.TeamReportBoardGet(teamID, date)
|
2026-08-14 07:52:01 +08:00
|
|
|
|
if e != nil {
|
|
|
|
|
|
emitErr(e.Error())
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
var sb strings.Builder
|
|
|
|
|
|
n := 0
|
2026-08-15 17:18:00 +08:00
|
|
|
|
for _, r := range board.Reports {
|
|
|
|
|
|
if strings.TrimSpace(r.Content) == "" {
|
|
|
|
|
|
continue
|
2026-08-14 07:52:01 +08:00
|
|
|
|
}
|
2026-08-15 17:18:00 +08:00
|
|
|
|
fmt.Fprintf(&sb, "### %s\n%s\n\n", r.User, r.Content)
|
|
|
|
|
|
n++
|
2026-08-14 07:52:01 +08:00
|
|
|
|
}
|
|
|
|
|
|
if n == 0 {
|
|
|
|
|
|
emitErr("TEAM_NO_REPORTS")
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
provider, e := a.aiProvider()
|
|
|
|
|
|
if e != nil {
|
|
|
|
|
|
emitErr(e.Error())
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
2026-08-15 17:18:00 +08:00
|
|
|
|
teamName := fmt.Sprintf("#%d", teamID)
|
|
|
|
|
|
if teams, le := a.TeamList(); le == nil {
|
|
|
|
|
|
for _, t := range teams {
|
|
|
|
|
|
if t.ID == teamID {
|
|
|
|
|
|
teamName = t.Name
|
|
|
|
|
|
break
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-08-14 07:52:01 +08:00
|
|
|
|
prompt := fmt.Sprintf("以下是团队「%s」%s 的 %d 份成员日报,请生成一份团队日报摘要(Markdown):\n"+
|
|
|
|
|
|
"1. 今日完成事项汇总(合并同类项)\n2. 进行中的工作\n3. 风险与阻塞(没有则省略)\n4. 每位成员一句话点评\n5. 明日建议\n\n%s",
|
|
|
|
|
|
teamName, date, n, sb.String())
|
|
|
|
|
|
stream, e := provider.ChatStream(ctx, []ai.Message{
|
|
|
|
|
|
{Role: "system", Content: "你是资深研发团队负责人助理,输出简洁、结构化的中文 Markdown 团队日报摘要,不要编造日报里没有的内容。"},
|
|
|
|
|
|
{Role: "user", Content: prompt},
|
|
|
|
|
|
})
|
|
|
|
|
|
if e != nil {
|
|
|
|
|
|
emitErr(e.Error())
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
var out strings.Builder
|
|
|
|
|
|
for chunk := range stream {
|
|
|
|
|
|
if chunk.Err != nil {
|
|
|
|
|
|
emitErr(chunk.Err.Error())
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
out.WriteString(chunk.Content)
|
|
|
|
|
|
}
|
|
|
|
|
|
content := dedentCommon(stripFence(strings.TrimSpace(out.String())))
|
|
|
|
|
|
if content == "" {
|
|
|
|
|
|
emitErr("AI_EMPTY_RESPONSE")
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
2026-08-15 17:18:00 +08:00
|
|
|
|
if e := a.apiDecode(http.MethodPut, teamPath(teamID, "/digests/"+url.PathEscape(date)), map[string]string{
|
|
|
|
|
|
"content": content,
|
|
|
|
|
|
"provider": provider.Name(),
|
|
|
|
|
|
}, nil, true); e != nil {
|
|
|
|
|
|
emitErr(e.Error())
|
2026-08-14 07:52:01 +08:00
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
a.store.Log("info", "团队", "团队日报摘要已生成", fmt.Sprintf("team=%d date=%s", teamID, date))
|
|
|
|
|
|
a.emit("team:digest", teamDigestEvent{TeamID: teamID, Date: date})
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func (a *App) runTeamDigestLoop() {
|
|
|
|
|
|
ticker := time.NewTicker(5 * time.Minute)
|
|
|
|
|
|
defer ticker.Stop()
|
|
|
|
|
|
for range ticker.C {
|
|
|
|
|
|
a.teamDigestTick(time.Now())
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func (a *App) teamDigestTick(now time.Time) {
|
|
|
|
|
|
if a.store == nil || a.syncUserID() <= 0 || !a.syncOnline.Load() {
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
2026-08-15 17:18:00 +08:00
|
|
|
|
teams, e := a.TeamList()
|
2026-08-14 07:52:01 +08:00
|
|
|
|
if e != nil {
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
today := now.Format("2006-01-02")
|
2026-08-15 17:18:00 +08:00
|
|
|
|
for _, t := range teams {
|
|
|
|
|
|
if teamRoleRank(t.Role) < teamRoleRank("admin") {
|
|
|
|
|
|
continue
|
|
|
|
|
|
}
|
|
|
|
|
|
if !service.DigestDue(t.DigestTime, now) {
|
2026-08-14 07:52:01 +08:00
|
|
|
|
continue
|
|
|
|
|
|
}
|
2026-08-15 17:18:00 +08:00
|
|
|
|
markKey := fmt.Sprintf("team_digest_auto_%d", t.ID)
|
2026-08-14 07:52:01 +08:00
|
|
|
|
if a.store.Meta(markKey) == today {
|
|
|
|
|
|
continue
|
|
|
|
|
|
}
|
2026-08-15 17:18:00 +08:00
|
|
|
|
board, e := a.TeamReportBoardGet(t.ID, today)
|
|
|
|
|
|
if e != nil {
|
|
|
|
|
|
continue
|
|
|
|
|
|
}
|
|
|
|
|
|
if board.Digest != nil && strings.TrimSpace(board.Digest.Content) != "" {
|
2026-08-14 07:52:01 +08:00
|
|
|
|
_ = a.store.SetMeta(markKey, today)
|
|
|
|
|
|
continue
|
|
|
|
|
|
}
|
2026-08-15 17:18:00 +08:00
|
|
|
|
if len(board.Reports) == 0 {
|
|
|
|
|
|
continue
|
2026-08-14 07:52:01 +08:00
|
|
|
|
}
|
|
|
|
|
|
if _, e := a.aiProvider(); e != nil {
|
|
|
|
|
|
continue
|
|
|
|
|
|
}
|
|
|
|
|
|
_ = a.store.SetMeta(markKey, today)
|
2026-08-15 17:18:00 +08:00
|
|
|
|
go a.generateTeamDigest(t.ID, today)
|
2026-08-14 07:52:01 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-15 17:18:00 +08:00
|
|
|
|
// refreshTeamAssignedCache 缓存"指派给我的未完成团队任务数"(导航徽标用)。
|
|
|
|
|
|
func (a *App) refreshTeamAssignedCache() {
|
2026-08-14 07:52:01 +08:00
|
|
|
|
n := 0
|
2026-08-15 17:18:00 +08:00
|
|
|
|
if teamID := a.currentTeamID(); teamID > 0 && a.syncUserID() > 0 && a.store.Meta("sync_access_token") != "" {
|
|
|
|
|
|
tasks, e := a.TeamTaskList(teamID, "mine")
|
|
|
|
|
|
if e == nil {
|
|
|
|
|
|
for _, t := range tasks {
|
|
|
|
|
|
if t.Status == "open" || t.Status == "doing" {
|
|
|
|
|
|
n++
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-08-14 07:52:01 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
_ = a.store.SetMeta("team_assigned_cache", fmt.Sprint(n))
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// pullTeamNotices 增量拉取发给本人的团队通知并转成本地消息。
|
2026-08-15 17:18:00 +08:00
|
|
|
|
func (a *App) pullTeamNotices() int {
|
2026-08-14 07:52:01 +08:00
|
|
|
|
cursor := a.store.Meta("team_notice_cursor")
|
|
|
|
|
|
if cursor == "" {
|
|
|
|
|
|
cursor = "0"
|
|
|
|
|
|
}
|
2026-08-15 17:18:00 +08:00
|
|
|
|
var resp struct {
|
|
|
|
|
|
Items []struct {
|
|
|
|
|
|
ID int64 `json:"id"`
|
|
|
|
|
|
TeamID int64 `json:"teamId"`
|
|
|
|
|
|
Kind string `json:"kind"`
|
|
|
|
|
|
Content string `json:"content"`
|
|
|
|
|
|
} `json:"items"`
|
|
|
|
|
|
}
|
|
|
|
|
|
if e := a.apiDecode(http.MethodGet, "/api/v1/notices?after="+url.QueryEscape(cursor), nil, &resp, true); e != nil {
|
2026-08-14 07:52:01 +08:00
|
|
|
|
return 0
|
|
|
|
|
|
}
|
|
|
|
|
|
titles := map[string]string{
|
|
|
|
|
|
"assign": "团队任务指派", "urge": "团队催办", "report_urge": "日报催交",
|
|
|
|
|
|
"member": "团队成员变动", "role": "团队角色变更", "status": "团队任务动态",
|
|
|
|
|
|
}
|
|
|
|
|
|
n := 0
|
|
|
|
|
|
maxID := cursor
|
2026-08-15 17:18:00 +08:00
|
|
|
|
for _, it := range resp.Items {
|
|
|
|
|
|
title := titles[it.Kind]
|
2026-08-14 07:52:01 +08:00
|
|
|
|
if title == "" {
|
|
|
|
|
|
title = "团队通知"
|
|
|
|
|
|
}
|
2026-08-15 17:18:00 +08:00
|
|
|
|
a.pushMessage("team", title, it.Content, "team", it.TeamID, true)
|
|
|
|
|
|
maxID = fmt.Sprint(it.ID)
|
2026-08-14 07:52:01 +08:00
|
|
|
|
n++
|
|
|
|
|
|
}
|
|
|
|
|
|
if maxID != cursor {
|
|
|
|
|
|
_ = a.store.SetMeta("team_notice_cursor", maxID)
|
|
|
|
|
|
}
|
|
|
|
|
|
return n
|
|
|
|
|
|
}
|