Files
magpie/internal/provider/mimo_usage.go
T
yetone 899ddf594c mimo: a Xiaomi MiMo account (the one MiMo's app signs in to) is signed in inside magpie and its models served through the gateway to any agent, and MiMo Code is an agent magpie configures (#249 thedavidweng) — magpie had neither. The subscription (id mimo-app, shown as Xiaomi MiMo) signs in with Xiaomi's long-poll login (account.xiaomi.com/longPolling/loginUrl, sid mimosgp) and keeps the MiMo server's cookie session (serviceToken, userId) in logins.json at 0600; the passToken is replayed to renew the session after a day or on a 401. Requests go to POST <base>/route/chat/completions on mimo-server-{sgp,ru,in}.xiaomimimo.com/api with X-Mimo-Source mimocode-cli-free and X-Client-Version; mimo-auto is sent as mimo-pro; the models are mimo-pro and mimo-flash. Usage from /user/usage and /user/xiaomi/subscription/self. Several accounts, switched, on/off and forgotten as the other subscriptions. The add list has a Xiaomi MiMo tile (Free · Starter · Plus · Pro · Ultra) that says Xiaomi may treat this as third-party use before anything is posted; en and zh. magpie accounts takes mimo. MiMo Code (agent mimocode, OpenCode-like) is configured in its config dir (MIMOCODE_HOME/config when set), reading mimocode.jsonc, mimocode.json or config.json, whichever is there. Tested with internal/provider/mimo_test.go against fake Xiaomi SSO and MiMo servers in a temp HOME, internal/agent tests for MiMo Code's config, and internal/gui/tests/mimo-sub.test.cjs, Chromium and WebKit, en and zh; they fail without the change. factory-sub, click-scroll, old-webkit, zed-sub and add-sheet pass; go vet, go build -tags nogui (and GOOS=linux, GOOS=windows) and go test -tags nogui ./... pass. Not tried with a real Xiaomi account: the long-poll's success payload, the SSO cookie chain, whether /route accepts magpie's requests, the UTC+8 times, and whether MiMo Code picks up the config; not tried in the real Wails window.
2026-09-30 07:43:58 +08:00

108 lines
3.3 KiB
Go

package provider
// What a MiMo account says of its allowance, from the pages the app's
// "Usage & billing" reads: /user/usage, how much of the week's allowance
// is left (percent remaining, and the day it resets; no reset date is no
// plan), and /user/xiaomi/subscription/self, the plan in force and when
// it ends. An account with no plan is on MiMo's free offer.
import (
"context"
"errors"
"strings"
"time"
)
// mimoTiers are the app's names for its plans' tiers.
var mimoTiers = map[int]string{1: "Starter", 2: "Plus", 3: "Pro", 4: "Ultra"}
// mimoZone is the MiMo server's clock: its times carry no zone, and the
// app shows them as they come, which is Beijing's (and Singapore's) time.
var mimoZone = time.FixedZone("UTC+8", 8*60*60)
type mimoSub struct {
PlanCode string `json:"planCode"`
Title string `json:"title"`
PlanTier int `json:"planTier"`
Status string `json:"status"`
RenewalMode string `json:"renewalMode"` // MONTHLY, YEARLY, ONE_TIME
EndTime string `json:"endTime"`
Percent float64 `json:"percent"`
NextReset string `json:"nextResetTime"`
Source string `json:"source"` // ORDER_SUB, INVITE
}
// mimoTime reads one of the server's times: "2026-10-01T00:00:00", or a
// day alone.
func mimoTime(s string) (time.Time, bool) {
s = strings.TrimSpace(s)
for _, layout := range []string{time.RFC3339, "2006-01-02T15:04:05", "2006-01-02 15:04:05", "2006-01-02"} {
if t, err := time.ParseInLocation(layout, s, mimoZone); err == nil {
return t, true
}
}
return time.Time{}, false
}
// mimoPlan is the plan an account is on: its name ("Free" with none) and
// what the server says of it (nil with none).
func mimoPlan(ctx context.Context, user string) (string, *mimoSub, error) {
var self struct {
Current *mimoSub `json:"current"`
}
if err := mimoGet(ctx, user, "/user/xiaomi/subscription/self", &self); err != nil {
return "", nil, err
}
c := self.Current
if c == nil {
return "Free", nil, nil
}
return firstNonEmpty(c.Title, mimoTiers[c.PlanTier], c.PlanCode, "MiMo"), c, nil
}
func mimoLoginQuota(ctx context.Context, l Login) SubscriptionQuota {
q := SubscriptionQuota{Provider: MiMoID, Name: "Xiaomi MiMo", Icon: "mimocode", Plan: l.Plan, User: l.User, Windows: []QuotaWindow{}}
ctx, cancel := context.WithTimeout(ctx, 20*time.Second)
defer cancel()
plan, sub, err := mimoPlan(ctx, l.User)
if err != nil {
q.Error = err.Error()
return q
}
q.Plan = plan
if plan != l.Plan {
_ = mimoEdit(l.User, func(_ *mimoCreds, s *savedLogin) { s.Plan = plan })
}
if sub != nil {
if at, ok := mimoTime(sub.EndTime); ok {
q.Until = &at
}
switch sub.RenewalMode {
case "MONTHLY", "YEARLY":
q.Renew = "auto"
case "ONE_TIME":
q.Renew = "off"
}
}
var use struct {
Percent *float64 `json:"percent"`
ResetDate *string `json:"resetDate"`
}
if err := mimoGet(ctx, l.User, "/user/usage", &use); err != nil {
if errors.Is(err, ErrMiMoSignIn) {
q.Error = err.Error()
}
// the plan alone, when the week's allowance can't be read
return q
}
if use.Percent == nil || use.ResetDate == nil {
return q // no plan: the free offer shows no allowance
}
w := QuotaWindow{Name: "7 days", Used: max(0, min(100, 100-*use.Percent)), Span: 7 * 24 * time.Hour}
if at, ok := mimoTime(*use.ResetDate); ok {
w.ResetsAt = &at
}
q.Windows = append(q.Windows, w)
return q
}