magpie-plugin-msmoli 0.2.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 +550 -372
  2. package/bin/sync.mjs +1569 -148
  3. package/index.mjs +2456 -272
  4. package/package.json +3 -4
  5. package/README.zh-CN.md +0 -358
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 领取(北京时间)`.
6
30
 
7
- ## What 魔粒 is
31
+ ## Contents
8
32
 
9
- ModelScope's API-Inference is free, paid in 魔粒:
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
46
+
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
+ ```
62
+
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,31 +76,37 @@ 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
+
21
90
  ## Purpose, responsibility, and risk
22
91
 
23
- **What it is for.** A personal utility: it claims the daily 魔粒 grant of the
24
- ModelScope (魔搭) account *you* sign in, and does nothing else. It is not an
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
25
94
  official ModelScope product; it is not affiliated with ModelScope, Alibaba or
26
- magpie, and it collects nothing about you — the token stays in magpie's own
95
+ magpie, and it collects nothing about you. The token stays in magpie's own
27
96
  `plugin-auth.json`, and every request goes to the site the account signed in
28
97
  to.
29
98
 
30
99
  **The risk you are taking.** ModelScope has no check-in endpoint. The daily
31
100
  grant exists for a *real* website sign-in, and this plugin obtains it by
32
- calling `login/info` once a day with the browser's session cookie — a script
101
+ calling `login/info` once a day with the browser's session cookie: a script
33
102
  posing as the browser, not a button the official client offers. ModelScope's
34
103
  user agreement forbids plugins that interfere with the service (4.1.2(1)) and
35
- "刷分" with third-party tools (4.1.2(5)), and lets the platform freeze a
36
- user's benefits or blacklist them for abuse (2.3.1 and the 魔粒 rules). An
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
37
106
  account is real-name verified and often tied to Aliyun, so what is at stake is
38
107
  the account, not the 250 credits that expire the same day. No account has been
39
- reported frozen for running this, and no one can promise one never will be —
40
- whether the service's detection would ever notice is not something this
41
- project can answer. Whether to run it is a decision about your own account.
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.
42
110
 
43
111
  **Responsibility.** You are responsible for complying with ModelScope's terms
44
112
  when you use this. The software is provided under the MIT licence, without
@@ -54,198 +122,300 @@ npm.
54
122
 
55
123
  ## Install
56
124
 
57
- 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,
58
127
  `/Applications/magpie.app/Contents/MacOS/magpie`.
59
128
 
60
- ### Install from npm
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.
61
134
 
62
- The package is on npm as `magpie-plugin-msmoli` (latest 0.1.0). Installs into
63
- magpie's own plugin folder, and `magpie plugin update` follows the versions.
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.
138
+
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>
64
143
 
65
144
  ```bash
66
- magpie plugin add magpie-plugin-msmoli
67
- printf 'ms-your-token\n' | magpie plugin login msmoli
145
+ magpie plugin add \
146
+ magpie-plugin-msmoli
68
147
  ```
69
148
 
70
- ### Install via GitHub URL (no clone, auto-updates)
149
+ `magpie plugin update` follows npm releases.
71
150
 
72
- 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`.
151
+ </td>
152
+ <td>
73
153
 
74
154
  ```bash
75
- magpie plugin add github:MisonL/magpie-plugin-msmoli
76
- printf 'ms-your-token\n' | magpie plugin login msmoli
155
+ magpie plugin add \
156
+ github:MisonL/magpie-plugin-msmoli
77
157
  ```
78
158
 
79
- The token is stored in `plugin-auth.json`; one login works regardless of the form you used to install.
80
-
81
- ### Install from a clone
159
+ Installed into magpie's plugin folder; updates with `magpie plugin update`.
82
160
 
83
- Keeps your own copy, which is what you want if you plan to change it. magpie
84
- references the folder where it lies rather than copying it, so it loads only
85
- while that folder is there, and an update means pulling and adding it again.
161
+ </td>
162
+ <td>
86
163
 
87
164
  ```bash
88
165
  git clone https://github.com/MisonL/magpie-plugin-msmoli.git
89
166
  cd magpie-plugin-msmoli
