magpie-plugin-msmoli 0.1.0 → 0.2.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.
- package/README.md +83 -6
- package/README.zh-CN.md +22 -4
- package/index.mjs +97 -50
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -18,11 +18,55 @@ Daily grants are **24 hours only**. They do not accumulate: what you don't
|
|
|
18
18
|
spend today is gone. A flagship model costs 2 魔粒 per call (mainstream 1,
|
|
19
19
|
lightweight 0.5), so the daily 250 is roughly 125 calls.
|
|
20
20
|
|
|
21
|
+
## Purpose, responsibility, and risk
|
|
22
|
+
|
|
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
|
|
25
|
+
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
|
|
27
|
+
`plugin-auth.json`, and every request goes to the site the account signed in
|
|
28
|
+
to.
|
|
29
|
+
|
|
30
|
+
**The risk you are taking.** ModelScope has no check-in endpoint. The daily
|
|
31
|
+
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
|
|
33
|
+
posing as the browser, not a button the official client offers. ModelScope's
|
|
34
|
+
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
|
|
37
|
+
account is real-name verified and often tied to Aliyun, so what is at stake is
|
|
38
|
+
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.
|
|
42
|
+
|
|
43
|
+
**Responsibility.** You are responsible for complying with ModelScope's terms
|
|
44
|
+
when you use this. The software is provided under the MIT licence, without
|
|
45
|
+
warranty of any kind; the maintainer is not liable for a suspended or frozen
|
|
46
|
+
account, lost credits, or anything else that follows from running it. See
|
|
47
|
+
[LICENSE](LICENSE).
|
|
48
|
+
|
|
49
|
+
**Not in the magpie market, and it will not be submitted.** magpie's community
|
|
50
|
+
market declined to list this package for exactly the reason above
|
|
51
|
+
([plugins#714](https://github.com/magpie-community/plugins/pull/714)), and the
|
|
52
|
+
project has decided not to submit it again. It stays installable by name from
|
|
53
|
+
npm.
|
|
54
|
+
|
|
21
55
|
## Install
|
|
22
56
|
|
|
23
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:
|
|
24
58
|
`/Applications/magpie.app/Contents/MacOS/magpie`.
|
|
25
59
|
|
|
60
|
+
### Install from npm
|
|
61
|
+
|
|
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.
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
magpie plugin add magpie-plugin-msmoli
|
|
67
|
+
printf 'ms-your-token\n' | magpie plugin login msmoli
|
|
68
|
+
```
|
|
69
|
+
|
|
26
70
|
### Install via GitHub URL (no clone, auto-updates)
|
|
27
71
|
|
|
28
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`.
|
|
@@ -52,12 +96,34 @@ The sign-in is kept per provider id (`msmoli`) rather than per folder, so
|
|
|
52
96
|
switching between the two forms (or moving the clone) keeps the token — no
|
|
53
97
|
second login.
|
|
54
98
|
|
|
99
|
+
### International site (modelscope.ai)
|
|
100
|
+
|
|
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.
|
|
105
|
+
|
|
106
|
+
The sign-in and check-in work the same way:
|
|
107
|
+
|
|
108
|
+
```bash
|
|
109
|
+
printf 'ms-your-intl-token\n' | magpie plugin login msmoli-intl
|
|
110
|
+
```
|
|
111
|
+
|
|
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).
|
|
116
|
+
|
|
55
117
|
### Already have ModelScope set up in magpie? Sync that key to the plugin
|
|
56
118
|
|
|
57
119
|
If you added ModelScope earlier just for chat, its key is already in
|
|
58
120
|
`providers.json`. This package ships a small CLI that logs the same token into
|
|
59
121
|
the plugin, so you don't have to find it again.
|
|
60
122
|
|
|
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`).
|
|
126
|
+
|
|
61
127
|
Installed from GitHub, magpie keeps its own copy in its plugin folder:
|
|
62
128
|
|
|
63
129
|
```bash
|
|
@@ -73,9 +139,9 @@ installs keep the same layout under their `data` folder; if detection misses,
|
|
|
73
139
|
where things are: for a portable install that is its `data` folder, not the
|
|
74
140
|
folder the executable sits in. `--magpie` takes a path, absolute or relative —
|
|
75
141
|
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`
|
|
77
|
-
|
|
78
|
-
don't start from that.
|
|
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.
|
|
79
145
|
|
|
80
146
|
`--dir` says where to *read*; magpie's CLI has no such flag, so a write always
|
|
81
147
|
lands in the binary's own folder. Given both and they disagree, the tool stops
|
|
@@ -137,7 +203,9 @@ magpie):
|
|
|
137
203
|
|
|
138
204
|
The token comes from <https://modelscope.cn/my/myaccesstoken>. The same `ms-`
|
|
139
205
|
token is both the inference `Authorization: Bearer` and the web session cookie,
|
|
140
|
-
so nothing else needs configuring.
|
|
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.
|
|
141
209
|
|
|
142
210
|
From then on magpie claims it once a Beijing day per signed-in account, retries
|
|
143
211
|
later that day if it fails, and reports the outcome on the account's usage card:
|
|
@@ -361,13 +429,19 @@ The one real overlap to know about: **both rows cost nothing extra**, but if you
|
|
|
361
429
|
remove the plugin's sign-in, chat is unaffected, and if you remove the preset,
|
|
362
430
|
the check-in keeps working. They fail independently.
|
|
363
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.
|
|
436
|
+
|
|
364
437
|
## Privacy
|
|
365
438
|
|
|
366
439
|
`login/info` responds with `Email` and `GitlabAccessToken` among other fields.
|
|
367
440
|
This plugin reads `NickName` / `Name` and nothing else; the rest is dropped as
|
|
368
441
|
soon as the response is parsed. The token lives only in magpie's own
|
|
369
442
|
`plugin-auth.json` — the plugin never writes it anywhere, never logs it, never
|
|
370
|
-
sends it anywhere but `modelscope.cn
|
|
443
|
+
sends it anywhere but the site the account was signed in for (`modelscope.cn`
|
|
444
|
+
for `msmoli`, `modelscope.ai` for `msmoli-intl`).
|
|
371
445
|
|
|
372
446
|
The one thing it writes is that nickname, as `accountId` on the sign-in it
|
|
373
447
|
already has, so magpie's card can name the account. It goes through the host's
|
|
@@ -381,6 +455,9 @@ nothing — your name stays, and the nickname is not put over it.
|
|
|
381
455
|
|
|
382
456
|
## Scope and limits
|
|
383
457
|
|
|
458
|
+
**ModelScope's terms and the risks of running this are stated above, in
|
|
459
|
+
[Purpose, responsibility, and risk](#purpose-responsibility-and-risk).**
|
|
460
|
+
|
|
384
461
|
- One account's own daily grant, idempotent: a check-in reads the ledger, touches
|
|
385
462
|
the sign-in endpoint, and reads the ledger again — three requests. A day already
|
|
386
463
|
claimed (a retry, a later ask) answers `done` in two; a touch the ledger has not
|
|
@@ -419,7 +496,7 @@ uses, so a ledger title or a server message of ordinary words is untouched.
|
|
|
419
496
|
|---|---|
|
|
420
497
|
| `not signed in` | `printf 'ms-token\n' \| magpie plugin login msmoli` |
|
|
421
498
|
| No 魔粒 window on the card, or no check-in | restart magpie (the plugin host caches) |
|
|
422
|
-
| `failed` with HTTP 401 | the token was revoked —
|
|
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 |
|
|
423
500
|
| `failed` with HTTP 403 | ModelScope's risk control refused the request; the token is fine. It retries in 30 minutes |
|
|
424
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 |
|
|
425
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 |
|
package/README.zh-CN.md
CHANGED
|
@@ -2,6 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
一个 [magpie](https://github.com/yetone/magpie) 插件,替你领取 ModelScope(魔搭)的每日**魔粒**——每天自动签到一次,代替人工打开网页——并在账号卡片上显示余额与今日消耗。
|
|
4
4
|
|
|
5
|
+
> **Before you install:** new material is written in English only, and this
|
|
6
|
+
> translation does not cover the English README's "Purpose, responsibility,
|
|
7
|
+
> and risk" section. Read
|
|
8
|
+
> [Purpose, responsibility, and risk](README.md#purpose-responsibility-and-risk)
|
|
9
|
+
> in the English README first — it states what the plugin is for, the risk to
|
|
10
|
+
> your ModelScope account, and that the project is not listed in magpie's
|
|
11
|
+
> market and will not be submitted to it.
|
|
12
|
+
|
|
5
13
|
## 魔粒是什么
|
|
6
14
|
|
|
7
15
|
ModelScope 的 API-Inference 是免费的,用魔粒结算:
|
|
@@ -19,7 +27,17 @@ ModelScope 的 API-Inference 是免费的,用魔粒结算:
|
|
|
19
27
|
需要先装好 magpie(<https://usemagpie.ai>)。macOS 上用 `magpie --version` 确认命令在 `PATH` 上;
|
|
20
28
|
不在的话用 app 里的完整路径 `/Applications/magpie.app/Contents/MacOS/magpie`。
|
|
21
29
|
|
|
22
|
-
###
|
|
30
|
+
### 方式一:从 npm 装(推荐,能跟着版本更新)
|
|
31
|
+
|
|
32
|
+
包已发布在 npm:`magpie-plugin-msmoli`(最新 0.1.0)。装进 magpie 自己的插件目录,
|
|
33
|
+
`magpie plugin update` 跟着版本更新:
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
magpie plugin add magpie-plugin-msmoli
|
|
37
|
+
printf 'ms-你的令牌\n' | magpie plugin login msmoli
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
### 方式二:直接从 GitHub 装(不用 clone,能跟着更新)
|
|
23
41
|
|
|
24
42
|
装进 magpie 自己的插件目录(安装版是 `~/.config/magpie/plugins`,便携版是 data 目录下的 `plugins`),
|
|
25
43
|
以后 `magpie plugin update` 就能更新:
|
|
@@ -29,7 +47,7 @@ magpie plugin add github:MisonL/magpie-plugin-msmoli
|
|
|
29
47
|
printf 'ms-你的令牌\n' | magpie plugin login msmoli
|
|
30
48
|
```
|
|
31
49
|
|
|
32
|
-
###
|
|
50
|
+
### 方式三:clone 到本地再装
|
|
33
51
|
|
|
34
52
|
留一份自己的副本,打算改代码就用这种。magpie 是**按位置引用**这个文件夹而不是拷走,
|
|
35
53
|
所以文件夹不在(比如外置盘没挂)插件就加载不了,更新也得先 pull 再 add 一次:
|
|
@@ -61,8 +79,8 @@ node ~/.config/magpie/plugins/node_modules/magpie-plugin-msmoli/bin/sync.mjs --t
|
|
|
61
79
|
便携版的目录结构相同,只是根目录是它的 `data` 文件夹;探测不到时用 `--dir <magpie 的配置
|
|
62
80
|
目录>`(安装版是 `~/.config/magpie`,便携版是它的 `data` 目录)和 `--magpie <magpie 可执行
|
|
63
81
|
文件的路径>` 指明。`--magpie` 要的是一个路径,绝对或相对都行;裸名 `magpie` 会被拒绝——
|
|
64
|
-
那个是按 `PATH` 解析的,PATH 上第一个 magpie 会拿到令牌。`npx magpie-plugin-msmoli`
|
|
65
|
-
|
|
82
|
+
那个是按 `PATH` 解析的,PATH 上第一个 magpie 会拿到令牌。`npx magpie-plugin-msmoli` 可以直接用
|
|
83
|
+
(已发布到 npm,0.1.0)——npm 12 默认禁用 `npx github:…`,别从那条路开始。
|
|
66
84
|
|
|
67
85
|
`--dir` 只决定**读**哪里;magpie 的 CLI 没有这个参数,写入永远落在可执行文件自己的目录。
|
|
68
86
|
两个都给了而指向**不同的地方**时,工具会在写之前停下,而不是读一个 magpie、写进另一个。
|
package/index.mjs
CHANGED
|
@@ -1,21 +1,47 @@
|
|
|
1
|
-
// ModelScope (魔搭) 魔粒 check-in for magpie
|
|
1
|
+
// ModelScope (魔搭) 魔粒 check-in for magpie — one provider per site:
|
|
2
|
+
// modelscope.cn (`msmoli`) and modelscope.ai (`msmoli-intl`).
|
|
2
3
|
//
|
|
3
4
|
// magpie calls auth.checkin once a Beijing day per signed-in account while the
|
|
4
5
|
// provider's check-in switch is on, and again later that day after a failure.
|
|
5
6
|
// Hooks here: checkin, usage.
|
|
6
7
|
//
|
|
7
|
-
//
|
|
8
|
-
// GET /api/v1/users/login/info
|
|
9
|
-
//
|
|
8
|
+
// How the claim works: ModelScope's OpenAPI has no check-in endpoint. What it
|
|
9
|
+
// has is a side effect — GET /api/v1/users/login/info counts as a website
|
|
10
|
+
// sign-in, and the server grants that day's credits with it (200 for the
|
|
11
|
+
// "注册并登录" daily grant, 50 for a bound Aliyun account), once per day.
|
|
12
|
+
// modelscope.ai is the same backend with the same paths and the same
|
|
13
|
+
// daily_active rule, but separate accounts and separate tokens: measured, an
|
|
14
|
+
// ms- token authenticates only on the site it came from.
|
|
10
15
|
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
16
|
+
// The plugin serves no chat models: inference keeps using the API-key modelscope
|
|
17
|
+
// preset in providers.json. Both providers exist only to carry the sign-in the
|
|
18
|
+
// check-in runs as.
|
|
13
19
|
|
|
14
20
|
import { readFileSync } from "node:fs"
|
|
15
21
|
import { homedir } from "node:os"
|
|
16
22
|
import { isAbsolute, join } from "node:path"
|
|
17
23
|
|
|
18
|
-
|
|
24
|
+
// A profile is everything that differs between the two deployments: the
|
|
25
|
+
// origin, the provider id (magpie keys the sign-in, the card and the alias by
|
|
26
|
+
// it), and the words on the card. The rest of this module is shared — the two
|
|
27
|
+
// sites answer with the same envelopes, the same magicubes rules and the same
|
|
28
|
+
// 24-hour expiry, so a second copy of the hooks would only be a second place
|
|
29
|
+
// for them to drift.
|
|
30
|
+
const CN = {
|
|
31
|
+
site: "https://modelscope.cn",
|
|
32
|
+
id: "msmoli",
|
|
33
|
+
name: "ModelScope 魔粒",
|
|
34
|
+
plan: "魔搭社区",
|
|
35
|
+
where: "国内站",
|
|
36
|
+
}
|
|
37
|
+
const INTL = {
|
|
38
|
+
site: "https://modelscope.ai",
|
|
39
|
+
id: "msmoli-intl",
|
|
40
|
+
name: "ModelScope 魔粒(国际版)",
|
|
41
|
+
plan: "魔搭社区(国际版)",
|
|
42
|
+
where: "国际站",
|
|
43
|
+
}
|
|
44
|
+
|
|
19
45
|
const LOGIN_INFO = "/api/v1/users/login/info"
|
|
20
46
|
const BALANCE = "/openapi/v1/magicubes/balance"
|
|
21
47
|
const TXNS = "/openapi/v1/magicubes/transactions"
|
|
@@ -26,8 +52,6 @@ const UA =
|
|
|
26
52
|
// 每日发放的规则。这类魔粒 24 小时过期,不累积。
|
|
27
53
|
const DAILY_RULES = ["daily_active", "aliyun_bindlogin"]
|
|
28
54
|
|
|
29
|
-
const ID = "msmoli"
|
|
30
|
-
|
|
31
55
|
const TIMEOUT_MS = 8000
|
|
32
56
|
// PAGE_SIZE is one page of the ledger, and one page is all that is read. The
|
|
33
57
|
// ledger is aggregated — one row per (day, type, rule_key, model_tier), with a
|
|
@@ -127,7 +151,7 @@ function saidFailure(e, ctl, budget) {
|
|
|
127
151
|
return e
|
|
128
152
|
}
|
|
129
153
|
|
|
130
|
-
async function json(token, path, deadline = Infinity) {
|
|
154
|
+
async function json(site, token, path, deadline = Infinity) {
|
|
131
155
|
const left = deadline - Date.now()
|
|
132
156
|
if (left <= 0) throw new Error("预算已用完,本次请求未发出")
|
|
133
157
|
const budget = Math.min(TIMEOUT_MS, left)
|
|
@@ -144,15 +168,15 @@ async function json(token, path, deadline = Infinity) {
|
|
|
144
168
|
try {
|
|
145
169
|
const res = await Promise.race([
|
|
146
170
|
aborted,
|
|
147
|
-
fetch(
|
|
171
|
+
fetch(site + path, {
|
|
148
172
|
// every redirect is refused, the site's own included: one that left
|
|
149
|
-
//
|
|
173
|
+
// the origin would carry the session cookie off-site (the runtimes
|
|
150
174
|
// happen not to forward it), and ModelScope's endpoints answer in place
|
|
151
175
|
redirect: "error",
|
|
152
176
|
headers: {
|
|
153
177
|
Cookie: "m_session_id=" + token,
|
|
154
178
|
"User-Agent": UA,
|
|
155
|
-
Referer:
|
|
179
|
+
Referer: site + "/",
|
|
156
180
|
Accept: "application/json",
|
|
157
181
|
},
|
|
158
182
|
signal: ctl.signal,
|
|
@@ -502,7 +526,10 @@ function labelOf(auth, token) {
|
|
|
502
526
|
// the user's name keyed to a label nothing carries any more — it would stop
|
|
503
527
|
// showing, with no sign of why. A file that is mid-write, unreadable, or not
|
|
504
528
|
// the shape we know must therefore count as "don't write", not as "no alias".
|
|
505
|
-
|
|
529
|
+
//
|
|
530
|
+
// `id` is the provider's own: both sites keep their accounts in the same
|
|
531
|
+
// providers.json, and an alias is only this plugin's when the row's id says so.
|
|
532
|
+
function aliasOf(id, dir, label) {
|
|
506
533
|
// no directory is not "the user has no alias": it is having nowhere to look
|
|
507
534
|
if (!dir || !label) return undefined
|
|
508
535
|
let raw
|
|
@@ -531,7 +558,7 @@ function aliasOf(dir, label) {
|
|
|
531
558
|
for (const list of lists) {
|
|
532
559
|
if (!Array.isArray(list)) continue
|
|
533
560
|
for (const p of list) {
|
|
534
|
-
if (p?.id !==
|
|
561
|
+
if (p?.id !== id) continue
|
|
535
562
|
const names = p.accountNames
|
|
536
563
|
if (!names || typeof names !== "object") continue
|
|
537
564
|
const mine = names[key]
|
|
@@ -832,7 +859,7 @@ const unusableName = (name, token) =>
|
|
|
832
859
|
nameIsKey(name, token) ||
|
|
833
860
|
["__proto__", "constructor", "prototype"].includes(name)
|
|
834
861
|
|
|
835
|
-
async function rememberAccountName(client, token, name, auth, dir) {
|
|
862
|
+
async function rememberAccountName(profile, client, token, name, auth, dir) {
|
|
836
863
|
// "can write" is a function, not a truthy value: `{ auth: { set: 1 } }` is a
|
|
837
864
|
// client that cannot write, and `!client?.auth?.set` let it through to call
|
|
838
865
|
// a number — the TypeError is caught below, but it logs "account name not
|
|
@@ -846,7 +873,7 @@ async function rememberAccountName(client, token, name, auth, dir) {
|
|
|
846
873
|
// it would re-key their alias to a label the account no longer has. A
|
|
847
874
|
// providers.json that can't be read says the same thing — don't write over
|
|
848
875
|
// a state we can't see.
|
|
849
|
-
if (aliasOf(dir, labelOf(auth, token)) !== "") return
|
|
876
|
+
if (aliasOf(profile.id, dir, labelOf(auth, token)) !== "") return
|
|
850
877
|
named.delete(token)
|
|
851
878
|
named.set(token, Date.now())
|
|
852
879
|
while (named.size > NAMES_MAX) named.delete(named.keys().next().value)
|
|
@@ -854,7 +881,7 @@ async function rememberAccountName(client, token, name, auth, dir) {
|
|
|
854
881
|
// auth.set replaces the whole sign-in, so what the host handed us is the
|
|
855
882
|
// base: a field we do not know about is kept, not dropped
|
|
856
883
|
const body = { ...(auth && typeof auth === "object" ? auth : {}), type: "api", key: token, accountId: name }
|
|
857
|
-
await client.auth.set({ path: { id:
|
|
884
|
+
await client.auth.set({ path: { id: profile.id }, body })
|
|
858
885
|
} catch (e) {
|
|
859
886
|
// the card keeps the key's tail; nothing else depends on this. Put the
|
|
860
887
|
// token back so the next ask tries the write again, and let the log call
|
|
@@ -865,7 +892,7 @@ async function rememberAccountName(client, token, name, auth, dir) {
|
|
|
865
892
|
// of it before it reaches magpie's log
|
|
866
893
|
const why = cardText(whyOf(e), token)
|
|
867
894
|
try {
|
|
868
|
-
const r = client?.app?.log?.({ body: { level: "warn", service:
|
|
895
|
+
const r = client?.app?.log?.({ body: { level: "warn", service: profile.id, message: `account name not written back: ${why}` } })
|
|
869
896
|
if (r && typeof r.catch === "function") r.catch(() => {})
|
|
870
897
|
} catch {
|
|
871
898
|
// a host whose log throws synchronously must not turn a successful
|
|
@@ -888,13 +915,15 @@ const whyOf = (e) =>
|
|
|
888
915
|
// used to exist for one condition ("令牌失效或不属于国内站", the same plus
|
|
889
916
|
// "(HTTP 401)", and a bare "HTTP 401" from the balance/ledger path), and the
|
|
890
917
|
// README's troubleshooting row promises the user will see the status code in
|
|
891
|
-
// it — which only two of the three carried.
|
|
892
|
-
|
|
918
|
+
// it — which only two of the three carried. `where` is the profile's 国内站/
|
|
919
|
+
// 国际站, and stays in the one wording so the card says which site refused
|
|
920
|
+
// the token.
|
|
921
|
+
const expired = (where) => `令牌失效或不属于${where}(HTTP 401)`
|
|
893
922
|
|
|
894
923
|
// deadToken is what every 401 becomes: the token is revoked, and magpie only
|
|
895
924
|
// signs the account out when the hook says so. Both call sites guard
|
|
896
925
|
// `status === 401` before throwing this, so there is no other status to name.
|
|
897
|
-
const deadToken = () => Object.assign(new Error(
|
|
926
|
+
const deadToken = (where) => Object.assign(new Error(expired(where)), { signIn: "expired" })
|
|
898
927
|
|
|
899
928
|
// RISK is what every 403 says — ModelScope's risk control, not a bad token:
|
|
900
929
|
// the account may be perfectly fine and only the request was refused. The
|
|
@@ -907,15 +936,16 @@ const RISK = "被魔搭风控拒绝(HTTP 403)"
|
|
|
907
936
|
// usage's ledger) do not each carry their own copy of the four checks. `what`
|
|
908
937
|
// names the read in the message ("账单查询失败" / "余额查询失败" / "登录接口
|
|
909
938
|
// 失败"); `risk` is the 403 wording, which the touch alone extends with the
|
|
910
|
-
// retry it can promise.
|
|
939
|
+
// retry it can promise. `profile` is there for the one site-specific word a
|
|
940
|
+
// 401 carries (国内站/国际站).
|
|
911
941
|
//
|
|
912
942
|
// The parse check and the status check are separate on purpose: a body that
|
|
913
943
|
// does not parse is not JSON; a body that parses and carries a 429/500 is the
|
|
914
944
|
// server saying why in its own words (「请求过于频繁」「系统繁忙」). Merged,
|
|
915
945
|
// a JSON-carrying 429 was reported as "响应不是 JSON" and sent the user to
|
|
916
946
|
// check their proxy.
|
|
917
|
-
const gate = (res, body, parsed, token, what, risk = RISK) => {
|
|
918
|
-
if (res.status === 401) throw deadToken()
|
|
947
|
+
const gate = (profile, res, body, parsed, token, what, risk = RISK) => {
|
|
948
|
+
if (res.status === 401) throw deadToken(profile.where)
|
|
919
949
|
if (res.status === 403) throw new Error(risk)
|
|
920
950
|
if (!parsed) throw new Error(`${what}:响应不是 JSON(HTTP ${res.status})`)
|
|
921
951
|
if (res.status !== 200 || !okOf(body)) throw new Error(`${what}:${saidOf(body, token) || `HTTP ${res.status}`}`)
|
|
@@ -925,9 +955,9 @@ const gate = (res, body, parsed, token, what, risk = RISK) => {
|
|
|
925
955
|
// The day is read at each call, on purpose: a check-in that crosses midnight
|
|
926
956
|
// between its reads reports what its touch granted on the new day, which is
|
|
927
957
|
// true, where diffing both reads against the old day would answer inactive.
|
|
928
|
-
async function todayEarn(token, deadline) {
|
|
929
|
-
const { res, body, parsed } = await json(token, `${TXNS}?page=1&page_size=${PAGE_SIZE}`, deadline)
|
|
930
|
-
gate(res, body, parsed, token, "账单查询失败")
|
|
958
|
+
async function todayEarn(profile, token, deadline) {
|
|
959
|
+
const { res, body, parsed } = await json(profile.site, token, `${TXNS}?page=1&page_size=${PAGE_SIZE}`, deadline)
|
|
960
|
+
gate(profile, res, body, parsed, token, "账单查询失败")
|
|
931
961
|
const day = todayCN()
|
|
932
962
|
return recordsOf(body).filter((r) => r?.type === "EARN" && onDay(r?.gmt_created, day))
|
|
933
963
|
}
|
|
@@ -996,7 +1026,11 @@ const yin = (n) => {
|
|
|
996
1026
|
|
|
997
1027
|
const sumOf = (records) => yin(records.reduce((n, r) => n + amountOf(r), 0))
|
|
998
1028
|
|
|
999
|
-
|
|
1029
|
+
// pluginFor is the whole plugin, with the one profile it serves passed in.
|
|
1030
|
+
// magpie calls every exported function as its own plugin entry, so the two
|
|
1031
|
+
// exports below are two providers over this one body: same hooks, same
|
|
1032
|
+
// wording, one place for both to drift from.
|
|
1033
|
+
async function pluginFor(profile, input) {
|
|
1000
1034
|
// input.directory is magpie's own folder (host.go passes settings.Dir()),
|
|
1001
1035
|
// which is where a portable magpie keeps everything too.
|
|
1002
1036
|
const configDir = typeof input?.directory === "string" ? input.directory : ""
|
|
@@ -1007,7 +1041,7 @@ export async function MsmoliPlugin(input) {
|
|
|
1007
1041
|
|
|
1008
1042
|
return {
|
|
1009
1043
|
auth: {
|
|
1010
|
-
provider:
|
|
1044
|
+
provider: profile.id,
|
|
1011
1045
|
|
|
1012
1046
|
// 静态令牌签到:粘贴一把 ms- 开头的访问令牌即可。
|
|
1013
1047
|
//
|
|
@@ -1086,7 +1120,7 @@ export async function MsmoliPlugin(input) {
|
|
|
1086
1120
|
let name = ""
|
|
1087
1121
|
const answer = await (async () => {
|
|
1088
1122
|
try {
|
|
1089
|
-
const before = await todayEarn(token, deadline)
|
|
1123
|
+
const before = await todayEarn(profile, token, deadline)
|
|
1090
1124
|
const beforeDaily = before.filter((r) => DAILY_RULES.includes(r?.rule_key))
|
|
1091
1125
|
const claimed = before.some((r) => r?.rule_key === "daily_active")
|
|
1092
1126
|
// What this run granted is the ledger's growth, not a new key: the
|
|
@@ -1129,9 +1163,9 @@ export async function MsmoliPlugin(input) {
|
|
|
1129
1163
|
if (claimed && known) {
|
|
1130
1164
|
name = known
|
|
1131
1165
|
} else {
|
|
1132
|
-
const { res, body, parsed } = await json(token, LOGIN_INFO, deadline)
|
|
1166
|
+
const { res, body, parsed } = await json(profile.site, token, LOGIN_INFO, deadline)
|
|
1133
1167
|
// 403 是风控,不是令牌坏了:账号可能是好的,只是这次请求被拒
|
|
1134
|
-
gate(res, body, parsed, token, "登录接口失败", `${RISK},稍后会自动重试`)
|
|
1168
|
+
gate(profile, res, body, parsed, token, "登录接口失败", `${RISK},稍后会自动重试`)
|
|
1135
1169
|
|
|
1136
1170
|
const me = dataOf(body)
|
|
1137
1171
|
// A name is a string. Anything else — a number, an object, a list —
|
|
@@ -1178,7 +1212,7 @@ export async function MsmoliPlugin(input) {
|
|
|
1178
1212
|
return { credit, which }
|
|
1179
1213
|
}
|
|
1180
1214
|
|
|
1181
|
-
const daily = gained((await todayEarn(token, deadline)).filter((r) => DAILY_RULES.includes(r?.rule_key)))
|
|
1215
|
+
const daily = gained((await todayEarn(profile, token, deadline)).filter((r) => DAILY_RULES.includes(r?.rule_key)))
|
|
1182
1216
|
if (daily) return { outcome: "claimed", credit: daily.credit, message: `领取 ${daily.credit} 魔粒:${daily.which}` }
|
|
1183
1217
|
// 触达了却没有到账记录:可能是账本还没写出来(发放是异步的),
|
|
1184
1218
|
// 也可能这个账号本来就不在活动范围。再读一次账本确认,别把刚写
|
|
@@ -1187,7 +1221,7 @@ export async function MsmoliPlugin(input) {
|
|
|
1187
1221
|
// 是宿主:还没见到领取的 done/inactive 是暂定答案,每 30 分钟重问
|
|
1188
1222
|
// (provisional,由 checkinSureHour 决定)。12 点起宿主就当天了结,
|
|
1189
1223
|
// 所以下午的时段由插件自己用 until 要一次半小时后的重问。
|
|
1190
|
-
const later = gained((await todayEarn(token, deadline)).filter((r) => DAILY_RULES.includes(r?.rule_key)))
|
|
1224
|
+
const later = gained((await todayEarn(profile, token, deadline)).filter((r) => DAILY_RULES.includes(r?.rule_key)))
|
|
1191
1225
|
if (later) return { outcome: "claimed", credit: later.credit, message: `领取 ${later.credit} 魔粒:${later.which}` }
|
|
1192
1226
|
const hourCN = Math.floor(minuteCN() / 60)
|
|
1193
1227
|
if (hourCN >= SURE_HOUR && deferred.get(token) !== today) {
|
|
@@ -1208,7 +1242,7 @@ export async function MsmoliPlugin(input) {
|
|
|
1208
1242
|
// catches its own failures, and anything it could still throw (an
|
|
1209
1243
|
// unreadable providers.json read) must not undo the answer above
|
|
1210
1244
|
try {
|
|
1211
|
-
await rememberAccountName(client, token, name, auth, configDir)
|
|
1245
|
+
await rememberAccountName(profile, client, token, name, auth, configDir)
|
|
1212
1246
|
} catch {}
|
|
1213
1247
|
return answer
|
|
1214
1248
|
},
|
|
@@ -1232,9 +1266,9 @@ export async function MsmoliPlugin(input) {
|
|
|
1232
1266
|
// Inside the try: the hook promises an {error}, never a rejection,
|
|
1233
1267
|
// and this path reaches client.auth.set.
|
|
1234
1268
|
try {
|
|
1235
|
-
await rememberAccountName(client, token, names.get(token) ?? "", auth, configDir)
|
|
1236
|
-
const bal = await json(token, BALANCE, deadline)
|
|
1237
|
-
gate(bal.res, bal.body, bal.parsed, token, "余额查询失败")
|
|
1269
|
+
await rememberAccountName(profile, client, token, names.get(token) ?? "", auth, configDir)
|
|
1270
|
+
const bal = await json(profile.site, token, BALANCE, deadline)
|
|
1271
|
+
gate(profile, bal.res, bal.body, bal.parsed, token, "余额查询失败")
|
|
1238
1272
|
// data 缺失读成 0 会把「没读懂」翻译成「余额为零」——一个确定又
|
|
1239
1273
|
// 错误的结论,卡片会写「0 魔粒可用」。账本那侧的 recordsOf 同理。
|
|
1240
1274
|
// 字段这一层也一样:available_balance 缺失、null、或改了名字,
|
|
@@ -1259,8 +1293,8 @@ export async function MsmoliPlugin(input) {
|
|
|
1259
1293
|
// below zero, and "-5 魔粒可用" is not a thing the card can show
|
|
1260
1294
|
const available = Math.max(0, yin(availableN))
|
|
1261
1295
|
|
|
1262
|
-
const tx = await json(token, `${TXNS}?page=1&page_size=${PAGE_SIZE}`, deadline)
|
|
1263
|
-
gate(tx.res, tx.body, tx.parsed, token, "账单查询失败")
|
|
1296
|
+
const tx = await json(profile.site, token, `${TXNS}?page=1&page_size=${PAGE_SIZE}`, deadline)
|
|
1297
|
+
gate(profile, tx.res, tx.body, tx.parsed, token, "账单查询失败")
|
|
1264
1298
|
|
|
1265
1299
|
const day = todayCN()
|
|
1266
1300
|
let earned = 0
|
|
@@ -1340,7 +1374,7 @@ export async function MsmoliPlugin(input) {
|
|
|
1340
1374
|
const hint = ahead && !ahead.due ? ` · ${clock(ahead.target)} 领取` : ""
|
|
1341
1375
|
|
|
1342
1376
|
return {
|
|
1343
|
-
plan:
|
|
1377
|
+
plan: profile.plan,
|
|
1344
1378
|
user: names.get(token) ?? "",
|
|
1345
1379
|
balance: `${available} 魔粒可用`,
|
|
1346
1380
|
windows: [
|
|
@@ -1367,27 +1401,40 @@ export async function MsmoliPlugin(input) {
|
|
|
1367
1401
|
async config(config) {
|
|
1368
1402
|
// 宿主是 `{ provider: {}, ...userConfig }` 拼出来的,用户配置里那个
|
|
1369
1403
|
// provider 会盖掉默认的 {}。它不是对象时(旧版本写的字符串、手改的
|
|
1370
|
-
// 数字),下面 `[
|
|
1371
|
-
//
|
|
1372
|
-
// null/undefined,挡不住 ""、0、false;一个装不下
|
|
1373
|
-
//
|
|
1404
|
+
// 数字),下面 `[profile.id] =` 会抛 TypeError,宿主只把 config 钩子
|
|
1405
|
+
// 的错误记进日志——插件照常出现,只是没有名字,看不出哪里错了。
|
|
1406
|
+
// `??=` 只挡 null/undefined,挡不住 ""、0、false;一个装不下 id 的
|
|
1407
|
+
// 容器里没有可保留的东西,整个换掉。
|
|
1374
1408
|
if (!config.provider || typeof config.provider !== "object") config.provider = {}
|
|
1375
1409
|
// 用户自己改过的名字留着:magpie 把 provider 的名字存在 providers.json
|
|
1376
1410
|
// 里,而钩子每次启动都跑一遍——强制写回默认值会把 UI 里的改名悄悄撤销,
|
|
1377
1411
|
// 与账号昵称那条「用户命名过就不动」是同一条规矩。值不是对象时当作没有。
|
|
1378
|
-
const was = config.provider[
|
|
1379
|
-
config.provider[
|
|
1412
|
+
const was = config.provider[profile.id] && typeof config.provider[profile.id] === "object" ? config.provider[profile.id] : {}
|
|
1413
|
+
config.provider[profile.id] = {
|
|
1380
1414
|
...was,
|
|
1381
|
-
name: typeof was.name === "string" && was.name ? was.name :
|
|
1415
|
+
name: typeof was.name === "string" && was.name ? was.name : profile.name,
|
|
1382
1416
|
}
|
|
1383
1417
|
},
|
|
1384
1418
|
}
|
|
1385
1419
|
}
|
|
1386
1420
|
|
|
1421
|
+
// Two entries: the domestic site (`msmoli`) and the international one
|
|
1422
|
+
// (`msmoli-intl`). Exporting two functions makes two providers — magpie's
|
|
1423
|
+
// loader takes every exported function as a plugin entry (`testables` is an
|
|
1424
|
+
// object, which it skips). The check-in window in msmoli.json is shared: it is
|
|
1425
|
+
// a personal preference about when to claim, not a per-site setting.
|
|
1426
|
+
export async function MsmoliPlugin(input) {
|
|
1427
|
+
return pluginFor(CN, input)
|
|
1428
|
+
}
|
|
1429
|
+
|
|
1430
|
+
export async function MsmoliIntlPlugin(input) {
|
|
1431
|
+
return pluginFor(INTL, input)
|
|
1432
|
+
}
|
|
1433
|
+
|
|
1387
1434
|
// testables is how the pure helpers the tests pin are reached without
|
|
1388
1435
|
// exporting them as functions: magpie's loader treats every exported function
|
|
1389
1436
|
// as a plugin entry (see windowOf above), but an object it skips. The tests
|
|
1390
1437
|
// are the only reader.
|
|
1391
|
-
export const testables = { windowOf }
|
|
1438
|
+
export const testables = { windowOf, CN, INTL }
|
|
1392
1439
|
|
|
1393
1440
|
export default MsmoliPlugin
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "magpie-plugin-msmoli",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "ModelScope (魔搭) 魔粒 daily check-in and balance for magpie — one check-in a day instead of opening the site",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "ModelScope (魔搭) 魔粒 daily check-in and balance for magpie, for modelscope.cn and modelscope.ai — one check-in a day instead of opening the site",
|
|
5
5
|
"author": "MisonL",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"repository": {
|