dsh-astudio-connect 0.1.1 → 0.2.1

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.en.md CHANGED
@@ -16,7 +16,7 @@ An adaptation of the design in [dsh-workbuddy-connect](https://github.com/corrin
16
16
  - [Model Catalog](#model-catalog)
17
17
  - [Reasoning Effort and Probing](#reasoning-effort-and-probing)
18
18
  - [Model Visibility](#model-visibility)
19
- - [Points Balance Badge](#points-balance-badge)
19
+ - [Points and the daily claim](#points-and-the-daily-claim)
20
20
  - [Billing and Points](#billing-and-points)
21
21
  - [Settings Card](#settings-card)
22
22
  - [DSH Version Support](#dsh-version-support)
@@ -33,7 +33,7 @@ An adaptation of the design in [dsh-workbuddy-connect](https://github.com/corrin
33
33
  ### Prerequisites
34
34
 
35
35
  - Node.js `^22.19.0 || >=24.0.0` installed locally
36
- - The AStudio desktop app installed and signed in (the plugin reuses the app's sign-in state; account switches follow automatically)
36
+ - A xfyun account: sign in with a WeChat QR code in DSH Settings — **AStudio is not required**. If the app is installed and signed in, the plugin reuses that sign-in too.
37
37
  - A DeepSeek Harness runtime — see [DSH Version Support](#dsh-version-support)
38
38
 
39
39
  ### Install the plugin
@@ -75,7 +75,7 @@ Both must hold for the install to count as successful. If the group does not app
75
75
  - **Model catalog**: taken entirely from AStudio's own model-manager endpoint, carrying real context windows and max output; the plugin ships no roster of its own and reports "No models available" when it cannot fetch one.
76
76
  - **Reasoning-effort probing**: for models that declare no levels, sends a minimal sentinel-bearing request under your explicit consent, first confirming the upstream really validates `reasoning_effort`, then which levels it accepts.
77
77
  - **Model visibility**: pick which models appear in the picker from the settings card; a model you switch off leaves the picker immediately, and a session using it must select another before it can send.
78
- - **Balance at a glance**: with an AStudio model selected, the composer shows the account's general and Spark points balances beside the model picker, with a ↻ manual refresh and no automatic polling.
78
+ - **Points and the daily claim**: a new **AStudio points** section in Settings lists the account's general and Spark balances with Refresh and Claim, so neither needs the AStudio client — nor an AStudio model selected.
79
79
  - **Settings card**: the AStudio Connect card under the sidebar Plugins panel, split into Account / Models / Probe sections, with manual catalog refresh and per-model probing.
80
80
  - **Command-line diagnostics**: `status`, `doctor` and `logout` — never emit credential material.
81
81
  - **Update check**: compares against npm's public dist-tags to decide whether a newer release exists, and enriches with GitHub release notes.
@@ -134,12 +134,16 @@ In the Context section of the settings card you can check which models appear in
134
134
 
135
135
  AStudio's token carries no stable uid, so this plugin has a single local identity, `astudio-local` — catalog snapshots, visibility preferences and probe records all hang off it, with no multi-account isolation.
136
136
 
137
- ## Points Balance Badge
137
+ ## Points and the daily claim
138
138
 
139
- With an AStudio model selected, the composer's tool row (beside the model picker) shows the account's two points balances, with a ↻ manual refresh on the right:
139
+ The last row of DSH's Settings navigation is **AStudio points**; opening it shows both balances and both actions:
140
140
 
141
141
  ```
142
- General 1393 · Spark 24621
142
+ General points 1393
143
+ ────────────────────────────────────────
144
+ Spark points 24621
145
+ ────────────────────────────────────────
146
+ One daily login reward per day… [Claim] [Refresh]
143
147
  ```
144
148
 
145
149
  The two balances are shown separately and are **never added together**, because they pay for different things:
@@ -147,30 +151,85 @@ The two balances are shown separately and are **never added together**, because
147
151
  - **General points** (`totalBalance`): every model, plus plugins and Skill calls (AStudio's own plugins and Skills — **not this plugin**).
148
152
  - **Spark points** (`sparkTotalBalance`): Spark-branded models only.
149
153
 
150
- The data comes from the account endpoint the AStudio desktop app itself uses (`https://agent.xfyun.cn/xingchen-studio/points/balance`). AStudio's settings page requests its own loopback service, which forwards to the account API carrying the desktop session cookie; this plugin reaches the same upstream directly using the `ssoSessionId` already present in `astron-session.json`, so it **adds no location discovery and does not require AStudio to be running**.
154
+ The data comes from the account endpoint the AStudio desktop app itself uses (`https://agent.xfyun.cn/xingchen-studio/points/balance`). AStudio's settings page requests its own loopback service, which forwards to the account API carrying the desktop session cookie; this plugin reaches the same upstream directly with that same cookie, so it **does not require AStudio to be running**. The cookie has two possible sources — a sign-in performed here, or the one AStudio wrote into `astron-session.json` — and ours wins; see [WeChat QR sign-in](#wechat-qr-sign-in).
151
155
 
152
156
  > `ssoSessionId`, the `modelBearerToken` used by the catalog, and the chat `experimental_bearer_token` all live in the same file — but they are three credentials for three different purposes.
153
157
 
154
158
  Behaviour guarantees:
155
159
 
156
- - **Read timing**: once when the badge appears, and again when you click ↻. **No timed polling** — every read hits the upstream.
157
- - **Visibility**: appears only when an AStudio model is selected in the current session; no request is issued under a provider-independent model.
158
- - **Never shows a fabricated number on failure**: if the balance cannot be read it degrades instead of rendering "unreadable" as `0`.
159
- - **30 second cache**: successful reads are cached for 30 seconds; ↻ sends `?refresh=1` to bypass the cache. Failures are not cached — the next read asks the upstream again.
160
+ - **Read timing**: once when the section opens, and again when you press **Refresh**. **No timed polling of the balance** — every read hits the upstream. (The one thing asked about on a timer is the sign-in QR code; see [WeChat QR sign-in](#wechat-qr-sign-in).)
161
+ - **Independent of the session**: this page does not look at the selected model, so the balance is readable with no AStudio model selected — even in a session running someone else's model. (The same numbers used to live in the composer badge, which only appeared while that session had an AStudio model selected.)
162
+ - **Fresh on open**: the mount read sends `?refresh=1`, bypassing the host's 30-second cache. Opening Settings is an explicit "how much do I have right now"; answering it with a 30-second-old number would be pointless.
163
+ - **Never shows a fabricated number on failure**: an unreadable balance shows `—` with the reason, never `0`.
164
+ - **Failures are not cached**, so the next read asks the upstream again.
160
165
 
161
166
  | Situation | Display |
162
167
  |---|---|
163
- | Network failure / upstream error | "Unavailable", ↻ retries |
164
- | Sign-in expired (upstream `code: 80000`, or 401/403) | "Sign-in expired" — sign in to AStudio again |
168
+ | Network failure / upstream error | Numbers show `—` with "the account service returned no readable balance"; **Refresh** retries |
169
+ | Sign-in expired (upstream `code: 80000`, or 401/403) | "The AStudio sign-in expired" — sign in to AStudio again |
165
170
  | Account has no Spark pool | Only general points are shown; no `0` |
166
171
  | The balance really is 0 | Shows `0` normally (0 is a real balance, not "unknown") |
167
- | Never signed in to AStudio | The badge does not appear at all |
172
+ | Never signed in to AStudio | "AStudio is not signed in"; **Claim** is disabled (there is no session to book against) |
168
173
 
169
- > **Known limitation**: the account endpoint only provides aggregate balances, with no per-transaction detail — the AStudio desktop app itself is the same way (it directs users to the web to view details). So the badge can reflect *balance changes* but cannot say *exactly how much this conversation consumed*.
174
+ > **Known limitation**: the account endpoint only provides aggregate balances, with no per-transaction detail — the AStudio desktop app itself is the same way (it directs users to the web to view details). So this page can reflect *balance changes* but cannot say *exactly how much this conversation consumed*.
170
175
 
171
176
  > **This number will not move while you chat through the plugin.** An unchanged balance does not mean nothing was spent — it means the call never entered AStudio's ledger at all. See [Billing and Points](#billing-and-points).
172
177
 
173
- If the badge shows "Unavailable" and clicking ↻ does nothing, first confirm the plugin artifacts match the running DSH version: DSH loads plugins at startup, so after rebuilding you must **fully quit and restart DSH** (refreshing the page is not enough).
178
+ ### WeChat QR sign-in
179
+
180
+ The top card of this section is the sign-in. **It needs neither an installed nor a running AStudio**: scan a WeChat code and the plugin exchanges it for the model key itself, storing the credential in `$DSH_HOME/.astudio-login.json` (mode `0600`, readable by this user only).
181
+
182
+ It performs the same three calls the desktop app does — plain HTTPS, no device fingerprint, no signature, no local service:
183
+
184
+ ```
185
+ GET sso.xfyun.cn/SSOService/wx/scanlogin/qrcode?fd=<ticket> → WeChat QR image URL
186
+ GET sso.xfyun.cn/SSOService/wx/scanlogin/uids?fd=<ticket> → code -1 until scanned; then infoList[{uid, ssoSessionId}]
187
+ GET agent.xfyun.cn/xingchen-studio/bot/models/configs → cookie from the scan; each row carries an api_key
188
+ ```
189
+
190
+ That `api_key` is the same kind of value AStudio stores as `experimental_bearer_token`, so once signed in everything on this path — chat, catalog, probing, balances, claim — keeps working with the credential's source switched to the plugin. The ticket is eight characters from AStudio's own reduced alphabet (no `I/l/0/1/o/O`), good for about a minute.
191
+
192
+ | Situation | Display |
193
+ |---|---|
194
+ | No sign-in at all | "No sign-in found" + **Sign in with WeChat** |
195
+ | Using the app's sign-in | "Using the AStudio app's sign-in"; scanning still replaces it |
196
+ | Waiting for the scan | The QR + "Waiting for the scan…", polled every 2 seconds; "The code expired" after a minute |
197
+ | WeChat bound to several accounts | The account names are listed; one press finishes |
198
+ | WeChat bound to no AStudio account | "That WeChat account has no AStudio account attached"; the code stays up for another WeChat |
199
+ | Scanned, but the account offers no model key | "Signed in, but the account offers no model credential"; nothing is stored |
200
+ | Signed in | "Signed in from DSH · Account …", and the button becomes **Sign out** |
201
+
202
+ Precedence is **our own sign-in over AStudio's files**. The reasoning is plain: whoever scanned here already said which account to use, and silently falling back to a stale file from an app that may no longer be installed would make the QR flow look broken. **Sign out** deletes only our file — the app's session is untouched — and the store falls back to it.
203
+
204
+ > The QR image is fetched by the host and inlined as a data URL, so the page loads nothing from a third-party origin.
205
+
206
+ ### Daily login reward
207
+
208
+ The same card ends with a **Claim** button, which books AStudio's daily login reward (100 on trial, 200 on basic, 400 on Pro/Enjoy) **without opening AStudio**.
209
+
210
+ It is not really a check-in. AStudio delivers the reward as a popup: `client-popups/pending` answers one `DAILY_REWARD_DIALOG` carrying `popupId`, `instanceKey` and the amount, and `client-popups/complete` books it. The desktop client fires that completion **itself** the moment the dialog arrives — the button in its UI is decoration — so "today's points landed" really means "AStudio was opened today". This button just makes the days it was not opened count too.
211
+
212
+ One press is two requests: read the pending list, then report completion. The balance the answer carries is then shown directly, so the press does not trigger a second read. Upstream is idempotent (a repeated completion answers `11003002`, read as "already claimed"), so extra presses cannot mint credit and a mistake cannot drain the account.
213
+
214
+ | Outcome | Display |
215
+ |---|---|
216
+ | Claimed | Balance updates in place, `+100` appears in green beside the button and fades after 6 seconds |
217
+ | Upstream answered `11003002` — booked before this press | "Already claimed today"; the balance does not move |
218
+ | The pending list holds no reward | "Nothing to claim right now"; the balance does not move |
219
+ | Sign-in expired (`code: 80000`, or 401/403) | Numbers cleared, "Sign-in expired" |
220
+ | Upstream error / no `clientType` for this platform | "Unavailable" |
221
+
222
+ The third row is deliberately not worded as "already claimed": the two are not the same state. `11003002` is upstream *saying* the popup was booked; an empty list only says the service is holding no reward at this moment — it may not have been issued yet, or the account may not have travelled with the request. Calling that "already claimed" tells someone to stop pressing a button that would still have worked, which is exactly what an earlier version did.
223
+
224
+ Two identity headers are mandatory (without either, the gateway answers `11001001`): `clientType` (`21` on Windows, `22` on macOS) and `studioVersion`. The latter is only shape-checked, which is why the plugin sends a constant instead of reading the installed app's version — and why this works with AStudio uninstalled.
225
+
226
+ One more detail `pending` will not forgive: `popupId` is a **positive integer**, and `complete` is parsed with `Number.isSafeInteger` upstream — a string makes the desktop client treat the whole popup config as invalid. The plugin used to read it as a string, which filtered out every reward the service sent while still reporting "already claimed", leaving the reward sitting there for the client to book. It now parses ids the way the client does and skips anything non-positive (the `-3` a preview uses, for instance).
227
+
228
+ These endpoints are also scoped to the **account**, not just the session: the desktop client sends `ssoSessionId`, `sso_sessionid`, `account_id` and `token` on every account call. The plugin now sends all four, where it used to send only the session.
229
+
230
+ > This is the plugin's **only** route that changes account state (`POST /astudio/claim`). It writes no local file, and it never touches other popup kinds in the pending list (announcements, feature tours): completing a popup it does not understand would mean deciding something on the user's behalf without knowing what.
231
+
232
+ If this page reports "Unavailable" and **Refresh** does nothing, first confirm the plugin artifacts match the running DSH version: DSH loads plugins at startup, so after rebuilding you must **fully quit and restart DSH** (refreshing the page is not enough). If the nav row is missing entirely, the installed plugin predates this section — it shipped in the same change that replaced the composer badge with it.
174
233
 
175
234
  ## Billing and Points
176
235
 
@@ -192,7 +251,7 @@ Three conditions must hold at once to be charged: the request belongs to an acod
192
251
  - **Upstream can move metering into the gateway at any time**, keying it on uid or token. When that happens this path closes immediately, with no code change on our side.
193
252
  - **Do not try to "report back" on the plugin's behalf**: the `session_id` / `thread_id` / `call_ids` / `capability_usages` in `ClientTurnUsageReport` can only be produced by the acode runtime, and the server keeps `turn_billing_usage_reviews` as a missing-usage audit. A fabricated report only lands in the audit table; it does not make the books right.
194
253
 
195
- The badge reads AStudio's own ledger, so of course it stays still while you chat through the plugin — **an unchanged balance does not mean nothing was spent, only that the spend missed that ledger**. Read the [Disclaimer](#disclaimer) before using this.
254
+ The points page reads AStudio's own ledger, so of course it stays still while you chat through the plugin — **an unchanged balance does not mean nothing was spent, only that the spend missed that ledger**. Read the [Disclaimer](#disclaimer) before using this.
196
255
 
197
256
  ## Settings Card
198
257
 
@@ -210,20 +269,30 @@ The card reads the status document once on mount and then every 60 seconds; all
210
269
  |---|---|---|---|
211
270
  | `/astudio/status` | `GET` | Status document | loopback Host + Origin |
212
271
  | `/astudio/points` | `GET` | Points balance (`?refresh=1` bypasses the cache) | loopback Host + Origin |
272
+ | `/astudio/claim` | `POST` | Book the daily login reward | loopback Host + Origin + in-process control key |
273
+ | `/astudio/login/qr` | `POST` | Mint a sign-in ticket and its QR code | loopback Host + Origin + in-process control key |
274
+ | `/astudio/login/qr/poll` | `POST` | Read the scan state; exchange and store the credential on a scan | loopback Host + Origin + in-process control key |
275
+ | `/astudio/login/logout` | `POST` | Forget the plugin's own sign-in (the app's session is untouched) | loopback Host + Origin + in-process control key |
213
276
  | `/astudio/update` | `GET` | Version and update info | loopback Host + Origin |
214
277
  | `/astudio/probe/{modelId}` | `POST` | Reasoning-effort probing | loopback Host + Origin + in-process control key |
215
278
  | `/astudio/visibility/{modelId}` | `POST` | Model visibility (one per changed model, sent by Save) | loopback Host + Origin + in-process control key |
216
279
 
217
- Both write routes require the same in-process control key (handed out with the status document, rotated per process, never persisted): the status route's loopback check stops a DNS-rebinding **page**, and that is not the same as authorising a **state-changing** action.
280
+ Five write routes (probing, visibility, claim, and the two sign-in routes) require the same in-process control key (handed out with the status document, rotated per process, never persisted): the status route's loopback check stops a DNS-rebinding **page**, and that is not the same as authorising a **state-changing** action. The claim route additionally checks Origin — a cross-site `fetch` reaches `127.0.0.1` just as well, and "another page claimed points for me" is exactly what that header rules out.
218
281
 
219
282
  ## DSH Version Support
220
283
 
221
- | Plugin version | DSH kernel it needs |
284
+ | Plugin version | DSH kernel range it supports |
222
285
  |---|---|
223
- | `0.1.1` | `0.2.1-rc.2` |
224
- | `0.1.0` | `0.2.0-rc.2` |
286
+ | `0.2.1` | `^0.2.1-rc.2` (from `0.2.1-rc.2`, below `0.3.0`) |
287
+ | `0.2.0` | `^0.2.1-rc.2` (from `0.2.1-rc.2`, below `0.3.0`) |
288
+ | `0.1.1` | `^0.2.1-rc.2` (from `0.2.1-rc.2`, below `0.3.0`) |
289
+ | `0.1.0` | `0.2.0-rc.2` (exact) |
290
+
291
+ The plugin declares a kernel range in `peerDependencies`; the host's `evaluatePluginCompatibility` checks it with semver and refuses to load the plugin when the range is unmet. `0.1.0` pinned an exact version, so every kernel rc needed a matching plugin release; from `0.1.1` the requirement is `^0.2.1-rc.2`, which covers every prerelease and the final release of the `0.2.1` line (`rc.3`, `rc.4`, `rc.5` all load as-is), and only a move to `0.3.x` requires a new plugin version. `0.1.0` shipped against `0.2.0-rc.2`, which was the real-machine environment; on the `0.1.1` line provider registration, the settings card and the points section are covered by local unit and integration tests, and the exact desktop build number still needs re-confirming on the new kernel.
292
+
293
+ `0.2.0` carries the same kernel range as `0.1.1` and adds nothing but features: the points section in Settings (balance and the daily claim) and WeChat QR sign-in. One thing to know before upgrading — **from `0.2.0` the plugin writes a credential file to disk**. That is unavoidable for QR sign-in: without the app, somebody has to hold the session. It lives at `$DSH_HOME/.astudio-login.json`, mode `0600`, created only when you scan a code and deleted when you sign out. If you want the old behaviour — read the app's credentials and write nothing at all — stay on `0.1.1`.
225
294
 
226
- The host refuses to load the plugin when the kernel generation does not match. `0.1.0` shipped against `0.2.0-rc.2`, which was the real-machine environment; from `0.1.1` on the plugin targets `0.2.1-rc.2`, where provider registration, the settings card and the points badge are covered by local unit and integration tests, and the exact desktop build number still needs re-confirming on the new kernel.
295
+ `0.2.1` is a fix-only release with an unchanged kernel range. **If you are on `0.2.0`, upgrade: its Claim button never actually claimed anything.** Pending popups carry `popupId` as a positive integer, but the plugin read it as a string, so every real reward was filtered out locally, the UI always said "already claimed today", and the reward sat untouched on the server — opening AStudio still claimed it. `0.2.1` parses the id the way the app does, separates "no pending reward" from an explicit `11003002` answer, and sends the four account-scoped cookies the claim endpoints expect.
227
296
 
228
297
  ## When the upstream refuses a request
229
298
 
@@ -248,19 +317,25 @@ A refused body is also written to `%TEMP%\dsh-astudio-connect\failed-requests\`
248
317
  - [ ] **A single local identity**: AStudio's token carries no stable uid, so catalog snapshots, visibility preferences and probe records all hang off `astudio-local`, with no multi-account isolation.
249
318
  - [ ] **Plugin calls are not billed to AStudio points**: points are settled by the desktop app per turn, and the plugin creates no turn, so nothing is deducted. For better or worse this is an unmetered path — see [Billing and Points](#billing-and-points). The plugin deliberately offers no way to "report back".
250
319
  - [ ] **The balance gate does not cover the plugin**: AStudio itself returns `402` at zero balance, but that check lives inside its own local gateway, which the plugin bypasses (inferred, not tested).
251
- - [ ] **No per-transaction billing detail**: the account endpoint only provides aggregate balances, so the badge can only reflect balance changes; plugin calls produce no billing records at all, let alone detail.
320
+ - [ ] **No per-transaction billing detail**: the account endpoint only provides aggregate balances, so Settings can only reflect balance changes; plugin calls produce no billing records at all, let alone detail.
252
321
  - [ ] **No long-term compatibility guarantee for upstream private endpoints**: the model catalog, account endpoint and reasoning-level information are the AStudio client's own interfaces rather than an official open API, and upstream changes may break the plugin. In that case it degrades to "last successful catalog", and says so plainly when there is no snapshot either, but long-term compatibility cannot be promised.
322
+ - [ ] **QR sign-in depends on three private upstream endpoints**: `scanlogin/qrcode` and `scanlogin/uids` on `sso.xfyun.cn`, plus the `api_key` inside `bot/models/configs`. If upstream adds a captcha, a signature or a device fingerprint, sign-in stops working (the panel then says "Sign-in is unavailable"), though a credential already stored keeps working.
323
+ - [ ] **A stored sign-in is not refreshed**: once its `ssoSessionId` expires you have to scan again; the plugin does not renew it (upstream exposes no renewal endpoint — the client simply signs in again).
324
+ - [ ] **One QR sign-in per DSH home**: switching accounts means signing out first; no multi-account coexistence.
325
+ - [ ] **The claim button depends on two hard-coded client identity headers**: `clientType` (platform) and `studioVersion` (the constant `3.5.5`). If upstream ever demands a build number, a signature or a device fingerprint, this path dies immediately and the button will only report "Unavailable".
253
326
  - [ ] **The CLI's `status` now issues one live fetch**: it waits for a real request so it can report the actual catalog source and model count; offline or signed out, it reports `empty` with the matching failure.
254
327
  - [ ] **No account-switch following**: unlike WorkBuddy's two variants, AStudio is a single product, so the plugin does not replicate variant-level capabilities.
255
328
 
256
329
  ## Security
257
330
 
258
- - Credentials are cached only in memory and never written to any file; points reads re-read the session file every time (so an AStudio re-login takes effect immediately) but only take `ssoSessionId`.
331
+ - Credentials read from AStudio's files are cached only in memory and never written to any file; points reads re-read the session file every time (so an AStudio re-login takes effect immediately) but only take `ssoSessionId`.
332
+ - **The only credential written to disk is the session the plugin's own QR sign-in produces**: `$DSH_HOME/.astudio-login.json`, mode `0600`, written only after a person presses **Sign in with WeChat** and completes a scan, and deleted by **Sign out**. Its `ssoSessionId` and model key never appear in any response, UI or log — the panel shows only the account id and nickname.
259
333
  - The shim binds only to `127.0.0.1` and accepts no external network access.
260
334
  - Inbound requests must pass four checks: Host, Origin, Content-Type and the Bearer shared secret; the Host check blocks DNS rebinding.
261
335
  - The shared secret is a random 32-byte value generated per process, compared in constant time with `timingSafeEqual`; the real AStudio token is resolved by the credential layer alone and never travels in a request header.
262
336
  - Tokens are never echoed in any UI or log; the CLI strips JWT-shaped strings from error text.
263
337
  - The points route and the status route apply the same loopback Host / Origin double check; responses contain only numbers and state, and account credentials stay inside the host process.
338
+ - There are exactly two account-side writes: the claim route reports that today's daily-reward popup is complete and touches no other popup kind, and the sign-in routes write the file above only after a scan. Neither answer carries a credential — only a state and account identity.
264
339
  - Every upstream response is treated as untrusted input: streaming reads carry size caps, and route responses are re-validated in the browser before rendering.
265
340
 
266
341
  ## Development
package/README.md CHANGED
@@ -16,7 +16,7 @@
16
16
  - [模型目录](#模型目录)
17
17
  - [推理档位与探测](#推理档位与探测)
18
18
  - [模型显隐](#模型显隐)
19
- - [积分余额徽标](#积分余额徽标)
19
+ - [积分与每日领取](#积分与每日领取)
20
20
  - [计费与积分](#计费与积分)
21
21
  - [设置卡片](#设置卡片)
22
22
  - [DSH 版本支持](#dsh-版本支持)
@@ -33,7 +33,7 @@
33
33
  ### 前置
34
34
 
35
35
  - 本机已安装 Node.js `^22.19.0 || >=24.0.0`
36
- - 本机已安装并登录 AStudio 桌面应用(插件直接复用 App 的登录状态,账号切换自动跟随)
36
+ - 一个讯飞账号:在 DSH 设置页微信扫码登录即可,**不需要安装 AStudio**;若本机已装并登录过 AStudio,插件也会直接复用那次的凭据
37
37
  - DeepSeek Harness 运行时,版本要求见 [DSH 版本支持](#dsh-版本支持)
38
38
 
39
39
  ### 安装插件
@@ -75,7 +75,7 @@ dsh plugin --profile web add dsh-astudio-connect@latest
75
75
  - **模型目录**:完全取自 AStudio 自己的 model-manager 接口,带真实的上下文窗口与最大输出;插件不内置任何清单,取不到就显示「无可用模型」。
76
76
  - **推理档位探测**:对未声明档位的模型,由你主动授权后发送带哨兵的最小请求,确认上游真的校验 `reasoning_effort`,再逐项确认接受哪些档位。
77
77
  - **模型显隐**:在设置卡片里勾选要在选择器中显示的模型;关掉的模型立刻从选择器消失,正用着它的会话需要另选模型才能继续发送。
78
- - **余额速览**:选中 AStudio 模型时,输入框模型选择器旁显示账户的通用 / 星火两份积分余额,右侧 ↻ 手动刷新,不自动轮询。
78
+ - **积分与领取**:设置页新增「AStudio 积分」分区,列出账户的通用 / 星火两份余额,带「刷新」和「领取每日登录积分」,不用打开 AStudio 客户端,也不用先选中 AStudio 模型。
79
79
  - **设置卡片**:侧栏「插件」面板下的 AStudio Connect 卡片,分「账户 / 模型 / 探测」三个分区,可手动刷新目录、逐个探测。
80
80
  - **命令行诊断**:`status` / `doctor` / `logout` 三个命令,全程不输出 token 材料。
81
81
  - **更新检查**:对照 npm 公共 dist-tags 判断是否有新版本,并用 GitHub releases 补充说明。
@@ -134,12 +134,16 @@ WorkBuddy 类上游的推理档位信息分散在上游接口与客户端私有
134
134
 
135
135
  AStudio 的 token 不携带稳定 uid,因此本插件只有一个本地身份 `astudio-local`——目录快照、显隐偏好和探测记录都挂在它名下,不做多账号隔离。
136
136
 
137
- ## 积分余额徽标
137
+ ## 积分与每日领取
138
138
 
139
- 选中 AStudio 模型时,输入框工具行(模型选择器旁)显示账户的两份积分余额,右侧带 ↻ 手动刷新:
139
+ 设置页左侧导航最下方有一行「AStudio 积分」,点开就是账户的两份余额与两个动作:
140
140
 
141
141
  ```
142
- 通用 1393 · 星火 24621
142
+ 通用积分 1393
143
+ ────────────────────────────────────────
144
+ 星火积分 24621
145
+ ────────────────────────────────────────
146
+ 每日登录奖励每天一份… [领取] [刷新]
143
147
  ```
144
148
 
145
149
  两份积分分开显示、**不相加**,因为用途不同:
@@ -147,30 +151,85 @@ AStudio 的 token 不携带稳定 uid,因此本插件只有一个本地身份
147
151
  - **通用积分**(`totalBalance`):所有模型,以及插件、Skill 调用等费用(这里是 AStudio 自身的插件与 Skill 调用,**不含本插件**)。
148
152
  - **星火专属积分**(`sparkTotalBalance`):仅星火模型的消耗。
149
153
 
150
- 数据来自 AStudio 桌面端自己使用的账户接口(`https://agent.xfyun.cn/xingchen-studio/points/balance`)。AStudio 的设置页会请求它自己的 loopback 服务,该服务带着桌面会话 Cookie 转发到账户 API;本插件直接用 `astron-session.json` 里已有的 `ssoSessionId` 访问同一个上游,因此**不新增位置发现,也不需要 AStudio 正在运行**。
154
+ 数据来自 AStudio 桌面端自己使用的账户接口(`https://agent.xfyun.cn/xingchen-studio/points/balance`)。AStudio 的设置页会请求它自己的 loopback 服务,该服务带着桌面会话 Cookie 转发到账户 API;本插件直接用同一个 `ssoSessionId` 访问同一个上游,因此**不需要 AStudio 正在运行**。这个 Cookie 有两个可能的来源:插件自己扫码登录得到的,或者 AStudio 写在 `astron-session.json` 里的——前者优先,见下面的[微信扫码登录](#微信扫码登录)。
151
155
 
152
156
  > `ssoSessionId` 与模型目录用的 `modelBearerToken`、聊天用的 `experimental_bearer_token` 都在同一个文件里,但是三份不同用途的凭据。
153
157
 
154
158
  行为约定:
155
159
 
156
- - **读取时机**:徽标出现时读一次,以及点 ↻ 时再读一次。**不做定时轮询**——每次读取都会打一次上游。
157
- - **可见性**:只在当前会话选中 AStudio 模型时出现,与 provider 无关的模型下不发起任何请求。
158
- - **失败绝不显示假数字**:读不到就降级,不会把「读不懂」渲染成 `0`。
159
- - **30 秒缓存**:成功的读取缓存 30 秒,↻ 会带 `?refresh=1` 绕过缓存;失败不缓存,下次读取重新问上游。
160
+ - **读取时机**:打开这个分区时读一次,点「刷新」再读一次。**余额不做定时轮询**——每次读取都会打一次上游。(唯一按秒问上游的是登录时那张二维码,见[微信扫码登录](#微信扫码登录)。)
161
+ - **与会话无关**:这一页不看当前选中的模型,没选 AStudio 模型、甚至开着别家模型的会话也能看余额。(这项能力原来挂在输入框的徽标上,只能在该会话选中 AStudio 模型时出现。)
162
+ - **打开即最新**:面板挂载时的读取带 `?refresh=1`,绕过宿主侧的 30 秒缓存。打开设置页是「我现在有多少」的明确提问,给它一个 30 秒前的数字没有意义。
163
+ - **失败绝不显示假数字**:读不到就显示 `—` 并说明原因,不会把「读不懂」渲染成 `0`。
164
+ - **失败不缓存**,下次读取重新问上游。
160
165
 
161
166
  | 情况 | 表现 |
162
167
  |---|---|
163
- | 网络失败 / 上游异常 | 显示「暂不可用」,↻ 可重试 |
164
- | 登录已失效(上游 `code: 80000`,或 401/403) | 显示「登录已失效」,需在 AStudio 重新登录 |
168
+ | 网络失败 / 上游异常 | 数字显示 `—`,提示「账户接口没有返回可读的余额」,「刷新」可重试 |
169
+ | 登录已失效(上游 `code: 80000`,或 401/403) | 提示「AStudio 登录已失效」,需在 AStudio 重新登录 |
165
170
  | 账户没有星火积分池 | 只显示通用积分,不显示 0 |
166
171
  | 余额确实为 0 | 正常显示 `0`(0 是真实余额,不是未知) |
167
- | 从未在 AStudio 登录 | 徽标整体不出现 |
172
+ | 从未在 AStudio 登录 | 提示「AStudio 未登录」,「领取」置灰(没有会话可领) |
168
173
 
169
- > **已知限制**:账户接口只提供聚合余额,没有逐笔明细——AStudio 桌面端本身也是如此(它引导用户去网页查看明细)。因此徽标能反映「余额变化」,但无法给出「这一轮对话精确扣了多少」。
174
+ > **已知限制**:账户接口只提供聚合余额,没有逐笔明细——AStudio 桌面端本身也是如此(它引导用户去网页查看明细)。因此这一页能反映「余额变化」,但无法给出「这一轮对话精确扣了多少」。
170
175
 
171
176
  > **用插件对话时,这个数字不会变化。** 余额不变不等于没花钱,而是这笔调用压根没走 AStudio 那本账——机制见[计费与积分](#计费与积分)。
172
177
 
173
- 若徽标显示「暂不可用」且点击 ↻ 无效,先确认插件产物是否与运行中的 DSH 版本匹配:DSH 在启动时加载插件,重新构建后必须**完全退出并重启 DSH**(仅刷新页面不够)。
178
+ ### 微信扫码登录
179
+
180
+ 这一栏最上面的卡片管登录。**不需要安装或打开 AStudio**:扫一个微信二维码,插件自己去换模型密钥,凭据存在 `$DSH_HOME/.astudio-login.json`(权限 `0600`,仅本用户可读)。
181
+
182
+ 它走的就是 AStudio 桌面端自己的那三步,全是普通 HTTPS,没有设备指纹、没有签名、不碰本地服务:
183
+
184
+ ```
185
+ GET sso.xfyun.cn/SSOService/wx/scanlogin/qrcode?fd=<票据> → 微信二维码图片地址
186
+ GET sso.xfyun.cn/SSOService/wx/scanlogin/uids?fd=<票据> → 未扫 code -1;已扫返回 infoList[{uid, ssoSessionId}]
187
+ GET agent.xfyun.cn/xingchen-studio/bot/models/configs → Cookie 用上面的 ssoSessionId,每行带 api_key
188
+ ```
189
+
190
+ 那个 `api_key` 与 AStudio 存成 `experimental_bearer_token` 的是同一种值,所以登录完成后,对话、目录、探测、余额、领取全都照旧工作——只是凭据来源换成了插件自己。票据 `fd` 是 8 位、从 AStudio 自己那套去掉了 `I/l/0/1/o/O` 的字母表里取,有效期约一分钟。
191
+
192
+ | 情况 | 表现 |
193
+ |---|---|
194
+ | 没有任何登录 | 「没有可用的登录」+「微信扫码登录」 |
195
+ | 正在用客户端的登录 | 「正在使用 AStudio 客户端的登录」,仍可扫码替换 |
196
+ | 扫码前 | 显示二维码 +「等待扫码…」,每 2 秒问一次,过期后提示「二维码已过期」 |
197
+ | 该微信绑了多个账号 | 列出账号名,点一个即完成 |
198
+ | 该微信未绑定 AStudio 账号 | 「该微信未绑定 AStudio 账号」,二维码留着,可换个微信扫 |
199
+ | 扫码成功但账号没有模型凭证 | 「已登录,但该账号没有可用的模型凭证」,这种情况不落盘 |
200
+ | 登录成功 | 「已通过 DSH 扫码登录 · 账号 …」,右侧变「退出登录」 |
201
+
202
+ 优先级是**插件自己的登录压过 AStudio 的文件**。理由很直接:在这里扫码的人已经指定了要用哪个账号;退一步悄悄回落到一个可能早已卸载的客户端的旧文件,会让扫码看起来像坏了。「退出登录」只删本插件那个文件,AStudio 自己的会话原样不动,随后自动回落到它。
203
+
204
+ > 二维码图片由宿主取回后以 data URL 内联给页面,浏览器不加载任何第三方来源的资源。
205
+
206
+ ### 每日登录积分
207
+
208
+ 余额下方还有一个「领取」按钮,用来领取 AStudio 的每日登录积分(金额随套餐:试用 100 / 基础 200 / Pro·Enjoy 400),**不需要打开 AStudio**。
209
+
210
+ 它其实不是一个签到动作。AStudio 把这份奖励做成一条弹窗下发:`client-popups/pending` 返回一条 `DAILY_REWARD_DIALOG`(带 `popupId`、`instanceKey` 和金额),上报 `client-popups/complete` 才算入账。桌面端收到弹窗时会**立刻自己上报完成**,界面上的按钮只是装饰——所以「今天的积分已到帐」实际上约等于「今天打开过一次 AStudio」。这个按钮只是让没打开的那天也能到帐。
211
+
212
+ 一次点击两个请求:读待领弹窗、上报完成;随后把领取回包里带的新余额直接显示出来,不再额外读一次。上游自带幂等(重复上报返回 `11003002`,按「今天已领」处理),所以多点几次不会多加分,写错也不会把账号刷爆。
213
+
214
+ | 结果 | 表现 |
215
+ |---|---|
216
+ | 领取成功 | 余额就地更新,按钮左侧显示 `+100`(绿色),6 秒后淡出 |
217
+ | 上游明确回 `11003002`(这次点击之前就已入账) | 显示「今日已领过」,余额不动 |
218
+ | 待领列表里没有这份奖励 | 显示「当前没有可领取的奖励」,余额不动 |
219
+ | 登录已失效(`code: 80000` 或 401/403) | 数字清空并显示「登录已失效」 |
220
+ | 上游异常 / 平台无对应 `clientType` | 显示「暂不可用」 |
221
+
222
+ 第三种情况不写成「今日已领过」,因为这两种状态并不等价:`11003002` 是上游**明确**说这条弹窗早就被核销了;而列表为空只说明服务端此刻没挂着奖励,也可能只是今天还没下发,或者账号信息没有传全。把后者说成「已领过」,人就再也不会去点第二次了——早期版本正是在这里骗了用户。
223
+
224
+ 两个身份头是硬性要求(缺任一个,网关直接回 `11001001`):`clientType`(Windows `21` / macOS `22`)与 `studioVersion`。后者只做形态校验,因此插件用常量而不是去读已安装的 AStudio 版本——这正是它能做到「不需要装 AStudio」的原因。
225
+
226
+ `pending` 的返回里还有一个必须按原样解析的细节:`popupId` 是**正整数**,`complete` 上游也用 `Number.isSafeInteger` 校验,客户端拿到字符串会直接判定整条弹窗配置非法。插件早期按字符串收,结果服务端下发的每一条奖励都被自己过滤掉了,界面照样报「今日已领过」,而奖励其实还老老实实躺在服务端——客户端一点就能领。现在按客户端的方式解析,非正整数的 id(例如预览用的 `-3`)直接跳过。
227
+
228
+ 这几个接口同时是按**账户**鉴权的:桌面端每个账号请求都带 `ssoSessionId`、`sso_sessionid`、`account_id`、`token` 四个 cookie。插件现在也照原样带上(早期只带 `ssoSessionId`)。
229
+
230
+ > 这是本插件**唯一**会改动账户状态的接口(`POST /astudio/claim`)。它不改本地任何文件,也不碰 `pending` 里其它类型的弹窗(公告、功能引导等):不理解的弹窗一律代用户完成,等于替用户做了一个不知道是什么的决定。
231
+
232
+ 若这一页显示「暂不可用」而「刷新」也无效,先确认插件产物是否与运行中的 DSH 版本匹配:DSH 在启动时加载插件,重新构建后必须**完全退出并重启 DSH**(仅刷新页面不够)。整行导航都不见了,则说明当前装的插件版本早于这个分区——它和输入框徽标是同一批改动,徽标被替换成了这一页。
174
233
 
175
234
  ## 计费与积分
176
235
 
@@ -192,7 +251,7 @@ AStudio 的 token 不携带稳定 uid,因此本插件只有一个本地身份
192
251
  - **上游随时可以把计量下沉到网关**,改成按 uid / token 记账。那一天到来时这条通道会立即关闭,插件不需要改任何一行代码;
193
252
  - **不要试图让插件「补上报」**:`ClientTurnUsageReport` 里的 `session_id` / `thread_id` / `call_ids` / `capability_usages` 只有 acode 运行时才产得出来,服务端另有 `turn_billing_usage_reviews` 做缺失审计。拼凑一份报告只会进审计表,不会让账变正确。
194
253
 
195
- 余额徽标读的就是 AStudio 自己的账本,用插件聊天时它当然不动——**余额不变不代表没花钱,只代表没走那本账**。使用前请先读[免责声明](#免责声明)。
254
+ 积分页面读的就是 AStudio 自己的账本,用插件聊天时它当然不动——**余额不变不代表没花钱,只代表没走那本账**。使用前请先读[免责声明](#免责声明)。
196
255
 
197
256
  ## 设置卡片
198
257
 
@@ -210,20 +269,30 @@ AStudio 的 token 不携带稳定 uid,因此本插件只有一个本地身份
210
269
  |---|---|---|---|
211
270
  | `/astudio/status` | `GET` | 状态文档 | loopback Host + Origin |
212
271
  | `/astudio/points` | `GET` | 积分余额(`?refresh=1` 绕过缓存) | loopback Host + Origin |
272
+ | `/astudio/claim` | `POST` | 领取每日登录积分 | loopback Host + Origin + 进程内控制密钥 |
273
+ | `/astudio/login/qr` | `POST` | 生成扫码登录票据与二维码 | loopback Host + Origin + 进程内控制密钥 |
274
+ | `/astudio/login/qr/poll` | `POST` | 查询扫码状态,扫到即换取并保存凭据 | loopback Host + Origin + 进程内控制密钥 |
275
+ | `/astudio/login/logout` | `POST` | 清除插件自己的登录(不动客户端会话) | loopback Host + Origin + 进程内控制密钥 |
213
276
  | `/astudio/update` | `GET` | 版本与更新信息 | loopback Host + Origin |
214
277
  | `/astudio/probe/{modelId}` | `POST` | 推理档位探测 | loopback Host + Origin + 进程内控制密钥 |
215
278
  | `/astudio/visibility/{modelId}` | `POST` | 模型显隐(每改一个模型一条,由「保存」触发) | loopback Host + Origin + 进程内控制密钥 |
216
279
 
217
- 两条写路由(探测、显隐)要求同一个进程内控制密钥(随状态文档下发、每进程轮换、不落盘):状态路由的 loopback 校验挡住的是 DNS 重绑定的**页面**,那并不等于授权了一个**会改状态**的操作。
280
+ 五条写路由(探测、显隐、领取、扫码登录的两条)要求同一个进程内控制密钥(随状态文档下发、每进程轮换、不落盘):状态路由的 loopback 校验挡住的是 DNS 重绑定的**页面**,那并不等于授权了一个**会改状态**的操作。领取这条还多一层 Origin 校验——跨站 `fetch` 一样能打到 `127.0.0.1`,而「别的网页替我领了积分」正是要挡住的那件事。
218
281
 
219
282
  ## DSH 版本支持
220
283
 
221
- | 插件版本 | 对应的 DSH 内核版本 |
284
+ | 插件版本 | 支持的 DSH 内核版本 |
222
285
  |---|---|
223
- | `0.1.1` | `0.2.1-rc.2` |
224
- | `0.1.0` | `0.2.0-rc.2` |
286
+ | `0.2.1` | `^0.2.1-rc.2`(`0.2.1-rc.2` 起,`0.3.0` 以下) |
287
+ | `0.2.0` | `^0.2.1-rc.2`(`0.2.1-rc.2` 起,`0.3.0` 以下) |
288
+ | `0.1.1` | `^0.2.1-rc.2`(`0.2.1-rc.2` 起,`0.3.0` 以下) |
289
+ | `0.1.0` | `0.2.0-rc.2`(精确版本) |
290
+
291
+ 插件在 `peerDependencies` 里声明内核范围,宿主的 `evaluatePluginCompatibility` 逐项用 semver 校验,范围不满足时直接拒载。`0.1.0` 把版本钉死成 `0.2.0-rc.2`,因此内核每推一个 rc 都得跟着发版;`0.1.1` 起改为 `^0.2.1-rc.2`,覆盖 `0.2.1` 这一代的全部预发布与正式版(`rc.3`、`rc.4`、`rc.5` 均可直接加载),只有跨到 `0.3.x` 才需要发布新版本。`0.1.0` 是对着 `0.2.0-rc.2` 发布的,那一代是实机验证环境;`0.1.1` 一代下 provider 注册、设置卡片、积分分区由本地单元测试与集成测试覆盖,桌面端具体构建号仍需在新内核上重新确认。
292
+
293
+ `0.2.0` 的内核范围与 `0.1.1` 完全一致,发的只有功能:设置页的积分分区(看余额、领每日积分)与微信扫码登录。升级前需要知道一件事——**从 `0.2.0` 起插件会在本地落盘一个凭据文件**。这是扫码登录的必然结果:不装客户端就只能自己存会话。文件是 `$DSH_HOME/.astudio-login.json`,权限 `0600`,只在你主动扫码或点「退出登录」时增删;如果你只想要「读完客户端凭据就走、一个字节也不写」的旧行为,留在 `0.1.1`。
225
294
 
226
- 内核代次不匹配时宿主会直接拒载。`0.1.0` 是对着 `0.2.0-rc.2` 发布的,那一代是实机验证环境;`0.1.1` 起对齐 `0.2.1-rc.2`,这一代次下 provider 注册、设置卡片、积分徽标由本地单元测试与集成测试覆盖,桌面端具体构建号仍需在新内核上重新确认。
295
+ `0.2.1` 是纯修复版,内核范围不变。**装了 `0.2.0` 的请升级:那一版的「领取」按钮实际领不到积分。** 待领弹窗的 `popupId` 是正整数,插件却按字符串接收,于是每条真实奖励都在本地被过滤掉,界面一律报「今日已领过」,而奖励还挂在服务端——打开 AStudio 客户端照样能领。`0.2.1` 按客户端的方式解析 id,把「待领列表为空」与「上游明确回 `11003002`」拆成两种结果,领取请求也补齐了按账户鉴权的四个 cookie。
227
296
 
228
297
  ## 请求被上游拒绝时
229
298
 
@@ -248,19 +317,25 @@ maas-api 对不合规的请求只回一句 `10005 invalid request`,不说明
248
317
  - [ ] **只有一个本地身份**:AStudio token 不携带稳定 uid,目录快照、显隐偏好和探测记录都挂在 `astudio-local` 名下,不做多账号隔离。
249
318
  - [ ] **插件调用不计入 AStudio 积分**:积分由桌面端按 turn 自报结算,插件不产生 turn,因此不扣费;这实际上是一条不被计量的通道,见[计费与积分](#计费与积分)。这里不提供任何补齐上报的能力。
250
319
  - [ ] **余额闸门不覆盖插件**:AStudio 自己在余额为 0 时返回 `402`,该判断在它的本地网关内;插件绕过该网关,所以余额耗尽也不会被它拦住(推断,未实测)。
251
- - [ ] **不做逐笔扣费明细**:账户接口只给聚合余额,徽标只能反映余额变化;插件调用则完全不产生扣费记录,更谈不上明细。
320
+ - [ ] **不做逐笔扣费明细**:账户接口只给聚合余额,设置页只能反映余额变化;插件调用则完全不产生扣费记录,更谈不上明细。
252
321
  - [ ] **不承诺长期兼容上游私有接口**:模型目录、账户接口与推理档位信息都是 AStudio 客户端自己的接口而非官方开放 API,上游改动可能使插件失效;届时插件按「上次成功目录」降级,连快照都没有时如实显示无可用模型,但不能保证长期兼容。
322
+ - [ ] **扫码登录依赖上游三个私有接口**:`sso.xfyun.cn` 的 `scanlogin/qrcode` 与 `scanlogin/uids`,以及 `bot/models/configs` 里的 `api_key`。上游若加验证码、签名或设备指纹,扫码登录会直接失效(届时只显示「登录暂不可用」),但已保存的凭据仍然可用。
323
+ - [ ] **扫码登录的凭据不会被主动刷新**:`ssoSessionId` 过期后需要重新扫码;插件不会拿它去换新(上游也没有公开的续期接口,客户端是重新登录)。
324
+ - [ ] **一个 DSH 只保存一个扫码登录**:后期换账号需要先「退出登录」再扫,不做多账号并存。
325
+ - [ ] **领取按钮依赖两个硬编码的客户端身份头**:`clientType`(平台)与 `studioVersion`(常量 `3.5.5`)。上游一旦要求构建号、签名或设备指纹,这条路会立即失效,届时按钮只会显示「暂不可用」。
253
326
  - [ ] **CLI 的 `status` 会发起一次实时刷新**:它现在等待一次真实请求,才能如实报出目录来源与模型数;离线或未登录时报告 `empty` 与对应的失败原因。
254
327
  - [ ] **不做账号切换跟随**:与 WorkBuddy 双 variant 不同,AStudio 只有一个产品,插件不复制 variant 级能力。
255
328
 
256
329
  ## 安全说明
257
330
 
258
- - 凭证仅在内存中缓存,不写入任何文件;积分读取每次重新读会话文件(以便 AStudio 重新登录后立即生效),但只取 `ssoSessionId`。
331
+ - 从 AStudio 文件读到的凭证仅在内存中缓存,不写入任何文件;积分读取每次重新读会话文件(以便 AStudio 重新登录后立即生效),但只取 `ssoSessionId`。
332
+ - **唯一写入磁盘的凭据是插件自己扫码登录得到的会话**:`$DSH_HOME/.astudio-login.json`,权限 `0600`,只在用户按下「微信扫码登录」并完成扫码时写入,按下「退出登录」即删除。文件里的 `ssoSessionId` 与模型密钥不会出现在任何响应、界面或日志里;界面只显示账号 id 与昵称。
259
333
  - shim 只绑定 `127.0.0.1`,不接受外部网络访问。
260
334
  - 入站请求需通过 Host、Origin、Content-Type、Bearer 共享密钥四重校验;Host 校验挡住 DNS 重绑定。
261
335
  - 共享密钥是每进程随机生成的 32 字节值,用 `timingSafeEqual` 做常量时间比较;真正的 AStudio token 只由凭证层解析,从不经过请求头。
262
336
  - token 不会在任何界面或日志中回显;CLI 的异常信息会剥离形如 JWT 的串。
263
337
  - 积分路由与状态路由同样做 loopback Host / Origin 双重校验,回包只含数字和状态,账户凭据始终留在宿主进程内。
338
+ - 账户侧写操作只有两条:领取路由只上报「今天的登录积分弹窗已完成」,不碰其它类型弹窗;登录路由只在扫码成功后写入上面那个文件。两条的回包里都只有状态与账号信息,没有凭据。
264
339
  - 所有上游响应都视为不可信输入:流式读取带大小上限,路由回包在浏览器端重新校验后再渲染。
265
340
 
266
341
  ## 开发
package/lib/bin.d.ts CHANGED
@@ -2,12 +2,15 @@
2
2
  /**
3
3
  * AStudio Connect CLI: secret-free status, doctor, and logout.
4
4
  *
5
- * The CLI is boot-free: it never reads the token into a shell prompt or writes
6
- * the AStudio config.toml. `logout` clears only the plugin's in-memory cache.
5
+ * The CLI is boot-free: it never reads the token into a shell prompt and never
6
+ * writes the AStudio config.toml. `logout` deletes the plugin's own sign-in
7
+ * file, which is the only credential this plugin ever writes; the AStudio app's
8
+ * session is the app's to clear.
7
9
  *
8
- * Hints: `doctor` reports config path/existence, sign-in state, whether the
9
- * model-manager token is present, and repair hints (exit 0 only when signed in
10
- * and the config file exists); `status` also reports catalog source and model
10
+ * Hints: `doctor` reports config path/existence, which sign-in is in use,
11
+ * whether the model-manager token is present, and repair hints (exit 0 when
12
+ * signed in at all — a QR sign-in needs no config.toml, so a missing one is a
13
+ * hint rather than a failure); `status` also reports catalog source and model
11
14
  * count. Neither prints the token — only `tokenLength` and presence — and
12
15
  * JWT-shaped strings in error output are replaced with `[redacted token]`.
13
16
  * `status` always claims the "built-in" catalog source, because the live
@@ -20,6 +23,13 @@
20
23
  interface RunOptions {
21
24
  /** Override the AStudio config.toml path (used by tests). */
22
25
  configPath?: string;
26
+ /**
27
+ * Override the plugin's own sign-in file (used by tests).
28
+ *
29
+ * Production always uses the DSH home; tests must not, or a real sign-in on
30
+ * the machine would answer for them.
31
+ */
32
+ loginSessionPath?: string;
23
33
  /**
24
34
  * Override the catalog snapshot path (used by tests).
25
35
  *
package/lib/bin.js CHANGED
@@ -1,17 +1,20 @@
1
1
  #!/usr/bin/env node
2
- import { i as createAStudioAuth, t as createAStudioCatalog } from "./catalog-DKQ7Jvn1.js";
2
+ import { i as createAStudioAuth, t as createAStudioCatalog } from "./catalog-CILzWwRh.js";
3
3
  import { existsSync, realpathSync } from "node:fs";
4
4
  import { fileURLToPath } from "node:url";
5
5
  //#region src/bin.ts
6
6
  /**
7
7
  * AStudio Connect CLI: secret-free status, doctor, and logout.
8
8
  *
9
- * The CLI is boot-free: it never reads the token into a shell prompt or writes
10
- * the AStudio config.toml. `logout` clears only the plugin's in-memory cache.
9
+ * The CLI is boot-free: it never reads the token into a shell prompt and never
10
+ * writes the AStudio config.toml. `logout` deletes the plugin's own sign-in
11
+ * file, which is the only credential this plugin ever writes; the AStudio app's
12
+ * session is the app's to clear.
11
13
  *
12
- * Hints: `doctor` reports config path/existence, sign-in state, whether the
13
- * model-manager token is present, and repair hints (exit 0 only when signed in
14
- * and the config file exists); `status` also reports catalog source and model
14
+ * Hints: `doctor` reports config path/existence, which sign-in is in use,
15
+ * whether the model-manager token is present, and repair hints (exit 0 when
16
+ * signed in at all — a QR sign-in needs no config.toml, so a missing one is a
17
+ * hint rather than a failure); `status` also reports catalog source and model
15
18
  * count. Neither prints the token — only `tokenLength` and presence — and
16
19
  * JWT-shaped strings in error output are replaced with `[redacted token]`.
17
20
  * `status` always claims the "built-in" catalog source, because the live
@@ -46,7 +49,8 @@ function safeMessage(error) {
46
49
  function makeStore(options = {}) {
47
50
  return createAStudioAuth({
48
51
  ...options.configPath !== void 0 ? { configPath: options.configPath } : {},
49
- ...options.dataHome !== void 0 ? { dataHome: options.dataHome } : {}
52
+ ...options.dataHome !== void 0 ? { dataHome: options.dataHome } : {},
53
+ ...options.loginSessionPath !== void 0 ? { loginSessionPath: options.loginSessionPath } : {}
50
54
  });
51
55
  }
52
56
  function makeCatalog(store, options = {}) {
@@ -67,14 +71,16 @@ async function doctor(jsonOutput, options) {
67
71
  package: "dsh-astudio-connect",
68
72
  provider: ASTRO_PROVIDER,
69
73
  signIn: status.state,
74
+ /** `dsh` is a sign-in performed in the plugin, `client` one read from the app. */
75
+ signInSource: status.source,
70
76
  configFile: {
71
77
  path: status.configPath,
72
78
  present: configPresent
73
79
  },
74
80
  modelCatalog: { hasModelToken: status.hasModelToken === true },
75
81
  hints: [
76
- ...status.state === "signed-out" ? ["Sign in once in the AStudio desktop app, then run status again."] : [],
77
- ...configPresent ? [] : [`No AStudio config.toml at ${status.configPath}.`],
82
+ ...status.state === "signed-out" ? ["No sign-in found. Sign in with WeChat from the AStudio points section in DSH Settings, or sign in once in the AStudio desktop app."] : [],
83
+ ...configPresent || status.source === "dsh" ? [] : [`No AStudio config.toml at ${status.configPath}.`],
78
84
  ...status.hints
79
85
  ]
80
86
  };
@@ -82,12 +88,12 @@ async function doctor(jsonOutput, options) {
82
88
  else process.stdout.write([
83
89
  `AStudio Connect on ${process.version}`,
84
90
  `Config file: ${configPresent ? "present" : "missing"} — ${status.configPath}`,
85
- `Sign-in state: ${report.signIn}`,
91
+ `Sign-in state: ${report.signIn} (source: ${report.signInSource})`,
86
92
  `Model catalog token: ${report.modelCatalog.hasModelToken ? "present" : "missing (no models will be listed)"}`,
87
93
  ...report.hints.map((hint) => `Hint: ${hint}`),
88
94
  ""
89
95
  ].join("\n"));
90
- return status.state === "signed-in" && configPresent ? 0 : 1;
96
+ return status.state === "signed-in" ? 0 : 1;
91
97
  }
92
98
  async function status(jsonOutput, options) {
93
99
  const store = makeStore(options);
@@ -100,6 +106,7 @@ async function status(jsonOutput, options) {
100
106
  package: "dsh-astudio-connect",
101
107
  provider: ASTRO_PROVIDER,
102
108
  state: authStatus.state,
109
+ signInSource: authStatus.source,
103
110
  source: catalog.source(),
104
111
  modelCount: catalog.current().length,
105
112
  ...failure !== void 0 ? { failure } : {}