90
167
  magpie plugin add "$PWD"
168
+ ```
169
+
170
+ Referenced in place: it loads only while the folder exists. Update by pulling.
171
+
172
+ </td>
173
+ </tr>
174
+ </table>
175
+
176
+ Then sign in and turn the check-in on:
177
+
178
+ ```bash
91
179
  printf 'ms-your-token\n' | magpie plugin login msmoli
92
180
  ```
93
181
 
94
- Either form ends the same way: a `msmoli` row under Settings → Usage → Plugins.
95
- The sign-in is kept per provider id (`msmoli`) rather than per folder, so
96
- switching between the two forms (or moving the clone) keeps the token — no
97
- second login.
182
+ The switch is **Settings → Usage → Plugins → `msmoli`**, or `settings.json` in
183
+ magpie's own folder (`~/.config/magpie` for an installed magpie):
98
184
 
99
- ### International site (modelscope.ai)
185
+ ```json
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
196
+ ```
100
197
 
101
- The plugin now provides a second provider for the international site:
102
- `msmoli-intl` (ModelScope 魔粒(国际版)). The same `ms-` token format, the same
103
- daily grant rules, **but tokens are site-specific** — a token from `modelscope.cn`
104
- answers 401 on `modelscope.ai`, and vice versa.
198
+ The same `ms-` token is both the inference `Authorization: Bearer` and the web
199
+ session cookie, so nothing else needs configuring.
105
200
 
106
- The sign-in and check-in work the same way:
201
+ ### Already use ModelScope for chat? Sync that key
202
+
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:
107
205
 
108
206
  ```bash
109
- printf 'ms-your-intl-token\n' | magpie plugin login msmoli-intl
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
110
209
  ```
111
210
 
112
- The check-in window (`msmoli.json`) is shared between both providers — it is
113
- a personal preference, not a per-site setting. To turn on the international
114
- check-in, set `msmoli-intl` in your plugin-checkins instead of or alongside
115
- `msmoli` (see "Turn the check-in on" below, substituting the provider id).
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.
116
215
 
117
- ### Already have ModelScope set up in magpie? Sync that key to the plugin
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.
118
220
 
119
- If you added ModelScope earlier just for chat, its key is already in
120
- `providers.json`. This package ships a small CLI that logs the same token into
121
- the plugin, so you don't have to find it again.
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.
122
225
 
123
- It syncs the domestic site only, because the built-in preset it reads is the
124
- `modelscope.cn` one. The international site has no preset; sign `msmoli-intl`
125
- in by hand (`magpie plugin login msmoli-intl`).
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.
126
228
 
127
- Installed from GitHub, magpie keeps its own copy in its plugin folder:
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:
128
308
 
129
309
  ```bash
