dsh-astudio-connect 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.en.md CHANGED
@@ -12,16 +12,13 @@ An adaptation of the design in [dsh-workbuddy-connect](https://github.com/corrin
12
12
 
13
13
  - [Installation](#installation)
14
14
  - [Features](#features)
15
- - [Command Line](#command-line)
16
- - [How It Works](#how-it-works)
17
15
  - [Configuration](#configuration)
18
16
  - [Model Catalog](#model-catalog)
19
17
  - [Reasoning Effort and Probing](#reasoning-effort-and-probing)
20
18
  - [Model Visibility](#model-visibility)
21
- - [Points Balance Badge](#points-balance-badge)
19
+ - [Points and the daily claim](#points-and-the-daily-claim)
20
+ - [Billing and Points](#billing-and-points)
22
21
  - [Settings Card](#settings-card)
23
- - [Update Check and Host Heartbeat](#update-check-and-host-heartbeat)
24
- - [Location Discovery (Windows)](#location-discovery-windows)
25
22
  - [DSH Version Support](#dsh-version-support)
26
23
  - [Known Limitations](#known-limitations)
27
24
  - [Security](#security)
@@ -36,34 +33,36 @@ An adaptation of the design in [dsh-workbuddy-connect](https://github.com/corrin
36
33
  ### Prerequisites
37
34
 
38
35
  - Node.js `^22.19.0 || >=24.0.0` installed locally
39
- - 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.
40
37
  - A DeepSeek Harness runtime — see [DSH Version Support](#dsh-version-support)
41
38
 
42
39
  ### Install the plugin
43
40
 
44
- The plugin registers its manifest through `cordis.patch.yml`, and DSH loads it at startup.
41
+ **Command line**:
45
42
 
46
43
  ```bash
47
- # Install from the local directory
48
- npm install -D ./dsh-astudio-connect
44
+ dsh plugin --profile web add dsh-astudio-connect@latest
49
45
  ```
50
46
 
51
- Or add this directory path in the DSH plugin manager.
47
+ Replace `web` with the profile you actually boot.
48
+
49
+ > The plugin version must match your DSH kernel generation, or the host refuses to load it. Installing the latest is enough; if it is still refused, report your DSH kernel version and the full error on [Issues](https://github.com/Cwentor/dsh-astudio-connect/issues) rather than bypassing the host check.
52
50
 
53
51
  ### Verify
54
52
 
55
- After installing you must **fully quit and restart DSH** (DSH loads plugins at startup; refreshing the page is not enough), then confirm:
53
+ After installing you must **fully quit and restart DSH**, then confirm:
56
54
 
57
55
  1. An **AStudio** group appears in the model picker;
58
56
  2. Pick a model from that group and complete a short conversation that answers normally.
59
57
 
60
- Both must hold for the install to count as successful. If the group does not appear, start with `doctor` in the [Command Line](#command-line) section to check sign-in state and the config path.
58
+ Both must hold for the install to count as successful. If the group does not appear, run `npx dsh-astudio-connect doctor` inside the profile directory (`$DSH_HOME/profiles/<name>`) to check sign-in state.
61
59
 
62
60
  > You can also hand the install to an agent you already have locally (DSH Agent, Codex, Claude Code, Cursor, …) by sending it this paragraph verbatim:
63
61
  >
64
- > Please install and verify the DSH (DeepSeek Harness) plugin dsh-astudio-connect (local directory: `<repo path>`):
62
+ > Please install and verify the DSH (DeepSeek Harness) plugin dsh-astudio-connect (npm package: `dsh-astudio-connect`):
65
63
  > Read the project README first, and confirm which interface I actually use (Web / Desktop / TUI) and the DSH kernel version;
66
- > Install from the local directory using the `cordis.patch.yml` registration, without modifying dependency packages or bypassing host validation;
64
+ > On Desktop use **Add plugin** in the Plugins panel and enter `dsh-astudio-connect`; for a command-line profile use `dsh plugin --profile <name> add dsh-astudio-connect@latest`, without modifying dependency packages or bypassing host validation;
65
+ > If the host refuses it because the kernel generation does not match, give me my DSH kernel version and the full error;
67
66
  > Ask me for consent and back up any local file or configuration change;
68
67
  > Once installed, verify that an AStudio group appears in the model picker, and run a short conversation with me.
69
68
 
@@ -73,84 +72,14 @@ Both must hold for the install to count as successful. If the group does not app
73
72
  - **Credential reuse**: reads AStudio's `experimental_bearer_token`; no separate OAuth flow.
74
73
  - **Standard protocol**: maas-api speaks standard OpenAI-compatible protocol, which pi-ai supports natively — no protocol translation.
75
74
  - **Full capability bridge**: non-streaming, streaming SSE, tool calls, `reasoning_content` from reasoning models, and context compaction all reuse the DSH host's own implementations.
76
- - **Model catalog**: taken from AStudio's own model-manager endpoint (the same document the AStudio UI renders), carrying real context windows and max output, with a three-tier fallback of live → saved → built-in.
77
- - **Reasoning-effort probing**: for models that declare no levels, sends a minimal sentinel-bearing request under your explicit consent, confirming the upstream actually validates `reasoning_effort` before accepting which levels it accepts.
78
- - **Model visibility**: pick which models appear in the picker from the settings card; preferences are stored per signed-in account, and hiding only affects selectability, not sessions already using that model.
79
- - **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.
80
- - **Settings card**: the AStudio Connect card under the sidebar Plugins panel, split into Account / Context / Probe sections, with manual catalog refresh and probing.
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
+ - **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
+ - **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
+ - **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
+ - **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.
81
80
  - **Command-line diagnostics**: `status`, `doctor` and `logout` — never emit credential material.
82
81
  - **Update check**: compares against npm's public dist-tags to decide whether a newer release exists, and enriches with GitHub release notes.
83
82
 
84
- ## Command Line
85
-
86
- The plugin ships a `dsh-astudio-connect` executable (the `bin` in `package.json`). It **does not start the host and does not load pi-ai**, so it can run standalone and be used to troubleshoot from anywhere:
87
-
88
- ```
89
- Usage: dsh-astudio-connect <doctor|status|logout> [--provider astudio] [--json]
90
-
91
- doctor secret-free sign-in and environment diagnostics
92
- status sign-in state, catalog source, and model count
93
- logout clear the plugin in-memory credential cache
94
-
95
- --provider which provider to inspect; defaults to astudio
96
- --json emit one secret-free JSON document (doctor/status only)
97
- ```
98
-
99
- Run it from a local checkout (when not globally installed):
100
-
101
- ```bash
102
- node lib/bin.js status
103
- node lib/bin.js doctor --json
104
- node lib/bin.js logout
105
- ```
106
-
107
- | Command | Output | Exit code |
108
- |---|---|---|
109
- | `doctor` | Config file path and presence, sign-in state, whether the model-manager token exists, remediation hints | `0` when `signed-in` **and** the config file exists, otherwise `1` |
110
- | `status` | Sign-in state, catalog source, model count | `0` when `signed-in`, otherwise `1` |
111
- | `logout` | Clears the plugin's own in-memory credential cache | Always `0` |
112
-
113
- Behaviour guarantees:
114
-
115
- - **Writes nothing**: none of the three commands writes AStudio's `config.toml`; `logout` clears only the plugin's own in-memory cache, leaves the desktop app's sign-in alone, and does not promise to make the model group disappear (it still applies as long as the app's credential file is present).
116
- - **Never echoes credentials**: output carries only `tokenLength` and "present / absent", never the token itself; JWT-shaped strings in error text are replaced with `[redacted token]`.
117
- - **`--provider` accepts only `astudio`** — anything else errors out.
118
-
119
- > **Known behaviour**: the catalog source reported by `status` is always `built-in`. The live refresh happens inside the host process, and a standalone CLI process does not issue that request, so it reflects the built-in roster rather than the catalog the host is actually using. For the host's real source, see the Account section of the settings card.
120
-
121
- ## How It Works
122
-
123
- The plugin registers maas-api's models into DSH's `llm` seam through a three-layer bridge:
124
-
125
- 1. **Credential layer** (`auth.ts`): reads `experimental_bearer_token` from AStudio's `config.toml`, caches it in memory, never writes it to disk.
126
- 2. **Local shim** (`shim.ts`): starts an OpenAI-compatible endpoint on `127.0.0.1`, validates four inbound guards, then forwards to maas-api.
127
- 3. **pi-ai adapter** (`adapter.ts`): registers the shim's endpoint as DSH's `astudio` provider using `openAICompletionsApi()` + `PiAiAdapter`.
128
-
129
- ```
130
- DSH (llm seam) → AStudioPiAiAdapter → 127.0.0.1 shim → maas-api (Bearer token)
131
- ```
132
-
133
- The model catalog and the points balance travel over two separate read-only paths that do not go through the shim:
134
-
135
- ```
136
- model-manager API ← modelBearerToken (model roster and context parameters)
137
- account points API ← ssoSessionId (general / Spark points balance)
138
- ```
139
-
140
- All three credentials come from AStudio's own files, but they serve different purposes and are not interchangeable.
141
-
142
- ### Shim routes
143
-
144
- | Method | Path | Purpose |
145
- |---|---|---|
146
- | `GET` | `/healthz` | Liveness probe |
147
- | `GET` | `/v1/models` | Model roster, **hidden entries filtered out** |
148
- | `POST` | `/v1/chat/completions` | Forwarded to maas-api, request body and streaming response passed through |
149
-
150
- Request bodies are capped at 64 MB; a broken stream gets a terminating `data: [DONE]\n\n`; a single stream idles out after 300 seconds.
151
-
152
- The port the shim binds avoids the ports the fetch standard refuses to connect to (`FETCH_BLOCKED_PORTS` in `loopback.ts`): the OS may hand out an ephemeral port that fetch will not dial, in which case the shim listens successfully yet every request fails with `TypeError: fetch failed`. So it rebinds, up to 16 attempts.
153
-
154
83
  ## Configuration
155
84
 
156
85
  Plugin configuration is managed through DSH's plugin page (on 0.2.x: sidebar **Plugins** panel → AStudio Connect):
@@ -164,29 +93,20 @@ Plugin configuration is managed through DSH's plugin page (on 0.2.x: sidebar **P
164
93
 
165
94
  ## Model Catalog
166
95
 
167
- The model roster and context parameters come from AStudio's **model-manager** endpoint (`GET /api/v1/model-manager/models`, production base `https://astronstudio-api-volces-prod.xf-yun.com/api/v1/model-manager`), which is the same document the AStudio UI renders. Authentication uses `modelBearerToken` from `astron-session.json` — a separate credential from the chat `experimental_bearer_token`.
168
-
169
- > `/v1/models` is deliberately not used: that OpenAI-compatible discovery endpoint answers with a smaller, staler subset and would drop models that are in fact callable.
170
-
171
- Catalog parsing rules:
96
+ The model roster and context parameters come from AStudio's **model-manager** endpoint.
172
97
 
173
- - Only models with `visibility` of `list` are kept; those with `supported_in_api` of `false` are dropped;
174
- - Context window and max output come from the upstream's real values (e.g. `spark-x2.6-preview` is 262144 / 100000);
175
- - The upstream's `none` in `supported_reasoning_levels` is translated into a "thinking can be switched off" capability flag rather than treated as an effort level;
176
- - When merging, the built-in roster's order and names win; models unique to the upstream are appended at the end.
98
+ **Catalog parsing rules:**
177
99
 
178
- **Fallback order**:
100
+ - Only models with `visibility` of `list` are kept; those with `supported_in_api` of `false` are dropped, as are rows missing a context window or output budget;
101
+ - Context window and max output come from the upstream's real values (e.g. `spark-x2.6-preview` is 262144 / 100000).
179
102
 
180
- 1. Live catalog
181
- 2. Last successful snapshot
182
- 3. Built-in roster
103
+ **The plugin ships no model roster at all.** The list is whatever upstream actually returned:
183
104
 
184
- Snapshots are written to `$DSH_HOME/.astudio-connect/catalog.json`, stored per local identity. The catalog source (`live` / `saved` / `fallback`) and the fetch time are both shown on the settings card.
105
+ 1. Live catalog — the models this fetch returned;
106
+ 2. Last successful snapshot — that same upstream answer, when this fetch could not be made (network, auth, malformed response);
107
+ 3. Neither — the list is empty.
185
108
 
186
- The current built-in roster covers:
187
-
188
- - **Reasoning models**: `spark-x2.5`, `spark-x2.6-preview` (thinking switchable, `low` / `high` / `max`)
189
- - **Chat models**: `spark-x2.5-4b`, `xopglm52`, `xopdeepseekv4pro`, `xopdeepseekv4pro0813`, `xopdsv4flash0731in`, `astronclaw-auto`
109
+ Case 3 is not filled in with a built-in roster, a placeholder or a default model: the settings card says "No models available" and names the reason (signed out / endpoint error / unreadable response / unreachable / upstream listed none), and the model picker shows the host's own "No models available". An upstream answer that is **explicitly empty** is treated the same way and written to the snapshot — otherwise a later failed refresh would resurrect models upstream has withdrawn. A response whose `models` field is missing, of the wrong type, or whose rows are all unusable is treated as a malformed response rather than as "upstream withdrew every model", and leaves the existing snapshot untouched.
190
110
 
191
111
  ## Reasoning Effort and Probing
192
112
 
@@ -201,9 +121,9 @@ So this plugin uses a **user-consented, dynamically discovered** approach:
201
121
  3. Upstream accepts → recorded as `validating`, storing only the accepted levels; upstream rejects with `invalid_parameter_value` → recorded as `rejected` / `non-validating`;
202
122
  4. Results are persisted per `account:modelId` and expire after 24 hours.
203
123
 
204
- Probing consumes a small amount of points. A result only means the current upstream accepts that level — **it does not promise that reasoning quality, speed, or point consumption will change**.
124
+ Probing sends one real upstream request, consuming a small number of upstream tokens (like every other call the plugin makes, that counts against upstream usage, not AStudio points — see [Billing and Points](#billing-and-points)). A result only means the current upstream accepts that level — **it does not promise that reasoning quality or speed will change**.
205
125
 
206
- The mapping from `reasoning_effort` to pi-ai levels only exposes levels the upstream declared; `off` is exposed only when the model declares `none` (i.e. thinking can be switched off).
126
+ The mapping from `reasoning_effort` to pi-ai levels only exposes levels the upstream declared; `off` is exposed only when the model declares `none` (i.e. thinking can be switched off), and **what actually goes out on the wire is `none`, not `off`** — upstream's vocabulary is only `none` / `high` / `max`, and a literal `off` gets the whole request refused with 10005.
207
127
 
208
128
  ## Model Visibility
209
129
 
@@ -214,132 +134,198 @@ In the Context section of the settings card you can check which models appear in
214
134
 
215
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.
216
136
 
217
- ## Points Balance Badge
137
+ ## Points and the daily claim
218
138
 
219
- 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:
220
140
 
221
141
  ```
222
- General 1393 · Spark 24621
142
+ General points 1393
143
+ ────────────────────────────────────────
144
+ Spark points 24621
145
+ ────────────────────────────────────────
146
+ One daily login reward per day… [Claim] [Refresh]
223
147
  ```
224
148
 
225
149
  The two balances are shown separately and are **never added together**, because they pay for different things:
226
150
 
227
- - **General points** (`totalBalance`): every model, plus plugins and Skill calls.
151
+ - **General points** (`totalBalance`): every model, plus plugins and Skill calls (AStudio's own plugins and Skills — **not this plugin**).
228
152
  - **Spark points** (`sparkTotalBalance`): Spark-branded models only.
229
153
 
230
- 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).
231
155
 
232
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.
233
157
 
234
158
  Behaviour guarantees:
235
159
 
236
- - **Read timing**: once when the badge appears, and again when you click ↻. **No timed polling** — every read hits the upstream.
237
- - **Visibility**: appears only when an AStudio model is selected in the current session; no request is issued under a provider-independent model.
238
- - **Never shows a fabricated number on failure**: if the balance cannot be read it degrades instead of rendering "unreadable" as `0`.
239
- - **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.
240
165
 
241
166
  | Situation | Display |
242
167
  |---|---|
243
- | Network failure / upstream error | "Unavailable", ↻ retries |
244
- | 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 |
245
170
  | Account has no Spark pool | Only general points are shown; no `0` |
246
171
  | The balance really is 0 | Shows `0` normally (0 is a real balance, not "unknown") |
247
- | 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) |
248
173
 
249
- > **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*.
250
175
 
251
- 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).
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).
252
177
 
253
- ## Settings Card
178
+ ### WeChat QR sign-in
254
179
 
255
- Sidebar **Plugins** panel → AStudio Connect. One card split into three sections, switching between Chinese and English with the plugin language:
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).
256
181
 
257
- - **Account**: sign-in state, config file path, catalog source (live / saved / built-in), model count, and a one-click catalog refresh.
258
- - **Context**: each model's context window and max output, plus the model visibility toggles.
259
- - **Probe**: probe state and accepted levels for models with no declared levels, with manual probe / re-probe.
182
+ It performs the same three calls the desktop app does — plain HTTPS, no device fingerprint, no signature, no local service:
260
183
 
261
- The card reads the status document once on mount and then every 60 seconds; all data comes from the same-origin `GET /astudio/status` and carries no credential material.
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
+ ```
262
189
 
263
- ### Same-origin routes
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.
264
191
 
265
- | Path | Method | Purpose | Guards |
266
- |---|---|---|---|
267
- | `/astudio/status` | `GET` | Status document | loopback Host + Origin |
268
- | `/astudio/points` | `GET` | Points balance (`?refresh=1` bypasses the cache) | loopback Host + Origin |
269
- | `/astudio/update` | `GET` | Version and update info | loopback Host + Origin |
270
- | `/astudio/probe/{modelId}` | `POST` | Reasoning-effort probing | loopback Host + Origin + in-process probe key |
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** |
271
201
 
272
- The probe route separately requires an in-process 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.
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.
273
203
 
274
- ## Update Check and Host Heartbeat
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.
275
205
 
276
- **Update check** (`update.ts`): reads npm's public dist-tags first to decide whether an update exists — looking only at the `latest` tag, since pre-release channels are not this plugin's release line; only once an update is confirmed does it query GitHub releases for the version notes. Both requests have a timeout (8 seconds) and a response body size cap, and the browser re-validates the version numbers, release URL and range with `parseAStudioUpdateResult` before rendering; any mismatch suppresses rendering.
206
+ ### Daily login reward
277
207
 
278
- **Host heartbeat** (`host-heartbeat.ts`): after the plugin registers the provider, it writes a JSON document containing `pid` and `registeredAt` to `$DSH_HOME/.astudio-host-heartbeat.json`; the host deletes it on `ctx.dispose()`.
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**.
279
209
 
280
- The heartbeat distinguishes "the host is still running" from "the host crashed and left the file behind". Checking the PID alone is not enough — after a crash the OS may hand the same PID to an unrelated process, and that uncleared heartbeat would falsely report "host alive". So the condition is that the PID exists **and** the process holding it started no later than `registeredAt`; when the start time cannot be read, it degrades to a plain PID check.
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.
281
211
 
282
- The browser half writes no files and reports failure only through `console.error`. That asymmetry is intentional: the host is the load-bearing half, and a missing heartbeat unambiguously means the host never started.
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.
283
213
 
284
- ## Location Discovery (Windows)
214
+ | Outcome | Display |
215
+ |---|---|
216
+ | Claimed | Balance updates in place, `+100` appears in green beside the button and fades after 6 seconds |
217
+ | Already claimed today / nothing pending yet | "Already claimed today"; the balance does not move |
218
+ | Sign-in expired (`code: 80000`, or 401/403) | Numbers cleared, "Sign-in expired" |
219
+ | Upstream error / no `clientType` for this platform | "Unavailable" |
285
220
 
286
- The plugin does not assume AStudio lives at a fixed path. It locates the data directory in this order:
221
+ 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.
287
222
 
288
- 1. A path explicitly set in the `configPath` setting (used verbatim, not probed)
289
- 2. The `ASTUDIO_DATA_HOME` environment variable (used verbatim, not probed)
290
- 3. AStudio's own registry record: `ProductionDataRoot` / `ProductionAcodeHome` under `HKCU\Software\AStudioStorage`
291
- 4. The `<install directory name> Data` directory beside the install directory
223
+ > 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.
292
224
 
293
- Item 3 is the record AStudio itself writes, so moving drives or reinstalling needs no configuration at all. A record that exists but is incomplete or of an unsupported schema version raises an explicit error rather than silently degrading to "not connected" — an older install this plugin cannot read must not win over a newer one.
225
+ 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.
294
226
 
295
- Registry value-name prefixes are split by release channel: `production` → `Production*`, `canary` → `Canary*`, `development` → `Development*`; item 4's directory name follows as `AStudio Data` / `AStudio Canary Data` / `AStudio Dev Data`.
227
+ ## Billing and Points
296
228
 
297
- Automatic discovery is not implemented on non-Windows platforms; set `ASTUDIO_DATA_HOME` explicitly there.
229
+ > **The short version**: calls made by this plugin **do not consume AStudio points**. AStudio's points are not charged by the model gateway. The desktop app settles them per *turn*, from usage it reports itself. The plugin is a separate process and is never on that path.
298
230
 
299
- ## DSH Version Support
231
+ The thing that actually charges you is the desktop app, not `maas-api`:
232
+
233
+ 1. The acode runtime opens a turn (carrying `turn_id` / `thread_id` / `session_id`).
234
+ 2. Each model request goes through the desktop's local model gateway to `model_providers.astron-spark.base_url`; the gateway records that request's input / output / cached tokens as a runtime event.
235
+ 3. When the turn ends, the desktop app aggregates those events into a `ClientTurnUsageReport` (schema 1.3) and writes it to `turn_billing_outbox` in `state.sqlite`.
236
+ 4. `DirectPointsGateway.settle()` POSTs that report to `{studioApiBaseUrl}points/deduce-balance` with the desktop session cookie; the server rates it into `ratedPoints` and deducts `GENERAL_POINTS` / `SPARK_POINTS`.
237
+
238
+ Three conditions must hold at once to be charged: the request belongs to an acode turn, that turn finishes as `completed`, and the settlement request is submitted successfully. Miss any one and nothing is charged — **merely leaving through that gateway is not enough**.
239
+
240
+ ### This is not the same as "free"
241
+
242
+ - **Upstream tokens are still consumed** — they just never reach AStudio's ledger.
243
+ - **The balance gate does not cover the plugin**: when its own balance hits 0, AStudio rejects requests with `402 astron_points_exhausted` — but that check lives in its own local gateway, which the plugin does not traverse. In theory the plugin still works at zero balance (inferred from the current implementation; not tested).
244
+ - **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.
245
+ - **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.
246
+
247
+ 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.
248
+
249
+ ## Settings Card
300
250
 
301
- The plugin declares host dependencies in two places, both pinned to seam package version **`0.2.0-rc.2`**.
251
+ Sidebar **Plugins** panel → AStudio Connect. One card split into three sections, switching between Chinese and English with the plugin language:
252
+
253
+ - **Account**: sign-in state, catalog source (live / saved / no models), model count, and a one-click catalog refresh.
254
+ - **Models**: a visibility switch per model, committed by **Save** — a model switched off disappears from DSH's model picker right away. A session that was using that model has to select another one before it can send again. Until then the card says "n unsaved" in its header and "once saved, n of m exposed to DSH" in the body; **Discard** drops the draft.
255
+ - **Probe**: probe state and accepted levels per model, with per-model probe / re-probe. A press turns the button into "Probing…" and answers with a pressed-in displacement plus a haptic tick where the platform has one.
302
256
 
303
- **Host-side `peerDependencies`** (run in the host process):
257
+ The card reads the status document once on mount and then every 60 seconds; all data comes from the same-origin `GET /astudio/status` and carries no credential material.
258
+
259
+ ### Same-origin routes
304
260
 
305
- | Package | Purpose |
261
+ | Path | Method | Purpose | Guards |
262
+ |---|---|---|---|
263
+ | `/astudio/status` | `GET` | Status document | loopback Host + Origin |
264
+ | `/astudio/points` | `GET` | Points balance (`?refresh=1` bypasses the cache) | loopback Host + Origin |
265
+ | `/astudio/claim` | `POST` | Book the daily login reward | loopback Host + Origin + in-process control key |
266
+ | `/astudio/login/qr` | `POST` | Mint a sign-in ticket and its QR code | loopback Host + Origin + in-process control key |
267
+ | `/astudio/login/qr/poll` | `POST` | Read the scan state; exchange and store the credential on a scan | loopback Host + Origin + in-process control key |
268
+ | `/astudio/login/logout` | `POST` | Forget the plugin's own sign-in (the app's session is untouched) | loopback Host + Origin + in-process control key |
269
+ | `/astudio/update` | `GET` | Version and update info | loopback Host + Origin |
270
+ | `/astudio/probe/{modelId}` | `POST` | Reasoning-effort probing | loopback Host + Origin + in-process control key |
271
+ | `/astudio/visibility/{modelId}` | `POST` | Model visibility (one per changed model, sent by Save) | loopback Host + Origin + in-process control key |
272
+
273
+ 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.
274
+
275
+ ## DSH Version Support
276
+
277
+ | Plugin version | DSH kernel range it supports |
306
278
  |---|---|
307
- | `dsh-llm` / `dsh-llm-pi-ai` | Register the `astudio` provider |
308
- | `dsh-host-webserver` | Routes for the settings card and points badge |
309
- | `dsh-settings` | Plugin configuration |
310
- | `dsh-home-paths` | Where snapshots, visibility preferences and the heartbeat are written |
311
- | `dsh-attachment` | Attachment passing |
312
- | `@deepseek-ai/cordis` | `^4.0.2` (`4.x`) |
313
- | `@deepseek-ai/schemastery` | `^3.18.2` |
314
- | `react` | `^18.2.0` |
279
+ | `0.2.0` | `^0.2.1-rc.2` (from `0.2.1-rc.2`, below `0.3.0`) |
280
+ | `0.1.1` | `^0.2.1-rc.2` (from `0.2.1-rc.2`, below `0.3.0`) |
281
+ | `0.1.0` | `0.2.0-rc.2` (exact) |
282
+
283
+ 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.
315
284
 
316
- **Browser-side `dsh.client.inject`** (host packages the client bundle depends on):
285
+ `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`.
317
286
 
318
- `dsh-client-ui-renderer`, `dsh-client-ui-conversation`, `dsh-client-ui-model-selection`, `dsh-client-locale`, `dsh-api-remotes`, `dsh-api-session-controller`.
287
+ ## When the upstream refuses a request
319
288
 
320
- | Status | Version | Notes |
321
- |---|---|---|
322
- | **Supported** | DSH Desktop **2.0.17** (seam `0.2.0-rc.2`) | Current development and verification environment: provider registration, settings card and points badge all confirmed on a real machine |
289
+ maas-api answers a non-conforming request with a bare `10005 invalid request` and never names the field. The plugin heals the request in the forwarding layer, in this order, without DSH or pi-ai having to change:
290
+
291
+ 1. **Wire repair** — `tool_calls[].function.arguments` that is not a string, a `tool` message with no `tool_call_id`, `content` set to `null`. Every OpenAI-compatible server rejects these, so they are fixed in place and logged at warn level.
292
+ 2. **Effort guard** — a `reasoning_effort` the model does not declare (or any effort for a model that does not reason) is dropped. The gateway validates the level per model and answers 400 for the whole request otherwise.
293
+ 3. **Shape fallback** — drop platform-only fields (`store`, `stream_options`, `prompt_cache_key`, …) and rewrite `developer` as `system`; if that still earns a 400, flatten text-only content arrays to strings. The first accepted shape is reused for the rest of the process, so later requests cost no extra round trip.
294
+ 4. **Self-diagnosis (once per process)** — when every shape is refused, the plugin asks the gateway instead of guessing: it changes one dimension at a time (`max_tokens`, the whole `tools` array, `reasoning_effort`, the message history), then bisects whichever one is at fault. The conclusion is included in the error handed back to DSH (`diagnosis: …`), and what can be fixed is fixed and resent, staying fixed for the process:
295
+ - `max_tokens` above the limit → clamped to 8192;
296
+ - a refused tool definition → dropped by name (logged), the rest still apply;
297
+ - an unsupported effort → removed.
298
+
299
+ `store` is never sent (`supportsStore: false`), so steps 1–3 normally do nothing and cost no requests.
300
+
301
+ A refused body is also written to `%TEMP%\dsh-astudio-connect\failed-requests\` (five files at most, 16 MB each) because only the real body shows what the gateway objects to. Those files contain conversation text and stay on the machine; set `ASTUDIO_DUMP_FAILED=0` to disable this, or `ASTUDIO_DUMP_FAILED_DIR` to relocate it.
323
302
 
324
303
  ## Known Limitations
325
304
 
326
305
  - [ ] **Image input is not enabled yet**: the adapter already maps a model to `input: ['text', 'image']` based on the catalog declaration, but model-manager parsing currently hardcodes `supportsImages: false` (and the upstream document has no corresponding field), so every model accepts plain text only.
327
- - [ ] **The update check requires the package to be published on npm**: the version judgement reads npm's public dist-tags, so with the package unpublished that step 404s and the check is always "unavailable". The repository URL also once named a nonexistent org, making the GitHub release enrichment 404 as well; both are corrected in `src/version.ts` and `package.json`'s `repository`. The enrichment path only takes effect once the package is published.
328
- - [ ] **Automatic location discovery is implemented only on Windows**: macOS / Linux need `ASTUDIO_DATA_HOME` set explicitly.
306
+ - [ ] **Windows only for now**: macOS / Linux are neither adapted nor verified, so they are unsupported.
329
307
  - [ ] **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.
330
- - [ ] **No per-transaction billing detail**: the account endpoint only provides aggregate balances, so the badge can only reflect balance changes.
331
- - [ ] **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 through "last successful catalog → built-in roster" and labels the source on the card, but long-term compatibility cannot be promised.
332
- - [ ] **The CLI's `status` does not reflect the live catalog**: see the note in [Command Line](#command-line).
308
+ - [ ] **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".
309
+ - [ ] **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).
310
+ - [ ] **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.
311
+ - [ ] **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.
312
+ - [ ] **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.
313
+ - [ ] **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).
314
+ - [ ] **One QR sign-in per DSH home**: switching accounts means signing out first; no multi-account coexistence.
315
+ - [ ] **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".
316
+ - [ ] **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.
333
317
  - [ ] **No account-switch following**: unlike WorkBuddy's two variants, AStudio is a single product, so the plugin does not replicate variant-level capabilities.
334
318
 
335
319
  ## Security
336
320
 
337
- - 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`.
321
+ - 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`.
322
+ - **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.
338
323
  - The shim binds only to `127.0.0.1` and accepts no external network access.
339
324
  - Inbound requests must pass four checks: Host, Origin, Content-Type and the Bearer shared secret; the Host check blocks DNS rebinding.
340
325
  - 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.
341
326
  - Tokens are never echoed in any UI or log; the CLI strips JWT-shaped strings from error text.
342
327
  - 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.
328
+ - 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.
343
329
  - Every upstream response is treated as untrusted input: streaming reads carry size caps, and route responses are re-validated in the browser before rendering.
344
330
 
345
331
  ## Development
@@ -347,7 +333,7 @@ The plugin declares host dependencies in two places, both pinned to seam package
347
333
  ```bash
348
334
  npm install
349
335
  npm run check # typecheck + test + build
350
- npm test # run 263 tests (28 test files)
336
+ npm test # run 302 tests (29 test files)
351
337
  npm run build # produce lib/ (the directory DSH actually loads)
352
338
  ```
353
339
 
@@ -361,6 +347,8 @@ This project is **for personal learning and research only**. It drives only the
361
347
 
362
348
  Users must comply with AStudio's terms of service; any consequence arising from use of this project (including but not limited to account restrictions, cleared credits, or service interruption) is borne by the user alone.
363
349
 
350
+ **This plugin implements neither AStudio's usage reporting nor its points settlement**: calls made through it never enter AStudio's points ledger (mechanism in [Billing and Points](#billing-and-points)). That gap follows from the other side's client-reported billing model — it is not a feature of this project, and it must not be used to evade billing. If you want to pay for these calls, use the official AStudio client.
351
+
364
352
  The authors accept no liability for any direct or indirect damages arising from the use or misuse of this project.
365
353
 
366
354
  This project is not affiliated with, authorised by, or endorsed by iFLYTEK, AStudio, or DeepSeek. Names appearing in this document are used only to describe compatibility; their trademark rights belong to their respective owners.
@@ -369,7 +357,7 @@ This project is not affiliated with, authorised by, or endorsed by iFLYTEK, AStu
369
357
 
370
358
  - **[corrinehu/dsh-workbuddy-connect](https://github.com/corrinehu/dsh-workbuddy-connect)** (MIT) — the sibling DSH bridge plugin, aimed at the WorkBuddy desktop app. This project follows its overall structure, with AStudio's maas-api as the upstream instead.
371
359
 
372
- Refer to that project's own documentation for supported versions and installation requirements. It is maintained independently by a third party; listing it is not an endorsement, and is not a stability or compatibility promise about this plugin's internal interfaces.
360
+ > Refer to that project's own documentation for supported versions and installation requirements. It is maintained independently by a third party; listing it is not an endorsement, and is not a stability or compatibility promise about this plugin's internal interfaces.
373
361
 
374
362
  ## Acknowledgements
375
363