Files
yetone fa9daa6945 docs: every built-in subscription a plugin serves once moved says so in its code, and AGENTS.md says where a fix for it goes (user: 对内置的订阅 provider 在代码层面和文档层面加上注释,防止又有别的 agent 在改内置订阅 provider 的代码而不知道生效的其实是对应的插件代码) — a moved Cursor, Devin, Grok, Command Code plan, Kiro, Zed, Qoder, Factory, Xiaomi MiMo, ZCode or WorkBuddy (provider.Moved; since v0.1.642 also the default for a new sign-in, provider.Adopt) is signed in, listed, requested and metered by its @magpie-community plugin, and none of the built-in's code in internal/provider/<name>*.go, internal/gateway/<name>.go, internal/zed or internal/qoder serves it; a fix made there alone never reaches those users. Each of those files now opens with a PLUGIN-SERVED notice naming the subscription's id, its plugin package, where the plugin lives (magpie-community/plugins, packages/<name>) and the mover whose min to raise so moved users get the fixed plugin (keepMovedCurrent); Qoder's adds that Qoder CN isn't moved and still runs there. AGENTS.md (CLAUDE.md imports it) lists the moved built-ins with their packages and movers, how to tell whether a user's is moved (move.state "plugin", onPlugins), the order a fix goes in (the plugin, publish, raise min, the built-in only for those not moved), the magpie code that does run for a moved one (internal/plugin, plugins.go, plugin_usage.go, pluginsignin.go, migrate*.go, app.js's subOf/pluginSubs/startPluginSignIn), and the parity tests. movers in migrate.go and SUBS in app.js point there too. TestMovedBuiltinsSayTheirPlugin fails for a mover whose code carries no PLUGIN-SERVED notice naming its id and package (it failed while a wrapped package name was split). Comments and docs only, no behaviour changed. go vet ./internal/..., go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui -count=1 ./... under a temp HOME pass (TestProvidersAnswerWhileListsComeIn and TestClaudeSignedOut failed once on their timing under load, pass alone; untouched).
2026-10-02 19:37:02 +08:00

118 lines
3.9 KiB
Go

package provider
// PLUGIN-SERVED (see AGENTS.md): Xiaomi MiMo ("mimo-app") is a deprecated
// built-in subscription served by its plugin,
// @magpie-community/opencode-mimo-auth, once moved onto it (provider.Moved;
// the default for a new sign-in). A moved one's sign-ins, models, requests
// and usage are all the plugin's, never this code's (only the move, in
// migrate*.go, still reads its accounts). A fix here alone doesn't reach
// those users; fix the plugin (github.com/magpie-community/plugins,
// packages/mimo) and raise the mover's min in
// internal/provider/migrate_mimo.go.
// 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
}