130
- node ~/.config/magpie/plugins/node_modules/magpie-plugin-msmoli/bin/sync.mjs --status # see both sides
131
- node ~/.config/magpie/plugins/node_modules/magpie-plugin-msmoli/bin/sync.mjs --to-plugin # sync
310
+ printf 'ms-your-intl-token\n' | magpie plugin login msmoli
311
+ magpie accounts # both accounts, each with its own credit window
132
312
  ```
133
313
 
134
- Installed from a clone, run the file in the clone — magpie references that
135
- folder and copies nothing (`./bin/sync.mjs`; on Windows or WSL, where the
136
- file may not carry an executable bit, run it as `node bin/sync.mjs`). Portable
137
- installs keep the same layout under their `data` folder; if detection misses,
138
- `--dir <magpie's config folder>` and `--magpie <path to the magpie binary>` say
139
- where things are: for a portable install that is its `data` folder, not the
140
- folder the executable sits in. `--magpie` takes a path, absolute or relative —
141
- a bare `magpie` is refused, because that one is resolved through `PATH` and the
142
- first `magpie` on it would be handed the token. `npx magpie-plugin-msmoli` runs
143
- the CLI from npm (published as 0.1.0) — npm 12 blocks `npx github:…` by
144
- default, so don't start from that.
145
-
146
- `--dir` says where to *read*; magpie's CLI has no such flag, so a write always
147
- lands in the binary's own folder. Given both and they disagree, the tool stops
148
- before writing anything rather than reading one magpie and writing another.
149
- "Disagree" means the folders are different places, not that the strings differ:
150
- a shell completes a directory with a trailing slash, `/private/tmp` and `/tmp`
151
- are one folder on a Mac, and `data/.` is `data` — each names the binary's own
152
- folder and is allowed.
153
-
154
- It reads magpie's config but never writes it: the writes are magpie's own
155
- (`magpie plugin login`, `magpie plugin logout`, `magpie provider key`), and the
156
- token is masked in everything it prints, failures included. When the plugin
157
- already uses the same key it changes nothing; when the two keys differ it
158
- refuses, because a second `login` would add a second account — pass `--force`
159
- to replace a single existing account. `--to-preset` goes the other way (needs
160
- `--force`: that key is what chat uses). If more than one provider could be the
161
- built-in side (say, a relay of the same host), the tool refuses to guess and
162
- `--from <id>` picks one; `--dry-run` prints the commands without running them.
163
- A value flag given twice (`--dir a --dir b`) is refused, not silently last-wins,
164
- and `--dir=<folder>` is read as well. `--from` and a stray argument are echoed
165
- back masked when the value looks like a key — a token pasted into the wrong
166
- position is never printed whole, whatever its shape. That covers the shapes
167
- `ms-…` does not: a GitHub or Slack token, an AWS secret (which carries `/`), a
168
- UUID, a hex digest, a base64 body — long, or short enough that only its trailing
169
- `=` (or a `+` beside a case mix) gives it away. The judgement runs
170
- word by word,
171
- so a key with a space or a newline pasted onto it — the shape a copy out of a
172
- terminal or a chat window leaves — or quoted inside a line is still the key it
173
- is. A word that is one key is masked whole, ends and all, including one split with
174
- separators — a hex digest with dashes, a key written as `AbCdEfGh.IjKlMnOp…` —
175
- and an AWS secret, whose two parts of 12+ characters with mixed case mark it. An
176
- invisible character dropped inside a key (a zero-width space or `\x01`, the
177
- shape a copy out of a chat window leaves) does not save it either: the
178
- control/format characters sitting between key-alphabet characters are removed
179
- before masking, so the two halves are rejoined into the one key they are —
180
- each half alone falls under the length floor and would have leaked. A
181
- word that starts like a path (`/`, `~`, `.`, a drive letter, a UNC root) keeps
182
- its shape, and only the key-like run inside it is masked, so `--dir
183
- /tmp/ghp_16C7…` still names the folder it read; a key pasted in *behind* a path
184
- prefix (`~/keys/sk-ant-…`) reads as part of that path, which is what keeping a
185
- path's shape costs. The folder is reported
186
- the same way on every line that names it, and magpie's own output goes through
187
- the same door — a key of another provider in the child's error message is
188
- masked there too, not only the `ms-` shape. A provider id and an account name
189
- are read out of magpie's own files and go through the same door: a key pasted
190
- into either field is masked on every line the tool names it.
191
-
192
- Turn the check-in on: Settings → Usage → Plugins → the `msmoli` row, or in
193
- `settings.json` (in magpie's own folder, `~/.config/magpie` for an installed
194
- magpie):
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.
195
320
 
196
- ```json
197
- {
198
- "pluginCheckins": {
199
- "msmoli": true
200
- }
201
- }
202
- ```
321
+ ### More than one account
203
322
 
204
- The token comes from <https://modelscope.cn/my/myaccesstoken>. The same `ms-`
205
- token is both the inference `Authorization: Bearer` and the web session cookie,
206
- so nothing else needs configuring. For the international provider
207
- (`msmoli-intl`), the token comes from <https://modelscope.ai/my/myaccesstoken>
208
- instead — the two sites' tokens do not interchange.
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:
209
326
 
210
- From then on magpie claims it once a Beijing day per signed-in account, retries
211
- later that day if it fails, and reports the outcome on the account's usage card:
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.
212
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
213
368
  ```
214
- ● msmoli API key …a1b2 · 魔搭社区 魔粒 1% (今日用 2 / 领 250 · 余额 249.5) ↻14h24m tomorrow 09:31 249.5 魔粒可用 left
215
- ```
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.
216
373
 
