magpie-plugin-msmoli 0.1.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (5) hide show
  1. package/README.md +586 -331
  2. package/bin/sync.mjs +1569 -148
  3. package/index.mjs +2492 -261
  4. package/package.json +3 -4
  5. package/README.zh-CN.md +0 -340
package/README.md CHANGED
@@ -1,12 +1,74 @@
1
+ <div align="center">
2
+
1
3
  # magpie-plugin-msmoli
2
4
 
3
- A [magpie](https://github.com/yetone/magpie) plugin that claims ModelScope's
4
- (魔搭) daily **魔粒** credits for you — one check-in a day instead of opening the
5
- site — and shows the balance and today's spend on the account's usage card.
5
+ **ModelScope daily credits, claimed for you every day.**
6
+
7
+ A [magpie](https://github.com/yetone/magpie) plugin that checks in to
8
+ ModelScope once a day and puts the balance and today's spend on the
9
+ account's usage card. Works with both **modelscope.cn** and **modelscope.ai**.
10
+
11
+ [![npm](https://img.shields.io/npm/v/magpie-plugin-msmoli?color=cb3837&logo=npm)](https://www.npmjs.com/package/magpie-plugin-msmoli)
12
+ [![tests](https://github.com/MisonL/magpie-plugin-msmoli/actions/workflows/test.yml/badge.svg)](https://github.com/MisonL/magpie-plugin-msmoli/actions/workflows/test.yml)
13
+ [![license](https://img.shields.io/npm/l/magpie-plugin-msmoli?color=blue)](LICENSE)
14
+
15
+ </div>
16
+
17
+ > [!WARNING]
18
+ > ModelScope has no check-in API. This plugin earns the daily grant by
19
+ > making a website sign-in happen, which ModelScope's terms may treat as
20
+ > abuse. **Read [Purpose, responsibility, and risk](#purpose-responsibility-and-risk)
21
+ > before you install it.**
22
+
23
+ ```
24
+ ● msmoli API key …a1b2 · 魔搭社区 魔粒 1% (今日用 2 / 领 250 · 余额 249.5) ↻ tomorrow 09:31
25
+ ● msmoli Mison · 魔搭社区(国际版) 魔粒 0% (今日用 0 / 领 300 · 余额 300) ↻ tomorrow 10:00
26
+ ```
27
+
28
+ Before the day's grant lands, the line ends with when it will:
29
+ `今日用 0 / 领 0 · 余额 300 · 09:56 领取(北京时间)`.
30
+
31
+ ## Contents
32
+
33
+ - [Features](#features)
34
+ - [Quick start](#quick-start)
35
+ - [What credits are](#what-credits-are)
36
+ - [Purpose, responsibility, and risk](#purpose-responsibility-and-risk)
37
+ - [Install](#install)
38
+ - [Accounts and sites](#accounts-and-sites)
39
+ - [How the check-in works](#how-the-check-in-works)
40
+ - [Why its own provider, with no models](#why-its-own-provider-with-no-models)
41
+ - [Privacy](#privacy)
42
+ - [Scope and limits](#scope-and-limits)
43
+ - [Troubleshooting](#troubleshooting)
44
+
45
+ ## Features
6
46
 
7
- ## What 魔粒 is
47
+ | | |
48
+ |---|---|
49
+ | 🗓️ **Daily check-in** | Claims the 200 + 50 daily grant once per Beijing day, per account |
50
+ | 📊 **Usage card** | Balance, today's spend and earnings, and when the soonest credits expire |
51
+ | 🌏 **Two sites, one card** | modelscope.cn and modelscope.ai accounts side by side; each token's site is found automatically |
52
+ | ⏰ **Check-in window** | Claims at an account's own minute in a configurable window (default 08:30–10:00) |
53
+ | 🔒 **Token stays put** | Lives only in magpie's `plugin-auth.json`; masked in every message the plugin or its CLI prints |
54
+ | 🔁 **Sync CLI** | Copies the key you already gave magpie's ModelScope preset into the plugin |
55
+
56
+ ## Quick start
57
+
58
+ ```bash
59
+ magpie plugin add magpie-plugin-msmoli
60
+ printf 'ms-your-token\n' | magpie plugin login msmoli
61
+ ```
8
62
 
9
- ModelScope's API-Inference is free, paid in 魔粒:
63
+ Then turn the check-in on: **Settings → Usage → Plugins → `msmoli`**.
64
+
65
+ Get the token from your site's access-token page:
66
+ [modelscope.cn](https://modelscope.cn/my/myaccesstoken) ·
67
+ [modelscope.ai](https://modelscope.ai/my/myaccesstoken).
68
+
69
+ ## What credits are
70
+
71
+ ModelScope's API-Inference is free, paid in credits:
10
72
 
11
73
  | Grant | Amount | Expires |
12
74
  |---|---|---|
@@ -14,170 +76,346 @@ ModelScope's API-Inference is free, paid in 魔粒:
14
76
  | Aliyun account bound, per day | 50 | 24 hours |
15
77
  | Email / profile / homepage completed | 50 each | 90 days, once |
16
78
 
17
- Daily grants are **24 hours only**. They do not accumulate: what you don't
18
- spend today is gone. A flagship model costs 2 魔粒 per call (mainstream 1,
79
+ Daily grants last **24 hours only** and do not accumulate: what you don't
80
+ spend today is gone. A flagship model costs 2 credits per call (mainstream 1,
19
81
  lightweight 0.5), so the daily 250 is roughly 125 calls.
20
82
 
83
+ The day's numbers come from the ledger, which a busy account can spread over
84
+ several pages. When a page cannot be read (a 5xx, a budget already gone), the
85
+ card does not present a floor as a fact: its line ends with
86
+ `账单未读完,数字偏低`. A check-in in that state does not read "no grant" into
87
+ a ledger it could not finish either — it answers `failed` and the host retries
88
+ in half an hour.
89
+
90
+ ## Purpose, responsibility, and risk
91
+
92
+ **What it is for.** A personal utility: it claims the daily credit grant of the
93
+ ModelScope account *you* sign in, and does nothing else. It is not an
94
+ official ModelScope product; it is not affiliated with ModelScope, Alibaba or
95
+ magpie, and it collects nothing about you. The token stays in magpie's own
96
+ `plugin-auth.json`, and every request goes to the site the account signed in
97
+ to.
98
+
99
+ **The risk you are taking.** ModelScope has no check-in endpoint. The daily
100
+ grant exists for a *real* website sign-in, and this plugin obtains it by
101
+ calling `login/info` once a day with the browser's session cookie: a script
102
+ posing as the browser, not a button the official client offers. ModelScope's
103
+ user agreement forbids plugins that interfere with the service (4.1.2(1)) and
104
+ point farming with third-party tools (4.1.2(5)), and lets the platform freeze a
105
+ user's benefits or blacklist them for abuse (2.3.1 and the credit rules). An
106
+ account is real-name verified and often tied to Aliyun, so what is at stake is
107
+ the account, not the 250 credits that expire the same day. No account has been
108
+ reported frozen for running this, and no one can promise one never will be.
109
+ Whether to run it is a decision about your own account.
110
+
111
+ **Responsibility.** You are responsible for complying with ModelScope's terms
112
+ when you use this. The software is provided under the MIT licence, without
113
+ warranty of any kind; the maintainer is not liable for a suspended or frozen
114
+ account, lost credits, or anything else that follows from running it. See
115
+ [LICENSE](LICENSE).
116
+
117
+ **Not in the magpie market, and it will not be submitted.** magpie's community
118
+ market declined to list this package for exactly the reason above
119
+ ([plugins#714](https://github.com/magpie-community/plugins/pull/714)), and the
120
+ project has decided not to submit it again. It stays installable by name from
121
+ npm.
122
+
21
123
  ## Install
22
124
 
23
- Needs magpie itself (<https://usemagpie.ai>). On macOS, use `magpie --version` to check it's on PATH; if not, use the app's full path:
125
+ You need magpie itself (<https://usemagpie.ai>). On macOS, `magpie --version`
126
+ tells you whether it is on `PATH`; if not, use the app's full path,
24
127
  `/Applications/magpie.app/Contents/MacOS/magpie`.
25
128
 
26
- ### Install via GitHub URL (no clone, auto-updates)
129
+ The plugin relies on the host's plugin API — the account scope a check-in and
130
+ a usage read run in, the provider argument `auth.checkin` is handed, the
131
+ `input.directory` the plugin's own folder comes from, and `auth.set` writing
132
+ back into the same account. It is checked against **magpie 0.1.3354**; an older
133
+ host may lack part of that contract.
134
+
135
+ Pick one of three ways. They all end the same: an `msmoli` row under
136
+ **Settings → Usage → Plugins**. The sign-in is kept per provider id, not per
137
+ folder, so switching between them (or moving a clone) keeps the token.
27
138
 
28
- This installs into magpie's own plugin folder (`~/.config/magpie/plugins` for a regular install, or `data/plugins` for the portable version), so it updates with `magpie plugin update`.
139
+ <table>
140
+ <tr><th>From npm (recommended)</th><th>From GitHub</th><th>From a clone</th></tr>
141
+ <tr valign="top">
142
+ <td>
29
143
 
30
144
  ```bash
31
- magpie plugin add github:MisonL/magpie-plugin-msmoli
32
- printf 'ms-your-token\n' | magpie plugin login msmoli
145
+ magpie plugin add \
146
+ magpie-plugin-msmoli
33
147
  ```
34
148
 
35
- The token is stored in `plugin-auth.json`; one login works regardless of the form you used to install.
149
+ `magpie plugin update` follows npm releases.
36
150
 
37
- ### Install from a clone
151
+ </td>
152
+ <td>
38
153
 
39
- Keeps your own copy, which is what you want if you plan to change it. magpie
40
- references the folder where it lies rather than copying it, so it loads only
41
- while that folder is there, and an update means pulling and adding it again.
154
+ ```bash
155
+ magpie plugin add \
156
+ github:MisonL/magpie-plugin-msmoli
157
+ ```
158
+
159
+ Installed into magpie's plugin folder; updates with `magpie plugin update`.
160
+
161
+ </td>
162
+ <td>
42
163
 
43
164
  ```bash
44
165
  git clone https://github.com/MisonL/magpie-plugin-msmoli.git
45
166
  cd magpie-plugin-msmoli
46
167
  magpie plugin add "$PWD"
47
- printf 'ms-your-token\n' | magpie plugin login msmoli
48
168
  ```
49
169
 
50
- Either form ends the same way: a `msmoli` row under Settings → Usage → Plugins.
51
- The sign-in is kept per provider id (`msmoli`) rather than per folder, so
52
- switching between the two forms (or moving the clone) keeps the token — no
53
- second login.
54
-
55
- ### Already have ModelScope set up in magpie? Sync that key to the plugin
170
+ Referenced in place: it loads only while the folder exists. Update by pulling.
56
171
 
57
- If you added ModelScope earlier just for chat, its key is already in
58
- `providers.json`. This package ships a small CLI that logs the same token into
59
- the plugin, so you don't have to find it again.
172
+ </td>
173
+ </tr>
174
+ </table>
60
175
 
61
- Installed from GitHub, magpie keeps its own copy in its plugin folder:
176
+ Then sign in and turn the check-in on:
62
177
 
63
178
  ```bash
64
- node ~/.config/magpie/plugins/node_modules/magpie-plugin-msmoli/bin/sync.mjs --status # see both sides
65
- node ~/.config/magpie/plugins/node_modules/magpie-plugin-msmoli/bin/sync.mjs --to-plugin # sync
179
+ printf 'ms-your-token\n' | magpie plugin login msmoli
66
180
  ```
67
181
 
68
- Installed from a clone, run the file in the clone — magpie references that
69
- folder and copies nothing (`./bin/sync.mjs`; on Windows or WSL, where the
70
- file may not carry an executable bit, run it as `node bin/sync.mjs`). Portable
71
- installs keep the same layout under their `data` folder; if detection misses,
72
- `--dir <magpie's config folder>` and `--magpie <path to the magpie binary>` say
73
- where things are: for a portable install that is its `data` folder, not the
74
- folder the executable sits in. `--magpie` takes a path, absolute or relative —
75
- a bare `magpie` is refused, because that one is resolved through `PATH` and the
76
- first `magpie` on it would be handed the token. `npx magpie-plugin-msmoli` will
77
- work once the package is on npm — npm 12 blocks `npx github:…` by default, so
78
- don't start from that.
79
-
80
- `--dir` says where to *read*; magpie's CLI has no such flag, so a write always
81
- lands in the binary's own folder. Given both and they disagree, the tool stops
82
- before writing anything rather than reading one magpie and writing another.
83
- "Disagree" means the folders are different places, not that the strings differ:
84
- a shell completes a directory with a trailing slash, `/private/tmp` and `/tmp`
85
- are one folder on a Mac, and `data/.` is `data` — each names the binary's own
86
- folder and is allowed.
87
-
88
- It reads magpie's config but never writes it: the writes are magpie's own
89
- (`magpie plugin login`, `magpie plugin logout`, `magpie provider key`), and the
90
- token is masked in everything it prints, failures included. When the plugin
91
- already uses the same key it changes nothing; when the two keys differ it
92
- refuses, because a second `login` would add a second account — pass `--force`
93
- to replace a single existing account. `--to-preset` goes the other way (needs
94
- `--force`: that key is what chat uses). If more than one provider could be the
95
- built-in side (say, a relay of the same host), the tool refuses to guess and
96
- `--from <id>` picks one; `--dry-run` prints the commands without running them.
97
- A value flag given twice (`--dir a --dir b`) is refused, not silently last-wins,
98
- and `--dir=<folder>` is read as well. `--from` and a stray argument are echoed
99
- back masked when the value looks like a key — a token pasted into the wrong
100
- position is never printed whole, whatever its shape. That covers the shapes
101
- `ms-…` does not: a GitHub or Slack token, an AWS secret (which carries `/`), a
102
- UUID, a hex digest, a base64 body — long, or short enough that only its trailing
103
- `=` (or a `+` beside a case mix) gives it away. The judgement runs
104
- word by word,
105
- so a key with a space or a newline pasted onto it — the shape a copy out of a
106
- terminal or a chat window leaves — or quoted inside a line is still the key it
107
- is. A word that is one key is masked whole, ends and all, including one split with
108
- separators — a hex digest with dashes, a key written as `AbCdEfGh.IjKlMnOp…` —
109
- and an AWS secret, whose two parts of 12+ characters with mixed case mark it. An
110
- invisible character dropped inside a key (a zero-width space or `\x01`, the
111
- shape a copy out of a chat window leaves) does not save it either: the
112
- control/format characters sitting between key-alphabet characters are removed
113
- before masking, so the two halves are rejoined into the one key they are —
114
- each half alone falls under the length floor and would have leaked. A
115
- word that starts like a path (`/`, `~`, `.`, a drive letter, a UNC root) keeps
116
- its shape, and only the key-like run inside it is masked, so `--dir
117
- /tmp/ghp_16C7…` still names the folder it read; a key pasted in *behind* a path
118
- prefix (`~/keys/sk-ant-…`) reads as part of that path, which is what keeping a
119
- path's shape costs. The folder is reported
120
- the same way on every line that names it, and magpie's own output goes through
121
- the same door — a key of another provider in the child's error message is
122
- masked there too, not only the `ms-` shape. A provider id and an account name
123
- are read out of magpie's own files and go through the same door: a key pasted
124
- into either field is masked on every line the tool names it.
125
-
126
- Turn the check-in on: Settings → Usage → Plugins → the `msmoli` row, or in
127
- `settings.json` (in magpie's own folder, `~/.config/magpie` for an installed
128
- magpie):
182
+ The switch is **Settings → Usage → Plugins → `msmoli`**, or `settings.json` in
183
+ magpie's own folder (`~/.config/magpie` for an installed magpie):
129
184
 
130
185
  ```json
131
- {
132
- "pluginCheckins": {
133
- "msmoli": true
134
- }
135
- }
186
+ { "pluginCheckins": { "msmoli": true } }
187
+ ```
188
+
189
+ One more optional step. The Providers row reads **未暴露模型 / no models
190
+ exposed** — accurate (the plugin registers no models) but it reads like
191
+ something failed. Say it serves none on purpose, and the row reads **仅看用量
192
+ / usage only**:
193
+
194
+ ```bash
195
+ magpie provider models msmoli none
136
196
  ```
137
197
 
138
- The token comes from <https://modelscope.cn/my/myaccesstoken>. The same `ms-`
139
- token is both the inference `Authorization: Bearer` and the web session cookie,
140
- so nothing else needs configuring.
198
+ The same `ms-` token is both the inference `Authorization: Bearer` and the web
199
+ session cookie, so nothing else needs configuring.
200
+
201
+ ### Already use ModelScope for chat? Sync that key
141
202
 
142
- From then on magpie claims it once a Beijing day per signed-in account, retries
143
- later that day if it fails, and reports the outcome on the account's usage card:
203
+ If you added ModelScope to magpie earlier for chat, its key is already in
204
+ `providers.json`. The bundled CLI logs the same token into the plugin:
144
205
 
206
+ ```bash
207
+ npx magpie-plugin-msmoli --status # see both sides, change nothing
208
+ npx magpie-plugin-msmoli --to-plugin # copy the preset's key into the plugin
145
209
  ```
146
- ● msmoli API key …a1b2 · 魔搭社区 魔粒 1% (今日用 2 / 领 250 · 余额 249.5) ↻14h24m tomorrow 09:31 249.5 魔粒可用 left
210
+
211
+ From a GitHub install, run magpie's own copy instead:
212
+ `node ~/.config/magpie/plugins/node_modules/magpie-plugin-msmoli/bin/sync.mjs`.
213
+ From a clone, run `./bin/sync.mjs` (or `node bin/sync.mjs` on Windows and
214
+ WSL). npm 12 blocks `npx github:…` by default, so don't start from that.
215
+
216
+ `npx`, `magpie plugin add magpie-plugin-msmoli` and `magpie plugin update` all
217
+ follow the npm package. The current release is **0.3.0**: one provider for
218
+ both sites, the row-level ledger dedupe, the chunked-echo masking, and the
219
+ budgets this document describes.
220
+
221
+ If you are on **0.2.0**, that version ships an older `bin/sync.mjs` with both
222
+ directions, but it keeps the international site in a second provider and
223
+ predates this round of fixes. Upgrade (`npm i -g magpie-plugin-msmoli@latest`
224
+ or `magpie plugin update msmoli`) to get what this document describes.
225
+
226
+ It syncs the **domestic site only**: the built-in preset it reads is the
227
+ `modelscope.cn` one. Sign an international account in by hand.
228
+
229
+ | Flag | What it does |
230
+ |---|---|
231
+ | `--status` | Report both sides and the plan; write nothing |
232
+ | `--to-plugin` | Log the preset's key into the plugin |
233
+ | `--to-preset` | The other way round; a preset that already holds a different key needs `--force` |
234
+ | `--force` | Replace a single existing plugin account whose key differs |
235
+ | `--from <id>` | Pick the preset when more than one provider could be it |
236
+ | `--dry-run` | Print the commands, masked, without running them or writing anything; no magpie binary needed |
237
+ | `--dir <folder>` | Where magpie's config is (a portable install's `data` folder) |
238
+ | `--magpie <path>` | Path to the magpie binary; a bare `magpie` is refused |
239
+ | `--version` | Print the version (read from the package's own `package.json`) |
240
+
241
+ <details>
242
+ <summary><b>How the CLI keeps your config and your token safe</b></summary>
243
+
244
+ - **It never writes magpie's files itself.** It reads them directly, and every
245
+ write goes through magpie's own CLI (`magpie plugin login`,
246
+ `magpie plugin logout`, `magpie provider key`).
247
+ - **A preset with no key is filled without `--force`, in either direction.**
248
+ There is nothing to overwrite; `--force` is for replacing a different key
249
+ that chat may be using.
250
+ - **It never adds a second account by accident.** Same key on both sides: it
251
+ changes nothing. Different keys: it refuses, because a second `login` would
252
+ add an account. `--force` replaces only when exactly one account exists.
253
+ - **It reads and writes the same magpie.** `--dir` says where to read; magpie's
254
+ CLI has no such flag, so a write always lands in the binary's own folder. If
255
+ `--dir` and `--magpie` name different places, it stops before writing.
256
+ "Different" means different folders, not different strings: a trailing
257
+ slash, `/private/tmp` vs `/tmp` on a Mac, and `data/.` all count as the same.
258
+ - **`--magpie` must be a path.** A bare `magpie` resolves through `PATH`, and
259
+ the first `magpie` on it would be handed the token.
260
+ - **A value flag given twice is refused**, not silently last-wins.
261
+ `--dir=<folder>` is read as well.
262
+ - **The token is masked in everything it prints, failures included.** That
263
+ covers magpie's own output, and keys of other shapes too: GitHub and Slack
264
+ tokens, AWS secrets, UUIDs, hex digests, base64 bodies. A key with a space,
265
+ newline or invisible character pasted into it is still masked whole, and so
266
+ is one printed another way: upper-cased, in full-width characters, or broken
267
+ into short groups (`ms-01234 56789abc …`), and so is a vendor key cut the
268
+ same way (`ghp_ 1234 AbCd 5678 …`, `sk-a nt-a pi03 …`, one character per
269
+ word, `AKIAIOSF ODNN7EXA MPLE`) — the words are judged as one key joined
270
+ back up. A value that starts like a path keeps its shape, and only the
271
+ key-like part inside it is masked, so `--dir /tmp/ghp_16C7…` still names the
272
+ folder. What the masking cannot know: a key typed into an argument the CLI
273
+ does not know, inside a path segment (`/keys/sk-ant-…`), and a run of words
274
+ that reads as prose as well as as a key.
275
+
276
+ </details>
277
+
278
+ ## Accounts and sites
279
+
280
+ ### The international site (modelscope.ai)
281
+
282
+ There is one provider, `msmoli`, and it serves both sites. Tokens are
283
+ site-specific (a `modelscope.cn` token answers 401 on `modelscope.ai`, and the
284
+ other way round), so the plugin works out each account's site from its token:
285
+
286
+ 1. The first read goes to the domestic site.
287
+ 2. A 401 sends it on to the international one, and so does a failure that is
288
+ not an answer at all (a transport error, a proxy's page, a 5xx, a JSON 429):
289
+ those say nothing about which site holds the token. The same is true of a
290
+ *remembered* site that times out or answers with nothing usable (a 4xx as
291
+ well as a 5xx): it is not the only site tried, the other one is asked
292
+ within the same request budget. When nothing else answers, the first
293
+ answer is the one reported.
294
+ 3. The answer is remembered for the rest of the process (the last 64 tokens
295
+ seen, oldest evicted first), and an international
296
+ one is also kept on the sign-in (`site`), so a restarted host goes straight
297
+ there instead of paying the 401 again. A 401 from a remembered site drops
298
+ it and the *other* site is asked — the sign-in may have been re-keyed with
299
+ a token of the other site, and the site that just refused the token is not
300
+ worth a second request.
301
+
302
+ The card's plan line tells you which site an account is on (`魔搭社区` or
303
+ `魔搭社区(国际版)`), and a 401 names the site that refused (or 任一站点 when
304
+ both did).
305
+
306
+ To add an international account, sign in again with that site's token. It
307
+ lands under the same `msmoli` card:
308
+
309
+ ```bash
310
+ printf 'ms-your-intl-token\n' | magpie plugin login msmoli
311
+ magpie accounts # both accounts, each with its own credit window
147
312
  ```
148
313
 
314
+ If you are coming from 0.2.0, which kept the international site in a second
315
+ provider called `msmoli-intl`: that provider is gone, and the sign-in filed
316
+ under it is not read by this version. Sign in once more under `msmoli` with
317
+ the same token (the command above), and the card picks the account up. Aliases
318
+ set on the old `msmoli-intl` row belong to a provider this version no longer
319
+ has.
320
+
321
+ ### More than one account
322
+
323
+ The plugin works per account, not per token. magpie keeps each signed-in token
324
+ under its own key (the first as `msmoli`, later ones as `msmoli#xxxxxx`) and
325
+ asks the check-in once per account. With two tokens:
326
+
327
+ - each account gets **its own target minute** that day (hashed from the day
328
+ and that account's token), so they normally claim at different minutes;
329
+ - each account's check-in is recorded, retried and shown separately;
330
+ - the name read at check-in is cached per token, so one account's nickname
331
+ never appears on another's row.
332
+
333
+ Three things worth knowing once there are two:
334
+
335
+ - Signing the **same token in twice** can leave two account rows: magpie folds
336
+ a repeated sign-in into the first row only while neither row carries a name
337
+ (host.js's `settle`), so once the nickname is there the second sign-in is a
338
+ row of its own. Both are asked; the day's grant lands once, and the second
339
+ answers `done`. Remove one in the editor to quiet it.
340
+ - Dropping **one account** of several: **Remove** on its row in the provider's
341
+ editor, or `magpie accounts forget msmoli "<the account as its card shows
342
+ it>"`. `magpie plugin logout msmoli` is the blunt one — it takes no account
343
+ name, so it forgets the provider's whole sign-in.
344
+ - An **alias** is kept under the account's label of the moment (see [Account
345
+ names](#account-names)). Remove an account and sign in again and the label
346
+ resets to the key's tail, so an alias you set under the nickname re-attaches
347
+ only once the next check-in writes the nickname back — or just set it again.
348
+
349
+ The check-in switch, the card and the window (`msmoli.json`) are per provider,
350
+ so one setting covers every account, whichever site it is from.
351
+
352
+ The daily grant belongs to the ModelScope account, not to the key: a second
353
+ token for the same ModelScope user gets `done` rather than another 250.
354
+
355
+ ### Account names
356
+
357
+ The card names an account by its ModelScope nickname. An api-type sign-in has
358
+ no name, so the first check-in that runs writes the nickname back as
359
+ `accountId` on the same sign-in (the token is still stored once). Until then a
360
+ fresh install shows `API key …a1b2`. A day already settled is not re-claimed,
361
+ but the name is still worth a `login/info` read, so the nickname appears on
362
+ the first ask after sign-in rather than with the next day's claim.
363
+
364
+ To choose the name yourself, magpie's own alias wins:
365
+
366
+ ```bash
367
+ magpie accounts alias msmoli "API key …a1b2" my-main
368
+ ```
369
+
370
+ The second argument is the account as the card shows it *now*: the key's tail
371
+ before the first check-in, the nickname after. An account you have named is
372
+ left alone; the check-in never writes over your alias.
373
+
149
374
  ## How the check-in works
150
375
 
151
376
  ModelScope publishes no check-in endpoint. What it does have is a side effect:
152
377
  `GET /api/v1/users/login/info` counts as a sign-in, and the first one of the
153
- day grants `daily_active` 200 and `aliyun_bindlogin` 50. Repeat it the same day
154
- and nothing more is granted.
378
+ day grants `daily_active` 200 and `aliyun_bindlogin` 50. Repeating it the same
379
+ day grants nothing more.
155
380
 
156
381
  A hook that just calls it can't tell "claimed now" from "claimed hours ago", so
157
- this one reads the day's ledger around it:
158
-
159
- 1. read today's `EARN` records — is `daily_active` there?
160
- 2. touch `login/info`
161
- 3. read the ledger again
162
-
163
- A `claimed` needs a grant the ledger did not already show around the touch. If
164
- the touch went through and the ledger still shows nothing, the plugin asks
165
- magpie to look again half an hour later (`until`), once — from Beijing noon on
166
- magpie would otherwise settle `inactive` for the day, and a grant written a few
167
- seconds late was recorded as "the account may not be in the campaign". Before
168
- noon magpie's own provisional re-ask (every 30 minutes) already covers it.
169
-
170
- Four outcomes follow:
382
+ this one reads the ledger around it:
383
+
384
+ ```mermaid
385
+ flowchart LR
386
+ A[Read today's EARN rows] -->|daily_active already there| D[done]
387
+ A -->|not yet| B[Touch login/info]
388
+ B --> C[Read the ledger again]
389
+ C -->|new grant| E[claimed]
390
+ C -->|nothing yet| F[look again in 30 min]
391
+ ```
171
392
 
172
393
  | Outcome | Meaning |
173
394
  |---|---|
174
395
  | `done` | already claimed today, with the day's total |
175
- | `claimed` | this run claimed it, with the amount — a grant the ledger did not already show, so a row that was there before the touch is never counted as this run's |
176
- | `inactive` | the sign-in went through but nothing was granted (from noon on, asked once more in 30 minutes before it stands for the day) |
177
- | `failed` | nothing was claimed — including "not yet time", explained below |
178
-
179
- The endpoints it uses, all on `modelscope.cn` with `Cookie: m_session_id=ms-…`
180
- (a browser UA and a `Referer` are enough — no CSRF token):
396
+ | `claimed` | this run claimed it, with the amount; a row that was there before the touch is never counted as this run's |
397
+ | `inactive` | the sign-in went through but nothing was granted (from noon on, checked once more 30 minutes later before it stands for the day) |
398
+ | `failed` | nothing was claimed, including "not yet time" (see below) |
399
+
400
+ When the touch went through but the ledger still shows nothing, the plugin
401
+ asks magpie to look again half an hour later (`until`), once. Before Beijing
402
+ noon magpie's own 30-minute re-ask already covers it.
403
+
404
+ A `done` or `claimed` also carries the streak: how many Beijing days in a row
405
+ the ledger shows a daily grant, today included. magpie prints it as
406
+ `3-day streak` beside the outcome. The ledger's `total` tells the plugin
407
+ whether one page is the whole ledger; when it is not — or is missing, or the
408
+ rows contradict it — and the run is still going at the oldest row, the next
409
+ page is read, up to eight. A run it could not follow to its end — a page that
410
+ fails, or one longer than those eight pages — is left unsaid rather than
411
+ printed short, because magpie shows any number it is given.
412
+
413
+ <details>
414
+ <summary><b>Endpoints and the ledger</b></summary>
415
+
416
+ Every request goes to the account's own site (`modelscope.cn` or
417
+ `modelscope.ai`) with `Cookie: m_session_id=ms-…`. A browser UA and a
418
+ `Referer` are enough; no CSRF token.
181
419
 
182
420
  | Purpose | Path |
183
421
  |---|---|
@@ -185,249 +423,266 @@ The endpoints it uses, all on `modelscope.cn` with `Cookie: m_session_id=ms-…`
185
423
  | Balance | `GET /openapi/v1/magicubes/balance` |
186
424
  | Ledger | `GET /openapi/v1/magicubes/transactions?page=1&page_size=50` |
187
425
 
188
- `type` in the ledger has been seen as `EARN` and `SPEND_CONFIRM`; there is
189
- also an `EXPIRE`, which this plugin ignores. Rows are aggregated per day, type,
190
- `rule_key` and `model_tier` — a day's spending shows one row per model tier it
191
- used — with a `count` on each, and the list is sorted newest first. The ordering
192
- is what makes the single page of 50 enough: today's rows come before any older
193
- one. Measured on a live account that is 3–7 rows a day, so a page reaches back
194
- about a fortnight rather than 50 days; what the read would not survive is a
195
- single day with more than 50 rows. Asking for a larger page does not help —
196
- the server answers `page_size=200` with 50.
426
+ The ledger's `type` has been seen as `EARN` and `SPEND_CONFIRM`; there is also
427
+ an `EXPIRE`, which the plugin ignores. Rows are aggregated per day, type,
428
+ `rule_key` and `model_tier`, with a `count` on each, and sorted newest first.
429
+ The ordering is what makes the first page enough in the common case: today's
430
+ rows always come first. A live account shows 3–7 rows a day, so a page reaches
431
+ back about a fortnight. A single day with more than 50 rows is followed onto
432
+ the next page (up to `DAY_PAGES` extra pages), and a walk that cannot finish is
433
+ answered with the numbers it saw and marked `未读完`. Asking for a bigger page
434
+ doesn't help: the server answers `page_size=200` with 50.
435
+
436
+ </details>
197
437
 
198
438
  ### When it claims: the check-in window
199
439
 
200
- The claim is held back until a time of day, by default somewhere in
201
- **08:30–10:00 Beijing time** — an account's own minute in that window,
202
- picked from a hash of the day and the account, so it doesn't move between
203
- the host's retries. It makes the traffic look like a person opening the
204
- site in the morning rather than at the stroke of midnight.
440
+ The claim waits until a time of day, by default an account's own minute in
441
+ **08:30–10:00 Beijing time**, picked from a hash of the day and the account so
442
+ it doesn't move between retries. The traffic looks like a person opening the
443
+ site in the morning, not a script at midnight. The times this plugin itself
444
+ names say **北京时间** — the day ModelScope grants in is Beijing's wherever
445
+ magpie's own clock is — while times magpie adds around them (a window's reset)
446
+ are in the machine's zone.
205
447
 
206
- Waiting costs nothing: daily grants **expire 24 hours after they land**
207
- (a grant at 15:35 expires at 15:35 the next day), so claiming at 9am leaves
208
- the credits good until 9am tomorrow.
448
+ Waiting costs nothing: daily grants expire **24 hours after they land**, so
449
+ claiming at 9am leaves the credits good until 9am tomorrow.
209
450
 
210
- Set it in `msmoli.json` in magpie's own folder (`~/.config/magpie` for an
211
- installed magpie, the data folder for a portable one):
451
+ Set the window in `msmoli.json` in magpie's own folder (`~/.config/magpie` for
452
+ an installed magpie, the data folder for a portable one). The file is read on
453
+ every check-in ask (and by a usage read only while the day has no credit yet
454
+ to show), so changes apply without restarting magpie.
212
455
 
213
456
  ```json
214
457
  { "checkinWindow": ["08:30", "10:00"] }
215
458
  ```
216
459
 
217
- One string works the same way: `{ "checkinWindow": "08:30-10:00" }`.
218
-
219
- `false` turns the window off (claim whenever magpie asks); so does `"off"`.
220
- An unusable value — `08:30`–`08:00`, a date that isn't a clock time — falls
221
- back to the default rather than never claiming. An end past **23:00** is
222
- capped there: the host does not ask at the target's minute — its own look runs
223
- every 30 minutes on a phase of its own, so the first ask past a target can come
224
- up to half an hour late, and the retry after a failure half an hour after that.
225
- A target needs both to still land on the same day. Capping at 23:00 leaves any
226
- target that room; later would leave some with one ask, or none. A window that sits entirely past 23:00 falls back to the
227
- default window too, since there would be nowhere left to put the target. The
228
- file is read on every ask, so a change takes effect without restarting
229
- magpie.
230
-
231
- How it fits magpie's schedule: magpie asks once when the Beijing day turns,
232
- then every 30 minutes — always after a `failed`, and also after a
233
- `done`/`inactive`/`ineligible` it hasn't seen a claim for yet, up to noon
234
- Beijing (a provisional answer: the vendor's day may not have turned; from noon
235
- on the plugin's own `until` buys the same re-ask when the ledger lagged). So
236
- before the window's moment the plugin answers `failed` with the time it will
237
- run, and sends **no request at all**. `failed` is still the one outcome that
238
- guarantees a re-ask; the others only get one while the day is unproven.
460
+ | Value | Effect |
461
+ |---|---|
462
+ | `["08:30", "10:00"]` or `"08:30-10:00"` | claim inside that window |
463
+ | `false`, `"off"`, `"no"` | no window; claim whenever magpie asks |
464
+ | an unusable value (end before start, not a clock time) | falls back to the default |
465
+ | an end past **23:00** | capped at 23:00; a window entirely past it falls back to the default |
466
+
467
+ <details>
468
+ <summary><b>Why "failed" before the window, and why the 23:00 cap</b></summary>
469
+
470
+ magpie asks once when the Beijing day turns, then every 30 minutes: always
471
+ after a `failed`, and after a `done`/`inactive`/`ineligible` only until Beijing
472
+ noon. So before the window's moment the plugin answers `failed` with the time
473
+ it will run, and sends **no request at all**. `failed` is the one outcome that
474
+ guarantees a re-ask.
239
475
 
240
476
  What that costs, since a plugin has no other way to defer: until the claim
241
- actually runs the account card shows a red dot and magpie's own "check-in
242
- failed; magpie tries again later" wording, each of those asks rewrites
243
- magpie's `plugin-checkin.json`, and `magpie accounts checkin` run in that
244
- period exits non-zero. Turning the window off removes all three.
245
-
246
- How long that is depends on when magpie next asks, not on the target alone.
247
- Measured on a real day: the target was 09:01, magpie asked at 09:00:57 (three
248
- seconds early, so `failed`), and the claim landed on the next ask at 09:31 —
249
- about 9.5 hours from midnight, rather than the 8.5 the window's 08:30 start
250
- alone suggests. That is also why the card names the cadence instead of a
251
- countdown: the host keeps the line until its next ask, half an hour later, so
252
- "约 8 分钟后" would be wrong for most of the time it is on screen.
477
+ runs, the card shows a red dot and magpie's "check-in failed; tries again
478
+ later" wording, under the line
479
+ `09:56 后自动领取(北京时间,magpie 每半小时问一次,无需操作)`.
480
+ Each such ask rewrites `plugin-checkin.json`, and `magpie accounts checkin` run
481
+ in that period exits non-zero. Turning the window off removes all three.
482
+
483
+ **Check in now** (Settings → Usage → Plugins) does not claim early either: the
484
+ plugin cannot tell a manual press from the host's half-hourly ask — both reach
485
+ it through the same `auth.checkin` call — so before the target minute it
486
+ answers the same line. After the target, the button claims like any ask.
487
+
488
+ How long it lasts depends on when magpie next asks. On a measured day the
489
+ target was 09:01, magpie asked at 09:00:57 (three seconds early, so `failed`),
490
+ and the claim landed on the next ask at 09:31. That is also why the card names
491
+ the time and the cadence instead of a countdown: the line stays up for half an
492
+ hour, so a countdown ("in about 8 minutes") would be wrong for most of that time.
493
+
494
+ The cap: magpie's asks run on a 30-minute phase of their own, so the first ask
495
+ after a target can be up to half an hour late, and the retry after a failure
496
+ half an hour after that. Both have to land on the same day, or the day's
497
+ credits are lost. Ending the window at 23:00 leaves every target that room.
498
+
499
+ </details>
253
500
 
254
501
  ### Upstream
255
502
 
256
503
  magpie gives a plugin no timer, no clock, and no say in when it is called:
257
- `auth.checkin(getAuth, info)` returns an outcome and the host decides when
258
- to ask again. Everything above is worked around within that contract, and
259
- the awkward parts (proving "not yet" as a failure, whole-day granularity for
260
- the account card) want a line in
261
- [this issue](https://github.com/yetone/magpie/issues/3245) proposing timers and
262
- `recheck` for plugins.
263
-
264
- Two consequences worth knowing, both from the same missing timer: a window's
265
- target is only reached by the host's own asks, so one that falls while the
266
- machine is asleep is claimed on the next ask after it wakes (the day is lost
267
- only if that happens past midnight); and the plugin cannot retry on its own
268
- between asks — the window's cap (above) is what buys the retry.
269
-
270
- ### More than one account
271
-
272
- The plugin is written per account, not per token. magpie keeps each signed-in
273
- token under its own key — the first as `msmoli`, later ones as `msmoli#xxxxxx`
274
- — and asks the check-in once per account, handing each its own token through
275
- `getAuth`. So with two tokens:
276
-
277
- - each account gets **its own target minute** that day (the hash is over the
278
- day and that account's token), so the two normally claim at different
279
- minutes — a collision is possible, but only costs both a shared minute;
280
- - each account's check-in is recorded, retried and shown separately in
281
- `plugin-checkin.json` and on the card;
282
- - the name read at check-in is cached per token, so one account's nickname
283
- never appears on another's row.
284
-
285
- Add a second one the same way, then pick which to use:
286
-
287
- ```bash
288
- printf 'ms-the-second-token\n' | magpie plugin login msmoli
289
- magpie accounts # both rows, each with its own 魔粒 window
290
- ```
291
-
292
- The card names an account by the name ModelScope gives it. An api-type
293
- sign-in has no such name, so the check-in writes the nickname back: the same
294
- sign-in entry, the token still stored once, now with `accountId` beside it.
504
+ `auth.checkin(getAuth, info)` returns an outcome and the host decides when to
505
+ ask again. Everything above works within that contract.
506
+ [yetone/magpie#3245](https://github.com/yetone/magpie/issues/3245) proposes
507
+ timers and `recheck` for plugins.
295
508
 
296
- It happens on the first check-in that actually runs in the host process, so a
297
- fresh install shows `API key …a1b2` until then. An account whose check-in is
298
- already settled for the day is not asked again — magpie reuses the record — so
299
- the name appears with the next day's check-in rather than on upgrade.
300
-
301
- If you would rather choose the name yourself, magpie's own alias wins over it:
302
-
303
- ```bash
304
- magpie accounts alias msmoli "API key …a1b2" my-main
305
- ```
306
-
307
- The second word is the account as the card shows it *now*: before the first
308
- check-in that is the key's tail, afterwards the ModelScope nickname. An account
309
- you have named is left alone — the check-in won't write its nickname over your
310
- alias — so either label works, and the alias outranks the nickname from then on.
311
-
312
- And the daily grant belongs to the ModelScope account, not to the key: the
313
- claim is idempotent server-side per account per day, so a second token for the
314
- same ModelScope user gets `done` rather than another 250.
509
+ Two consequences of the missing timer: a target that falls while the machine
510
+ is asleep is claimed on the first ask after it wakes (the day is lost only if
511
+ that is past midnight), and the plugin cannot retry on its own between asks.
315
512
 
316
513
  ## Why its own provider, with no models
317
514
 
318
- The plugin registers `msmoli` rather than the built-in `modelscope` preset, for
319
- two reasons:
515
+ The plugin registers `msmoli` instead of riding on the built-in `modelscope`
516
+ preset:
320
517
 
321
- 1. magpie namespaces a plugin id that collides with a preset into
322
- `<id>-plugin`, so a plugin that named itself `modelscope` would not be the
323
- same provider anyway — it would only sit next to the preset and invite people
324
- to take one for the other.
518
+ 1. magpie renames a plugin id that collides with a preset to `<id>-plugin`, so
519
+ a plugin called `modelscope` would not be the same provider anyway.
325
520
  2. **Safety.** magpie's gateway passes over an account whose usage window reads
326
- as used up. If a check-in plugin served the provider that inference actually
327
- routes through, reporting 100% would take the whole chat path out of magpie.
328
-
329
- So chat keeps using the `modelscope` preset with your API key in
330
- `providers.json`, and this plugin only carries the sign-in its check-in runs as.
331
-
332
- ### Two ModelScope rows, and why that is fine
521
+ as used up. If a check-in plugin served the provider chat routes through,
522
+ reporting 100% would take the whole chat path out of magpie.
333
523
 
334
- The providers page then shows two: `ModelScope` (the preset, with your models)
335
- and `ModelScope 魔粒` (this plugin, with none). They are separate providers that
336
- happen to share one token, and they do not fight:
524
+ So chat keeps using the `modelscope` preset with your API key, and this plugin
525
+ only carries the sign-in its check-in runs as. The Providers page shows both:
337
526
 
338
- | | `modelscope` preset | `msmoli` (this plugin) |
527
+ | | `ModelScope` (preset) | `ModelScope 魔粒` (this plugin) |
339
528
  |---|---|---|
340
- | Serves chat models | yes | no — `magpie models` never lists it |
341
- | Its usage card | none (the preset has no balance endpoint) | 魔粒 balance and today's spend |
342
- | Check-in | not built in for ModelScope | this plugin, once a day |
343
- | Reachable by the gateway | yes | no, so a card reading 100% cannot route around it |
344
-
345
- The token being in both places is how magpie is built: a preset keeps its key
346
- in `providers.json`, a plugin keeps its sign-in in `plugin-auth.json`. Entering
347
- the same `ms-` token in both is expected. Nothing reads the plugin's usage card
348
- to decide where chat goes — that is the point of the separate id.
349
-
350
- The preset's empty card has one consequence worth knowing. ModelScope's
351
- inference API has no balance endpoint, so magpie cannot see that the key's 魔粒
352
- are gone; the gateway learns it from the vendor's own `429 insufficient balance`
353
- and then rests the key for 30 minutes. Until then each request spends one failed
354
- try on it first. The balance is on the account, and `modelscope.cn` does answer
355
- it, so the preset can be told: in its editor, Balance URL
356
- `https://modelscope.cn/openapi/v1/magicubes/balance` and Balance field
357
- `data.available_balance` (the same token answers it — `magpie quota modelscope`
358
- then reads a number instead of `no balance endpoint known for it`).
359
-
360
- The one real overlap to know about: **both rows cost nothing extra**, but if you
361
- remove the plugin's sign-in, chat is unaffected, and if you remove the preset,
362
- the check-in keeps working. They fail independently.
529
+ | Serves chat models | yes | no; `magpie models` never lists it |
530
+ | Usage card | none (no balance endpoint) | the credit balance and today's spend |
531
+ | Check-in | not built in | once a day |
532
+ | Used by the gateway | yes | no, so a card at 100% can't block chat |
533
+
534
+ Its row reads 未暴露模型 / no models exposed until told it serves none on
535
+ purpose; `magpie provider models msmoli none` (see [Install](#install)) makes
536
+ it read 仅看用量 / usage only. Either way nothing is served.
537
+
538
+ Entering the same `ms-` token in both is expected: a preset keeps its key in
539
+ `providers.json`, a plugin in `plugin-auth.json`. They fail independently;
540
+ remove either and the other keeps working. International accounts are extra
541
+ accounts under the same `msmoli` row, not new rows.
542
+
543
+ > [!TIP]
544
+ > The preset's card is empty because ModelScope's inference API has no balance
545
+ > endpoint, so the gateway only learns the credits are gone from the vendor's
546
+ > `429 insufficient balance`. You can give the preset a reading: in its editor,
547
+ > set Balance URL `https://modelscope.cn/openapi/v1/magicubes/balance` and
548
+ > Balance field `data.available_balance`. `magpie quota modelscope` then shows a
549
+ > number.
363
550
 
364
551
  ## Privacy
365
552
 
366
- `login/info` responds with `Email` and `GitlabAccessToken` among other fields.
367
- This plugin reads `NickName` / `Name` and nothing else; the rest is dropped as
368
- soon as the response is parsed. The token lives only in magpie's own
369
- `plugin-auth.json` — the plugin never writes it anywhere, never logs it, never
370
- sends it anywhere but `modelscope.cn`.
371
-
372
- The one thing it writes is that nickname, as `accountId` on the sign-in it
373
- already has, so magpie's card can name the account. It goes through the host's
374
- own `client.auth.set`, which replaces the sign-in entry — so the plugin hands
375
- back what the host gave it, with the name added, and the token is still
376
- stored once.
377
-
378
- It also reads `providers.json`, magpie's own list of providers, for one thing:
379
- whether you already named this account yourself. If you have, it writes
380
- nothing — your name stays, and the nickname is not put over it.
553
+ `login/info` returns `Email` and `GitlabAccessToken` among other fields. The
554
+ plugin reads `NickName` / `Name` and drops the rest as soon as the response is
555
+ parsed. The token lives only in magpie's own `plugin-auth.json`: the plugin
556
+ never writes it anywhere, never logs it, and never sends it anywhere but the
557
+ two ModelScope sites (`modelscope.cn` and `modelscope.ai`) — the domestic one
558
+ first, and the international one when the domestic one refuses the token, or
559
+ when the sign-in is already known to belong there.
560
+
561
+ The one place a token could still be copied is the account name: a reply whose
562
+ `NickName` is the token, or a piece of it, or a key of a vendor's shape
563
+ (`sk-…`, `ghp_…`, `AKIA…`) is not written back as `accountId` and is masked
564
+ on the card instead. A name that happens to begin like one is refused too: `sk-`
565
+ is a key's front before it is a nickname. Whether a name reads as a key is the
566
+ card's own question, asked of the name as written, so "ms-office 2016",
567
+ "ms-cli 2026.11.9" and all-capitals names ("CHRISTOPHER LEE 2026") are names
568
+ and are written back. Three situations the mask cannot cover: the
569
+ first 4 and last 4 characters of the token do appear, that is the mask; a key
570
+ of a shape this plugin does not recognise, echoed back in a spelling it does
571
+ not fold (the `ms-` shapes are covered in any case and full width); and a word
572
+ that is already masked is not judged again, so its ellipsis is printed as the
573
+ mask it is.
574
+
575
+ Text from the server that carries a key cut by whitespace is masked the same
576
+ way as the CLI's (`ghp_ 1234 AbCd …`, `sk-a nt-a pi03 …`), via the same
577
+ reading: a run of words that joins back into a key. The cut it reads across
578
+ is a space, a no-break or ideographic space, a line break, or a middot or a
579
+ bullet (one or two in a row, at most one space either side) — a value copied
580
+ out of a web page or a Chinese IME carries these as often as plain spaces. A
581
+ middot is read across when a piece beside it is not a word (`A·K·I·A·…`,
582
+ `9f8e·7d6c·…`, the pieces of a key cut in fours), and a run of single-character
583
+ pieces is read whatever the separator (`A· K· I· A· …`). The card's own middot
584
+ sits between words on both sides ("Qwen3-Max · 调用返还") and stays as written;
585
+ a middot inside a key stays in its mask when the mask shows only its ends,
586
+ because the mask keeps everything between the first four characters and the
587
+ last four. A full-width hyphen
588
+ (`-`, which Unicode does not fold) is read as the key's own. A run that also
589
+ reads as an ordinary sentence is left alone.
590
+
591
+ The run readings give up on a body past 32 KiB and only the per-word pass
592
+ runs: a body of thousands of one-word pieces ("s k - a n t" repeated) costs
593
+ two orders of magnitude more reading than any message this pass exists for,
594
+ and a key stored inside such a body as whole words is still masked. The
595
+ bound exists so a hostile server's error cannot eat a hook's budget.
596
+
597
+ A piece is masked only when it carries ten characters of the key's own —
598
+ **letters and digits, not the hyphens or spaces the token was written in**. A
599
+ five-character slice of a hyphenated key (`ms-…`'s middle `c-3333`) is left as
600
+ it is written: no one can read a full key out of a slice that short, and
601
+ masking every string that shares six characters would eat ordinary words. The
602
+ same floor is what keeps a nickname a person actually picked ("Mison 2026")
603
+ from being refused as an echo.
604
+
605
+ A key quoted in a frame keeps the frame: `(ghp_…Qrst)`, `[ghp_…]`, `"ghp_…"`.
606
+ The mask spends four characters at each end on the key's own, so the brackets
607
+ are printed as written rather than eaten by it.
608
+
609
+ The plugin writes two things, both onto the sign-in it already has and through
610
+ the host's own `client.auth.set`: the nickname, as `accountId` (so the card
611
+ reads "Mison" instead of "API key …a1b2"), and, for an international account,
612
+ the site as `site`. It re-reads the sign-in through the host first and writes
613
+ onto that copy, so a key you paste or a field you add while a check-in runs is
614
+ not replaced by an older read. It hands back what the host gave it with those
615
+ fields added, so the token is still stored once. An account on the domestic
616
+ site carries no `site`: a probe tries that one first anyway.
617
+
618
+ It reads `providers.json` for one thing only: whether you already named the
619
+ account yourself. A name you chose is never overwritten. The `site` of an
620
+ international account is written even then — it is not a name, and it only
621
+ saves the probe.
381
622
 
382
623
  ## Scope and limits
383
624
 
384
- - One account's own daily grant, idempotent: a check-in reads the ledger, touches
385
- the sign-in endpoint, and reads the ledger again — three requests. A day already
386
- claimed (a retry, a later ask) answers `done` in two; a touch the ledger has not
387
- caught up with reads it once more, four. Once a day.
388
- Asks that arrive before the window open send nothing at all.
389
- The usage read makes two requests and never touches the sign-in endpoint, so the
390
- menu bar's polling cannot turn into check-ins.
391
- - Every request gives up after 8 seconds, and a response body is read to at most
392
- 256 KiB; neither can hold the host or grow without bound. The timeout covers the
393
- body as well as the request: a proxy that stops mid-reply is cut at the same
394
- 8 seconds, and says so rather than passing the runtime's own word for it
395
- through (`terminated` on Node, a sentence about `verbose: true` on Bun). A whole
396
- check-in gives up after 25 seconds and a whole usage read after 18, because
397
- magpie runs every provider's check-ins under one two-minute budget and asks
398
- accounts one at a time — a hook cut short answers `failed`, which the host
399
- retries in half an hour, and that retry reads the ledger before claiming, so
400
- nothing is lost.
401
- - **A body it does not understand fails; it never reads as zero.** No `data`, a
402
- `data` that is a list rather than an object, a balance with no
403
- `available_balance` or one that is not a number — each is a failure with the
404
- reason, because "0 魔粒可用" is a definite answer that would be false. The same
405
- goes for the server's own text: a message or a grant's title carrying a key —
406
- this account's token, or one of another vendor's, which this plugin never held
407
- and therefore cannot compare against — has it masked before the text reaches
408
- the card or magpie's log. What counts as a key there is the same rule `--from`
409
- uses, so a ledger title or a server message of ordinary words is untouched.
410
- - `MAGPIE_MSMOLI_CONFIG` overrides the config file's path. It exists so the test
411
- suite never touches a real file; it works in production too.
412
- - The other 170 魔粒 a day come from liking 10 items (20), posting AIGC works
413
- (100) and AIGC models (50). None of those have an API, and automating them
414
- means driving the web UI, so this plugin does not touch them.
625
+ The terms and risks are in
626
+ [Purpose, responsibility, and risk](#purpose-responsibility-and-risk).
627
+
628
+ - **Requests per day.** A check-in is three requests (ledger, touch, ledger);
629
+ a day already claimed answers `done` in one ledger read when the nickname
630
+ is known, two otherwise; a ledger that hasn't caught up takes
631
+ four. A run longer than one ledger page adds one read per page walked back,
632
+ up to seven — only while the streak is still going at the oldest row. A day
633
+ whose own rows fill a page (a busy account) reads the next page too, up to
634
+ four more, for both the check-in and the card. Asks before the window send
635
+ nothing. A usage read is two requests and never touches the sign-in
636
+ endpoint, so menu-bar polling cannot turn into check-ins.
637
+ - **International accounts cost one extra 401 per token, per process**, and
638
+ that 401 is on the first read that does not already know the site: the
639
+ sign-in's `site` field saves it after a restart, so an international usage
640
+ read is three requests the first time (domestic 401, balance, ledger) and two
641
+ after that. A domestic account costs nothing extra. A remembered site that
642
+ 401s drops the memory and probes again; one that times out or answers
643
+ nothing usable is asked no more than the other site, so a dead link costs
644
+ the 8-second request timeout, not the whole budget.
645
+ - **Time limits.** Every request gives up after 8 seconds, body included, and a
646
+ body is read to at most 256 KiB. A whole check-in gives up after 25 seconds
647
+ and a usage read after 18, because magpie runs every provider's check-ins
648
+ under one two-minute budget. A hook cut short answers `failed`, the host
649
+ retries in half an hour, and the retry reads the ledger first, so nothing is
650
+ lost.
651
+ - **What a 401 or a 403 says.** A 401 signs the account out only when it
652
+ reads as the site refusing the token — "unauthorized", an invalid or expired
653
+ token, a refusal in Chinese ("请先登录", "登录状态已失效"), an empty JSON
654
+ object, or a code of 401 — and never when it names the path instead ("proxy
655
+ authentication required", a gateway's page, a bodyless 401). A 403 is
656
+ reported as ModelScope's risk control only when it carries ModelScope's
657
+ shape; a proxy's 403 page is reported as an ordinary refusal. A sign-in
658
+ that was refused after the day's credits landed reports the expiry but
659
+ keeps the day's answer.
660
+ - **A body it doesn't understand fails; it never reads as zero.** A missing
661
+ `data`, a list where an object belongs, a missing or non-numeric
662
+ `available_balance`: each is a failure with its reason, because
663
+ "0 魔粒可用" would be a definite and false answer. Server text (a message, a
664
+ grant's title) has any key in it masked before it reaches the card or the log.
665
+ - `MAGPIE_MSMOLI_CONFIG` overrides the config file's path. It exists for the
666
+ test suite, and works in production too.
667
+ - The other 170 credits a day (liking items, posting AIGC works and models) have
668
+ no API and would mean driving the web UI. This plugin doesn't touch them.
415
669
 
416
670
  ## Troubleshooting
417
671
 
418
672
  | Symptom | Fix |
419
673
  |---|---|
420
- | `not signed in` | `printf 'ms-token\n' \| magpie plugin login msmoli` |
421
- | No 魔粒 window on the card, or no check-in | restart magpie (the plugin host caches) |
422
- | `failed` with HTTP 401 | the token was revoked — make a new one and sign in again |
423
- | `failed` with HTTP 403 | ModelScope's risk control refused the request; the token is fine. It retries in 30 minutes |
424
- | Before the window the card reads failed, with a line naming the claim time: `09:56 后自动领取(magpie 每半小时问一次,无需操作)` | expected — the card leads with when it will claim; see "When it claims" above |
425
- | Always `inactive` | the sign-in works but nothing shows in today's ledger: the account may not be eligible, or the grant hasn't been written yet (a claim made elsewhere shows as `done`). Before Beijing noon magpie asks again every 30 minutes; from noon on the plugin asks once more itself, half an hour later ("半小时后再核对一次"), and only the second `inactive` stands for the day |
426
- | Chat through the `modelscope` preset answers `429 insufficient balance` in the Routing log | the account's daily 250 魔粒 are spent — a flagship call costs 2, so ~125 calls run the day dry and the vendor refuses the rest. Not this plugin: it registers no models. If you want magpie to *see* the balance on the preset's card too, give the preset the same reading — Balance URL `https://modelscope.cn/openapi/v1/magicubes/balance`, Balance field `data.available_balance` in its editor (the same token answers it; read `1.50` on 2026-10-28). Whether the gateway then holds a drained key before spending a request on it is not verified — the safe path is still the 429's own 30-minute rest |
427
- | Two accounts of one ModelScope nickname, and only one of them checks in | magpie before 2026-10-20 kept a plugin's check-in per display name rather than per sign-in, so namesakes collapsed into one. Update magpie: the fix landed upstream on 2026-10-20 (magpie-community/plugins#254) |
428
- | Edits to this plugin do nothing | path plugins are referenced in place; restart magpie |
429
- | Installed both ways by accident, and `magpie plugin list` says `msmoli` is "served by" one of them | that is magpie keeping two copies of one provider id. Remove one: `magpie plugin rm github:MisonL/magpie-plugin-msmoli` or `magpie plugin rm <path>`. magpie files your alias with the package it was set against, so removing a copy can take the display name with it — it comes back if you add that package again, or you set the alias once more |
674
+ | `没有登录信息,请先登录` | `printf 'ms-token\n' \| magpie plugin login msmoli` |
675
+ | No credit window on the card, or no check-in | Restart magpie; the plugin host caches. |
676
+ | `failed` with HTTP 401 | The token was revoked, or belongs to the other site; `modelscope.cn` and `modelscope.ai` don't share tokens. The message names the site that refused (国内站 / 国际站), or 任一站点 when both did. Make a new token there and sign in again. |
677
+ | `failed` with HTTP 403 | ModelScope's risk control refused the request; the token is fine. A check-in is asked again in 30 minutes; a usage read shows the 403 on the card until the next poll. |
678
+ | Before the window: failed, with `09:56 后自动领取(北京时间,magpie 每半小时问一次,无需操作)` | Expected. See [When it claims](#when-it-claims-the-check-in-window). |
679
+ | Check in now before the target minute answers failed with the same line | Expected. A manual press and the host's half-hourly ask reach the plugin the same way, so only the target minute starts the claim. |
680
+ | Always `inactive` | The sign-in works but today's ledger shows nothing: the account may not be eligible, or the grant hasn't been written yet. Before noon magpie re-asks every 30 minutes; from noon the plugin checks once more ("半小时后再核对一次"), and only the second `inactive` stands. |
681
+ | Chat through the `modelscope` preset answers `429 insufficient balance` | The day's 250 credits are spent (~125 flagship calls). Not this plugin: it registers no models. See the tip under [Why its own provider](#why-its-own-provider-with-no-models). |
682
+ | Two accounts with the same nickname, and only one checks in | magpie before 2026-10-20 kept check-ins per display name. Update magpie (fixed in magpie-community/plugins#254). |
683
+ | Edits to a clone do nothing | Path plugins are referenced in place; restart magpie. |
684
+ | `magpie plugin list` says `msmoli` is "served by" one of two copies | It was installed two ways. Remove one with `magpie plugin rm github:MisonL/magpie-plugin-msmoli` or `magpie plugin rm <path>`. The alias is filed with the package it was set against, so you may need to set it again. |
430
685
 
431
686
  ## License
432
687
 
433
- MIT
688
+ [MIT](LICENSE). Contributions welcome; see [CONTRIBUTING.md](CONTRIBUTING.md).