Files
agent-radar 2ed3415fb7
Publish Claude Code Binaries / Stage and publish binaries (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-arm64 (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build linux-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build windows-amd64 (push) Canceled after 0s
Sync Tutti Agent Skills / sync (push) Canceled after 0s
Windows Agent Adapters / Test Windows agent adapters (push) Canceled after 0s
Windows Daemon Adapters / Test Windows daemon adapters (push) Canceled after 0s
Publish Tutti App Runtime / Publish runtime catalog (push) Canceled after 0s
源码快照 @ 2026-09-01 (source-only, 剔除非源码素材)
2026-09-01 20:17:27 +08:00

207 lines
5.0 KiB
Go

package commerce
import (
"context"
"encoding/json"
"fmt"
"strconv"
"strings"
"sync"
"time"
)
type loginClaimResponse struct {
GrantNo string `json:"grant_no"`
FirstLoginClaimed bool `json:"first_login_claimed"`
FirstLoginGrantNo string `json:"first_login_grant_no"`
FirstLoginGrantCredits loginClaimCredits `json:"first_login_grant_credits"`
DailyClaimed bool `json:"daily_claimed"`
DailyGrantNo string `json:"daily_grant_no"`
DailyGrantCredits loginClaimCredits `json:"daily_grant_credits"`
}
type loginClaimCredits int64
func (credits *loginClaimCredits) UnmarshalJSON(data []byte) error {
if strings.TrimSpace(string(data)) == "null" {
*credits = 0
return nil
}
var number int64
if err := json.Unmarshal(data, &number); err == nil {
*credits = loginClaimCredits(number)
return nil
}
var text string
if err := json.Unmarshal(data, &text); err != nil {
return err
}
text = strings.TrimSpace(text)
if text == "" {
*credits = 0
return nil
}
parsed, err := strconv.ParseInt(text, 10, 64)
if err != nil {
return err
}
*credits = loginClaimCredits(parsed)
return nil
}
type rewardCoordinator struct {
client *client
store RewardReceiptStore
now func() time.Time
mu sync.Mutex
failClosedUsers map[string]struct{}
}
func (r *rewardCoordinator) reward(
ctx context.Context,
userID string,
) *RegistrationCreditsReward {
userID = strings.TrimSpace(userID)
if userID == "" {
return nil
}
r.mu.Lock()
defer r.mu.Unlock()
if _, blocked := r.failClosedUsers[userID]; blocked {
return nil
}
state, err := r.store.Load(ctx)
if err != nil {
// Fail closed: without durable receipt state the coordinator cannot
// guarantee that a registration reward is shown at most once.
r.failClosedUsers[userID] = struct{}{}
return nil
}
if pending := visibleRegistrationCreditsReward(state, userID); pending != nil {
return pending
}
if state.Attempted != nil && state.Attempted[userID] > 0 {
return nil
}
claim, err := r.client.loginClaim(ctx)
if err != nil {
return nil
}
now := r.now().UTC()
if state.Attempted == nil {
state.Attempted = map[string]int64{}
}
state.Attempted[userID] = now.UnixMilli()
if !claim.FirstLoginClaimed || claim.FirstLoginGrantCredits <= 0 {
if err := r.store.Save(ctx, state); err != nil {
r.failClosedUsers[userID] = struct{}{}
}
return nil
}
grantNo := strings.TrimSpace(claim.FirstLoginGrantNo)
if grantNo == "" {
grantNo = strings.TrimSpace(claim.GrantNo)
}
if grantNo == "" {
grantNo = fmt.Sprintf("first-login-%d", now.UnixMilli())
}
rewardID := RegistrationCreditsRewardID(userID, grantNo)
if state.Shown != nil && state.Shown[rewardID] > 0 {
if err := r.store.Save(ctx, state); err != nil {
r.failClosedUsers[userID] = struct{}{}
}
return nil
}
reward := &RegistrationCreditsReward{
ID: rewardID,
UserID: userID,
GrantNo: grantNo,
Credits: int64(claim.FirstLoginGrantCredits),
CreatedAt: now,
}
state.Pending = reward
if err := r.store.Save(ctx, state); err != nil {
r.failClosedUsers[userID] = struct{}{}
return nil
}
return cloneRegistrationCreditsReward(reward)
}
func (r *rewardCoordinator) dismiss(ctx context.Context, rewardID string) error {
rewardID = strings.TrimSpace(rewardID)
if rewardID == "" {
return ErrRegistrationCreditsRewardIDRequired
}
r.mu.Lock()
defer r.mu.Unlock()
state, err := r.store.Load(ctx)
if err != nil {
return err
}
if state.Shown == nil {
state.Shown = map[string]int64{}
}
state.Shown[rewardID] = r.now().UTC().UnixMilli()
if state.Pending != nil && state.Pending.ID == rewardID {
state.Pending = nil
}
return r.store.Save(ctx, state)
}
func visibleRegistrationCreditsReward(
state RewardReceiptState,
userID string,
) *RegistrationCreditsReward {
if state.Pending == nil ||
state.Pending.UserID != userID ||
state.Pending.Credits <= 0 {
return nil
}
if state.Shown != nil && state.Shown[state.Pending.ID] > 0 {
return nil
}
return cloneRegistrationCreditsReward(state.Pending)
}
func RegistrationCreditsRewardID(userID string, grantNo string) string {
return "registrationCreditsToastShown:" +
strings.TrimSpace(userID) +
":" +
strings.TrimSpace(grantNo)
}
func cloneRegistrationCreditsReward(
reward *RegistrationCreditsReward,
) *RegistrationCreditsReward {
if reward == nil {
return nil
}
cloned := *reward
return &cloned
}
func cloneRewardReceiptState(state RewardReceiptState) RewardReceiptState {
cloned := RewardReceiptState{
Pending: cloneRegistrationCreditsReward(state.Pending),
}
if state.Shown != nil {
cloned.Shown = make(map[string]int64, len(state.Shown))
for key, value := range state.Shown {
cloned.Shown[key] = value
}
}
if state.Attempted != nil {
cloned.Attempted = make(map[string]int64, len(state.Attempted))
for key, value := range state.Attempted {
cloned.Attempted[key] = value
}
}
return cloned
}