217
374
  ## How the check-in works
218
375
 
219
376
  ModelScope publishes no check-in endpoint. What it does have is a side effect:
220
377
  `GET /api/v1/users/login/info` counts as a sign-in, and the first one of the
221
- day grants `daily_active` 200 and `aliyun_bindlogin` 50. Repeat it the same day
222
- and nothing more is granted.
378
+ day grants `daily_active` 200 and `aliyun_bindlogin` 50. Repeating it the same
379
+ day grants nothing more.
223
380
 
224
381
  A hook that just calls it can't tell "claimed now" from "claimed hours ago", so
225
- this one reads the day's ledger around it:
226
-
227
- 1. read today's `EARN` records — is `daily_active` there?
228
- 2. touch `login/info`
229
- 3. read the ledger again
230
-
231
- A `claimed` needs a grant the ledger did not already show around the touch. If
232
- the touch went through and the ledger still shows nothing, the plugin asks
233
- magpie to look again half an hour later (`until`), once — from Beijing noon on
234
- magpie would otherwise settle `inactive` for the day, and a grant written a few
235
- seconds late was recorded as "the account may not be in the campaign". Before
236
- noon magpie's own provisional re-ask (every 30 minutes) already covers it.
237
-
238
- 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
+ ```
239
392
 
240
393
  | Outcome | Meaning |
241
394
  |---|---|
242
395
  | `done` | already claimed today, with the day's total |
243
- | `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 |
244
- | `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) |
245
- | `failed` | nothing was claimed — including "not yet time", explained below |
246
-
247
- The endpoints it uses, all on `modelscope.cn` with `Cookie: m_session_id=ms-…`
248
- (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.
249
419
 
250
420
  | Purpose | Path |
251
421
  |---|---|
@@ -253,258 +423,266 @@ The endpoints it uses, all on `modelscope.cn` with `Cookie: m_session_id=ms-…`
253
423
  | Balance | `GET /openapi/v1/magicubes/balance` |
254
424
  | Ledger | `GET /openapi/v1/magicubes/transactions?page=1&page_size=50` |
255
425
 
256
- `type` in the ledger has been seen as `EARN` and `SPEND_CONFIRM`; there is
257
- also an `EXPIRE`, which this plugin ignores. Rows are aggregated per day, type,
258
- `rule_key` and `model_tier` — a day's spending shows one row per model tier it
259
- used — with a `count` on each, and the list is sorted newest first. The ordering
260
- is what makes the single page of 50 enough: today's rows come before any older
261
- one. Measured on a live account that is 3–7 rows a day, so a page reaches back
262
- about a fortnight rather than 50 days; what the read would not survive is a
263
- single day with more than 50 rows. Asking for a larger page does not help —
264
- 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>
265
437
 
266
438
  ### When it claims: the check-in window
267
439
 
268
- The claim is held back until a time of day, by default somewhere in
269
- **08:30–10:00 Beijing time** — an account's own minute in that window,
270
- picked from a hash of the day and the account, so it doesn't move between
271
- the host's retries. It makes the traffic look like a person opening the
272
- 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.
273
447
 
274
- Waiting costs nothing: daily grants **expire 24 hours after they land**
275
- (a grant at 15:35 expires at 15:35 the next day), so claiming at 9am leaves
276
- 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.
277
450
 
278
- Set it in `msmoli.json` in magpie's own folder (`~/.config/magpie` for an
279
- 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.
280
455
 
281
456
  ```json
282
457
  { "checkinWindow": ["08:30", "10:00"] }
283
458
  ```
284
459
 
285
- One string works the same way: `{ "checkinWindow": "08:30-10:00" }`.
286
-
287
- `false` turns the window off (claim whenever magpie asks); so does `"off"`.
288
- An unusable value — `08:30`–`08:00`, a date that isn't a clock time — falls
289
- back to the default rather than never claiming. An end past **23:00** is
290
- capped there: the host does not ask at the target's minute — its own look runs
291
- every 30 minutes on a phase of its own, so the first ask past a target can come
292
- up to half an hour late, and the retry after a failure half an hour after that.
293
- A target needs both to still land on the same day. Capping at 23:00 leaves any
294
- target that room; later would leave some with one ask, or none. A window that sits entirely past 23:00 falls back to the
295
- default window too, since there would be nowhere left to put the target. The
296
- file is read on every ask, so a change takes effect without restarting
297
- magpie.
298
-
299
- How it fits magpie's schedule: magpie asks once when the Beijing day turns,
300
- then every 30 minutes — always after a `failed`, and also after a
301
- `done`/`inactive`/`ineligible` it hasn't seen a claim for yet, up to noon
302
- Beijing (a provisional answer: the vendor's day may not have turned; from noon
303
- on the plugin's own `until` buys the same re-ask when the ledger lagged). So
304
- before the window's moment the plugin answers `failed` with the time it will
305
- run, and sends **no request at all**. `failed` is still the one outcome that
306
- 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.
307
475
 
308
476
  What that costs, since a plugin has no other way to defer: until the claim
309
- actually runs the account card shows a red dot and magpie's own "check-in
310
- failed; magpie tries again later" wording, each of those asks rewrites
311
- magpie's `plugin-checkin.json`, and `magpie accounts checkin` run in that
312
- period exits non-zero. Turning the window off removes all three.
313
-
314
- How long that is depends on when magpie next asks, not on the target alone.
315
- Measured on a real day: the target was 09:01, magpie asked at 09:00:57 (three
316
- seconds early, so `failed`), and the claim landed on the next ask at 09:31 —
317
- about 9.5 hours from midnight, rather than the 8.5 the window's 08:30 start
318
- alone suggests. That is also why the card names the cadence instead of a
319
- countdown: the host keeps the line until its next ask, half an hour later, so
320
- "约 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>
321
500
 
322
501
  ### Upstream
323
502
 
324
503
  magpie gives a plugin no timer, no clock, and no say in when it is called:
325
- `auth.checkin(getAuth, info)` returns an outcome and the host decides when
326
- to ask again. Everything above is worked around within that contract, and
327
- the awkward parts (proving "not yet" as a failure, whole-day granularity for
328
- the account card) want a line in
329
- [this issue](https://github.com/yetone/magpie/issues/3245) proposing timers and
330
- `recheck` for plugins.
331
-
332
- Two consequences worth knowing, both from the same missing timer: a window's
333
- target is only reached by the host's own asks, so one that falls while the
334
- machine is asleep is claimed on the next ask after it wakes (the day is lost
335
- only if that happens past midnight); and the plugin cannot retry on its own
336
- between asks — the window's cap (above) is what buys the retry.
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.
337
508
 
338
- ### More than one account
339
-
340
- The plugin is written per account, not per token. magpie keeps each signed-in
341
- token under its own key — the first as `msmoli`, later ones as `msmoli#xxxxxx`
342
- — and asks the check-in once per account, handing each its own token through
343
- `getAuth`. So with two tokens:
344
-
345
- - each account gets **its own target minute** that day (the hash is over the
346
- day and that account's token), so the two normally claim at different
347
- minutes — a collision is possible, but only costs both a shared minute;
348
- - each account's check-in is recorded, retried and shown separately in
349
- `plugin-checkin.json` and on the card;
350
- - the name read at check-in is cached per token, so one account's nickname
351
- never appears on another's row.
352
-
353
- Add a second one the same way, then pick which to use:
354
-
355
- ```bash
356
- printf 'ms-the-second-token\n' | magpie plugin login msmoli
357
- magpie accounts # both rows, each with its own 魔粒 window
358
- ```
359
-
360
- The card names an account by the name ModelScope gives it. An api-type
361
- sign-in has no such name, so the check-in writes the nickname back: the same
362
- sign-in entry, the token still stored once, now with `accountId` beside it.
363
-
364
- It happens on the first check-in that actually runs in the host process, so a
365
- fresh install shows `API key …a1b2` until then. An account whose check-in is
366
- already settled for the day is not asked again — magpie reuses the record — so
367
- the name appears with the next day's check-in rather than on upgrade.
368
-
369
- If you would rather choose the name yourself, magpie's own alias wins over it:
370
-
371
- ```bash
372
- magpie accounts alias msmoli "API key …a1b2" my-main
373
- ```
374
-
375
- The second word is the account as the card shows it *now*: before the first
376
- check-in that is the key's tail, afterwards the ModelScope nickname. An account
377
- you have named is left alone — the check-in won't write its nickname over your
378
- alias — so either label works, and the alias outranks the nickname from then on.
379
-
380
- And the daily grant belongs to the ModelScope account, not to the key: the
381
- claim is idempotent server-side per account per day, so a second token for the
382
- 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.
383
512
 
384
513
  ## Why its own provider, with no models
385
514
 
386
- The plugin registers `msmoli` rather than the built-in `modelscope` preset, for
387
- two reasons:
515
+ The plugin registers `msmoli` instead of riding on the built-in `modelscope`
516
+ preset:
388
517
 
389
- 1. magpie namespaces a plugin id that collides with a preset into
390
- `<id>-plugin`, so a plugin that named itself `modelscope` would not be the
391
- same provider anyway — it would only sit next to the preset and invite people
392
- 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.
393
520
  2. **Safety.** magpie's gateway passes over an account whose usage window reads
394
- as used up. If a check-in plugin served the provider that inference actually
395
- routes through, reporting 100% would take the whole chat path out of magpie.
396
-
397
- So chat keeps using the `modelscope` preset with your API key in
398
- `providers.json`, and this plugin only carries the sign-in its check-in runs as.
399
-
400
- ### 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.
401
523
 
402
- The providers page then shows two: `ModelScope` (the preset, with your models)
403
- and `ModelScope 魔粒` (this plugin, with none). They are separate providers that
404
- 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:
405
526
 
406
- | | `modelscope` preset | `msmoli` (this plugin) |
527
+ | | `ModelScope` (preset) | `ModelScope 魔粒` (this plugin) |
407
528
  |---|---|---|
408
- | Serves chat models | yes | no — `magpie models` never lists it |
409
- | Its usage card | none (the preset has no balance endpoint) | 魔粒 balance and today's spend |
410
- | Check-in | not built in for ModelScope | this plugin, once a day |
411
- | Reachable by the gateway | yes | no, so a card reading 100% cannot route around it |
412
-
413
- The token being in both places is how magpie is built: a preset keeps its key
414
- in `providers.json`, a plugin keeps its sign-in in `plugin-auth.json`. Entering
415
- the same `ms-` token in both is expected. Nothing reads the plugin's usage card
416
- to decide where chat goes — that is the point of the separate id.
417
-
418
- The preset's empty card has one consequence worth knowing. ModelScope's
419
- inference API has no balance endpoint, so magpie cannot see that the key's 魔粒
420
- are gone; the gateway learns it from the vendor's own `429 insufficient balance`
421
- and then rests the key for 30 minutes. Until then each request spends one failed
422
- try on it first. The balance is on the account, and `modelscope.cn` does answer
423
- it, so the preset can be told: in its editor, Balance URL
424
- `https://modelscope.cn/openapi/v1/magicubes/balance` and Balance field
425
- `data.available_balance` (the same token answers it — `magpie quota modelscope`
426
- then reads a number instead of `no balance endpoint known for it`).
427
-
428
- The one real overlap to know about: **both rows cost nothing extra**, but if you
429
- remove the plugin's sign-in, chat is unaffected, and if you remove the preset,
430
- the check-in keeps working. They fail independently.
431
-
432
- If you sign in the international site as well, that adds one more row,
433
- `msmoli-intl` (ModelScope 魔粒(国际版)). It has every property in the table
434
- above — no models, its own card, its own check-in — and fails independently of
435
- both the preset and the domestic plugin row.
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.
436
550
 
437
551
  ## Privacy
438
552
 
439
- `login/info` responds with `Email` and `GitlabAccessToken` among other fields.
440
- This plugin reads `NickName` / `Name` and nothing else; the rest is dropped as
441
- soon as the response is parsed. The token lives only in magpie's own
442
- `plugin-auth.json` — the plugin never writes it anywhere, never logs it, never
443
- sends it anywhere but the site the account was signed in for (`modelscope.cn`
444
- for `msmoli`, `modelscope.ai` for `msmoli-intl`).
445
-
446
- The one thing it writes is that nickname, as `accountId` on the sign-in it
447
- already has, so magpie's card can name the account. It goes through the host's
448
- own `client.auth.set`, which replaces the sign-in entry — so the plugin hands
449
- back what the host gave it, with the name added, and the token is still
450
- stored once.
451
-
452
- It also reads `providers.json`, magpie's own list of providers, for one thing:
453
- whether you already named this account yourself. If you have, it writes
454
- 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.
455
622
 
456
623
  ## Scope and limits
457
624
 
458
- **ModelScope's terms and the risks of running this are stated above, in
459
- [Purpose, responsibility, and risk](#purpose-responsibility-and-risk).**
460
-
461
- - One account's own daily grant, idempotent: a check-in reads the ledger, touches
462
- the sign-in endpoint, and reads the ledger again — three requests. A day already
463
- claimed (a retry, a later ask) answers `done` in two; a touch the ledger has not
464
- caught up with reads it once more, four. Once a day.
465
- Asks that arrive before the window open send nothing at all.
466
- The usage read makes two requests and never touches the sign-in endpoint, so the
467
- menu bar's polling cannot turn into check-ins.
468
- - Every request gives up after 8 seconds, and a response body is read to at most
469
- 256 KiB; neither can hold the host or grow without bound. The timeout covers the
470
- body as well as the request: a proxy that stops mid-reply is cut at the same
471
- 8 seconds, and says so rather than passing the runtime's own word for it
472
- through (`terminated` on Node, a sentence about `verbose: true` on Bun). A whole
473
- check-in gives up after 25 seconds and a whole usage read after 18, because
474
- magpie runs every provider's check-ins under one two-minute budget and asks
475
- accounts one at a time — a hook cut short answers `failed`, which the host
476
- retries in half an hour, and that retry reads the ledger before claiming, so
477
- nothing is lost.
478
- - **A body it does not understand fails; it never reads as zero.** No `data`, a
479
- `data` that is a list rather than an object, a balance with no
480
- `available_balance` or one that is not a number — each is a failure with the
481
- reason, because "0 魔粒可用" is a definite answer that would be false. The same
482
- goes for the server's own text: a message or a grant's title carrying a key —
483
- this account's token, or one of another vendor's, which this plugin never held
484
- and therefore cannot compare against — has it masked before the text reaches
485
- the card or magpie's log. What counts as a key there is the same rule `--from`
486
- uses, so a ledger title or a server message of ordinary words is untouched.
487
- - `MAGPIE_MSMOLI_CONFIG` overrides the config file's path. It exists so the test
488
- suite never touches a real file; it works in production too.
489
- - The other 170 魔粒 a day come from liking 10 items (20), posting AIGC works
490
- (100) and AIGC models (50). None of those have an API, and automating them
491
- 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.
492
669
 
493
670
  ## Troubleshooting
494
671
 
495
672
  | Symptom | Fix |
496
673
  |---|---|
497
- | `not signed in` | `printf 'ms-token\n' \| magpie plugin login msmoli` |
498
- | No 魔粒 window on the card, or no check-in | restart magpie (the plugin host caches) |
499
- | `failed` with HTTP 401 | the token was revoked, or it belongs to the other site — `modelscope.cn` and `modelscope.ai` do not share tokens. The card names which site refused (国内站 / 国际站). Make a new token on that site and sign in again |
500
- | `failed` with HTTP 403 | ModelScope's risk control refused the request; the token is fine. It retries in 30 minutes |
501
- | 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 |
502
- | 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 |
503
- | 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 |
504
- | 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) |
505
- | Edits to this plugin do nothing | path plugins are referenced in place; restart magpie |
506
- | 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. |
507
685
 
508
686
  ## License
509
687
 
510
- MIT
688
+ [MIT](LICENSE). Contributions welcome; see [CONTRIBUTING.md](CONTRIBUTING.md).