dsh-astudio-connect 0.2.0 → 0.2.2

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
@@ -214,11 +214,20 @@ One press is two requests: read the pending list, then report completion. The ba
214
214
  | Outcome | Display |
215
215
  |---|---|
216
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 |
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 |
218
219
  | Sign-in expired (`code: 80000`, or 401/403) | Numbers cleared, "Sign-in expired" |
219
220
  | Upstream error / no `clientType` for this platform | "Unavailable" |
220
221
 
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.
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
+ "Not issued yet" is the common case, not the corner one: **the daily popup is only attached to the account once the desktop app starts.** Measured on `2026-11-05` with AStudio not running, every header combination polled `client-popups/pending` and got an empty list with the balance unchanged (7793); replaying the read-only endpoints the app hits on startup (`membership/me`, `points/summary`, `bot/models/configs`) changed nothing either. Starting the app the same day added 100 straight away. So the plugin can claim, but only on days you have already opened AStudio — otherwise there is no popup and the button truthfully reports "Nothing to claim right now". The headers are not the cause: adding `clientVersion` made no difference in an A/B run.
225
+
226
+ `clientType` (`21` on Windows, `22` on macOS) and `studioVersion` are mandatory (without either, the gateway answers `11001001`). The desktop app also adds `clientVersion` to every request bound for an xfyun domain — both come from `electron.app.getVersion()`, currently `3.6.0` — and the plugin sends all three so its requests match the app's. The version is only shape-checked, which is why a constant works instead of reading the installed app's version, and why this works with AStudio uninstalled.
227
+
228
+ 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).
229
+
230
+ 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.
222
231
 
223
232
  > 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.
224
233
 
@@ -276,6 +285,8 @@ Five write routes (probing, visibility, claim, and the two sign-in routes) requi
276
285
 
277
286
  | Plugin version | DSH kernel range it supports |
278
287
  |---|---|
288
+ | `0.2.2` | `^0.2.1-rc.2` (from `0.2.1-rc.2`, below `0.3.0`) |
289
+ | `0.2.1` | `^0.2.1-rc.2` (from `0.2.1-rc.2`, below `0.3.0`) |
279
290
  | `0.2.0` | `^0.2.1-rc.2` (from `0.2.1-rc.2`, below `0.3.0`) |
280
291
  | `0.1.1` | `^0.2.1-rc.2` (from `0.2.1-rc.2`, below `0.3.0`) |
281
292
  | `0.1.0` | `0.2.0-rc.2` (exact) |
@@ -284,6 +295,10 @@ The plugin declares a kernel range in `peerDependencies`; the host's `evaluatePl
284
295
 
285
296
  `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`.
286
297
 
298
+ `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.
299
+
300
+ `0.2.2` only aligns the request headers, and **does not fix the "claimable only while the client is running" symptom**. Two changes: a new `clientVersion` header (constant `3.6.0`), and `studioVersion` moving from `3.5.5` to `3.6.0` — the desktop app's `fetchWithClientVersion` adds the former to every xfyun request and the plugin never sent it at all. An A/B run before and after this release showed the header is not the cause (see [Known limitations](#known-limitations)), so what this version actually delivers is making a plugin request indistinguishable from the app's, plus writing the real constraint into the docs: the daily reward is only issued after the desktop app has started. Any release note claiming this version fixed claiming is wrong.
301
+
287
302
  ## When the upstream refuses a request
288
303
 
289
304
  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:
@@ -312,7 +327,8 @@ A refused body is also written to `%TEMP%\dsh-astudio-connect\failed-requests\`
312
327
  - [ ] **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
328
  - [ ] **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
329
  - [ ] **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".
330
+ - [ ] **The claim button depends on three hard-coded client identity headers**: `clientType` (platform), `studioVersion` and `clientVersion` (both constants, `3.6.0`, tracking the current client release). If upstream ever demands a build number, a signature or a device fingerprint, this path dies immediately and the button will only report "Unavailable".
331
+ - [ ] **The daily reward is only claimable after the desktop app has run**: without the app, upstream never attaches the day's popup, so the plugin polls an empty list and the button truthfully reports "Nothing to claim right now". Measured on `2026-11-05` with the app closed: three header combinations plus a replay of the app's startup endpoints all left it empty; starting the app the same day added 100 immediately. This is upstream's issuance timing, not a defect the plugin can fix — claiming from DSH requires opening AStudio once that day.
316
332
  - [ ] **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.
317
333
  - [ ] **No account-switch following**: unlike WorkBuddy's two variants, AStudio is a single product, so the plugin does not replicate variant-level capabilities.
318
334
 
package/README.md CHANGED
@@ -214,11 +214,20 @@ GET agent.xfyun.cn/xingchen-studio/bot/models/configs → Cookie 用上
214
214
  | 结果 | 表现 |
215
215
  |---|---|
216
216
  | 领取成功 | 余额就地更新,按钮左侧显示 `+100`(绿色),6 秒后淡出 |
217
- | 今天已领过 / 服务端还没下发 | 显示「今日已领过」,余额不动 |
217
+ | 上游明确回 `11003002`(这次点击之前就已入账) | 显示「今日已领过」,余额不动 |
218
+ | 待领列表里没有这份奖励 | 显示「当前没有可领取的奖励」,余额不动 |
218
219
  | 登录已失效(`code: 80000` 或 401/403) | 数字清空并显示「登录已失效」 |
219
220
  | 上游异常 / 平台无对应 `clientType` | 显示「暂不可用」 |
220
221
 
221
- 两个身份头是硬性要求(缺任一个,网关直接回 `11001001`):`clientType`(Windows `21` / macOS `22`)与 `studioVersion`。后者只做形态校验,因此插件用常量而不是去读已安装的 AStudio 版本——这正是它能做到「不需要装 AStudio」的原因。
222
+ 第三种情况不写成「今日已领过」,因为这两种状态并不等价:`11003002` 是上游**明确**说这条弹窗早就被核销了;而列表为空只说明服务端此刻没挂着奖励,也可能只是今天还没下发,或者账号信息没有传全。把后者说成「已领过」,人就再也不会去点第二次了——早期版本正是在这里骗了用户。
223
+
224
+ 而「今天还没下发」这句话比想象中更常成立:**每日弹窗是桌面端启动之后才挂到账号上的**。实测(`2026-11-05`,客户端完全没开)三个请求头组合轮询 `client-popups/pending` 全部返回空列表、余额纹丝不动(7793);把桌面端启动的那些只读接口(`membership/me`、`points/summary`、`bot/models/configs`)重放一遍,结论一样。同日启动客户端,余额随即 +100。所以插件能领,但前提是这一天你已经启动过 AStudio——不启动就不会有弹窗,按钮只能如实报「当前没有可领取的奖励」。请求头不是原因:补上 `clientVersion` 之后 A/B 结果没有任何变化。
225
+
226
+ `clientType`(Windows `21` / macOS `22`)与 `studioVersion` 是硬性要求(缺任一个,网关直接回 `11001001`)。桌面端还会给所有讯飞域名的请求额外加一个 `clientVersion`(两者都取自 `electron.app.getVersion()`,当前是 `3.6.0`);插件三个头都发,保持与客户端请求一致。版本号只做形态校验,所以用常量而不是去读已安装的 AStudio 版本——这正是它能做到「不需要装 AStudio」的原因。
227
+
228
+ `pending` 的返回里还有一个必须按原样解析的细节:`popupId` 是**正整数**,`complete` 上游也用 `Number.isSafeInteger` 校验,客户端拿到字符串会直接判定整条弹窗配置非法。插件早期按字符串收,结果服务端下发的每一条奖励都被自己过滤掉了,界面照样报「今日已领过」,而奖励其实还老老实实躺在服务端——客户端一点就能领。现在按客户端的方式解析,非正整数的 id(例如预览用的 `-3`)直接跳过。
229
+
230
+ 这几个接口同时是按**账户**鉴权的:桌面端每个账号请求都带 `ssoSessionId`、`sso_sessionid`、`account_id`、`token` 四个 cookie。插件现在也照原样带上(早期只带 `ssoSessionId`)。
222
231
 
223
232
  > 这是本插件**唯一**会改动账户状态的接口(`POST /astudio/claim`)。它不改本地任何文件,也不碰 `pending` 里其它类型的弹窗(公告、功能引导等):不理解的弹窗一律代用户完成,等于替用户做了一个不知道是什么的决定。
224
233
 
@@ -276,6 +285,8 @@ GET agent.xfyun.cn/xingchen-studio/bot/models/configs → Cookie 用上
276
285
 
277
286
  | 插件版本 | 支持的 DSH 内核版本 |
278
287
  |---|---|
288
+ | `0.2.2` | `^0.2.1-rc.2`(`0.2.1-rc.2` 起,`0.3.0` 以下) |
289
+ | `0.2.1` | `^0.2.1-rc.2`(`0.2.1-rc.2` 起,`0.3.0` 以下) |
279
290
  | `0.2.0` | `^0.2.1-rc.2`(`0.2.1-rc.2` 起,`0.3.0` 以下) |
280
291
  | `0.1.1` | `^0.2.1-rc.2`(`0.2.1-rc.2` 起,`0.3.0` 以下) |
281
292
  | `0.1.0` | `0.2.0-rc.2`(精确版本) |
@@ -284,6 +295,10 @@ GET agent.xfyun.cn/xingchen-studio/bot/models/configs → Cookie 用上
284
295
 
285
296
  `0.2.0` 的内核范围与 `0.1.1` 完全一致,发的只有功能:设置页的积分分区(看余额、领每日积分)与微信扫码登录。升级前需要知道一件事——**从 `0.2.0` 起插件会在本地落盘一个凭据文件**。这是扫码登录的必然结果:不装客户端就只能自己存会话。文件是 `$DSH_HOME/.astudio-login.json`,权限 `0600`,只在你主动扫码或点「退出登录」时增删;如果你只想要「读完客户端凭据就走、一个字节也不写」的旧行为,留在 `0.1.1`。
286
297
 
298
+ `0.2.1` 是纯修复版,内核范围不变。**装了 `0.2.0` 的请升级:那一版的「领取」按钮实际领不到积分。** 待领弹窗的 `popupId` 是正整数,插件却按字符串接收,于是每条真实奖励都在本地被过滤掉,界面一律报「今日已领过」,而奖励还挂在服务端——打开 AStudio 客户端照样能领。`0.2.1` 按客户端的方式解析 id,把「待领列表为空」与「上游明确回 `11003002`」拆成两种结果,领取请求也补齐了按账户鉴权的四个 cookie。
299
+
300
+ `0.2.2` 只做请求头对齐,**不解决「只有客户端开着才能领」那个现象**。改动有两处:新增 `clientVersion`(常量 `3.6.0`),`studioVersion` 从 `3.5.5` 提到 `3.6.0`——桌面端的 `fetchWithClientVersion` 会给所有讯飞域名请求加这个头,插件此前完全没有发过。发版前后的 A/B 实测表明这个头不是原因(见[已知限制](#已知限制)),所以这版的实际价值是把「插件发出的请求与客户端无法区分」这件事做实,以及把「每日奖励只在桌面端启动后才下发」这个限制如实写进文档。谁要是在版本说明里看到这版修好了领取,那话说错了。
301
+
287
302
  ## 请求被上游拒绝时
288
303
 
289
304
  maas-api 对不合规的请求只回一句 `10005 invalid request`,不说明是哪个字段。插件按下面的顺序自愈,全部发生在转发层,DSH 与 pi-ai 不需要改动:
@@ -312,7 +327,8 @@ maas-api 对不合规的请求只回一句 `10005 invalid request`,不说明
312
327
  - [ ] **扫码登录依赖上游三个私有接口**:`sso.xfyun.cn` 的 `scanlogin/qrcode` 与 `scanlogin/uids`,以及 `bot/models/configs` 里的 `api_key`。上游若加验证码、签名或设备指纹,扫码登录会直接失效(届时只显示「登录暂不可用」),但已保存的凭据仍然可用。
313
328
  - [ ] **扫码登录的凭据不会被主动刷新**:`ssoSessionId` 过期后需要重新扫码;插件不会拿它去换新(上游也没有公开的续期接口,客户端是重新登录)。
314
329
  - [ ] **一个 DSH 只保存一个扫码登录**:后期换账号需要先「退出登录」再扫,不做多账号并存。
315
- - [ ] **领取按钮依赖两个硬编码的客户端身份头**:`clientType`(平台)与 `studioVersion`(常量 `3.5.5`)。上游一旦要求构建号、签名或设备指纹,这条路会立即失效,届时按钮只会显示「暂不可用」。
330
+ - [ ] **领取按钮依赖三个硬编码的客户端身份头**:`clientType`(平台)、`studioVersion` 与 `clientVersion`(后两者都是常量 `3.6.0`,跟随当前客户端版本)。上游一旦要求构建号、签名或设备指纹,这条路会立即失效,届时按钮只会显示「暂不可用」。
331
+ - [ ] **每日奖励只在桌面端启动之后才可领**:客户端不启动,服务端就不挂当天的弹窗,插件轮到的永远是空列表,按钮只能如实报「当前没有可领取的奖励」。实测(`2026-11-05`,客户端未运行)请求头三种组合、外加重放客户端启动时的只读接口,都改变不了这一点;同日启动客户端后余额立即 +100。这不是插件能修的缺陷,而是上游的发放时机——想当天在 DSH 里领,就得先开一次 AStudio。
316
332
  - [ ] **CLI 的 `status` 会发起一次实时刷新**:它现在等待一次真实请求,才能如实报出目录来源与模型数;离线或未登录时报告 `empty` 与对应的失败原因。
317
333
  - [ ] **不做账号切换跟随**:与 WorkBuddy 双 variant 不同,AStudio 只有一个产品,插件不复制 variant 级能力。
318
334
 
package/lib/bin.js CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { i as createAStudioAuth, t as createAStudioCatalog } from "./catalog-CF9LFZ4P.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
@@ -518,6 +518,32 @@ function createAStudioAuth(options = {}) {
518
518
  };
519
519
  /** The chat token in use: our own sign-in wins over the app's. */
520
520
  const resolvedToken = () => own()?.modelBearerToken ?? clientToken();
521
+ /**
522
+ * The cookie parts the account APIs authenticate with.
523
+ *
524
+ * The desktop client sends all four cookies (`ssoSessionId`, `sso_sessionid`,
525
+ * `account_id`, `token`) on every account call, and the reward endpoints are
526
+ * the ones that care about the extra two: `pending` is written against an
527
+ * identified account, not just a session. `token` is missing for a sign-in
528
+ * this plugin made itself — the QR exchange returns a model key, not an
529
+ * account token — so it is omitted rather than faked.
530
+ */
531
+ const claimCredentials = () => {
532
+ const session = own();
533
+ if (session !== void 0) return {
534
+ ssoSessionId: session.ssoSessionId,
535
+ accountId: session.accountId
536
+ };
537
+ const ssoSessionId = readClientSsoSessionId(sessionPath());
538
+ if (ssoSessionId === void 0) return void 0;
539
+ const accountId = readSessionField(sessionPath(), "accountId");
540
+ const token = readSessionField(sessionPath(), "token");
541
+ return {
542
+ ssoSessionId,
543
+ ...accountId === void 0 ? {} : { accountId },
544
+ ...token === void 0 ? {} : { token }
545
+ };
546
+ };
521
547
  /** The model key in use, with the same precedence. */
522
548
  const resolvedModelToken = () => own()?.modelBearerToken ?? clientModelToken();
523
549
  return {
@@ -538,6 +564,16 @@ function createAStudioAuth(options = {}) {
538
564
  return own()?.ssoSessionId ?? readClientSsoSessionId(sessionPath());
539
565
  },
540
566
  /**
567
+ * The same credential, in the shape the claim's cookie needs.
568
+ *
569
+ * Separate from {@link ssoSessionId} because the reward endpoints want the
570
+ * account identified by cookie rather than by session alone, and only this
571
+ * module knows where the account id and token live.
572
+ */
573
+ claimCredentials() {
574
+ return claimCredentials();
575
+ },
576
+ /**
541
577
  * The `astron-session.json` this store reads the app's credentials from.
542
578
  *
543
579
  * Still reported for the status card, which names the file it looked at —
package/lib/client.js CHANGED
@@ -1015,7 +1015,7 @@ window.__ModuleLoader__.load({
1015
1015
  function isClaimDocument(value) {
1016
1016
  if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
1017
1017
  const status = value["status"];
1018
- return status === "claimed" || status === "none" || status === "signed-out" || status === "expired" || status === "unavailable";
1018
+ return status === "claimed" || status === "already" || status === "none" || status === "signed-out" || status === "expired" || status === "unavailable";
1019
1019
  }
1020
1020
  /** Narrow an unknown JSON value to the QR start route's document. */
1021
1021
  function isQrStartDocument(value) {
@@ -1636,6 +1636,13 @@ window.__ModuleLoader__.load({
1636
1636
  });
1637
1637
  return;
1638
1638
  }
1639
+ if (result.status === "already") {
1640
+ setNotice({
1641
+ ok: false,
1642
+ text: t("claimAlready")
1643
+ });
1644
+ return;
1645
+ }
1639
1646
  if (result.status === "none") {
1640
1647
  setNotice({
1641
1648
  ok: false,
@@ -1792,7 +1799,8 @@ window.__ModuleLoader__.load({
1792
1799
  claiming: "Claiming…",
1793
1800
  claimed: "+{points}",
1794
1801
  claimedUnknown: "Claimed",
1795
- claimNone: "Already claimed today",
1802
+ claimAlready: "Already claimed today",
1803
+ claimNone: "Nothing to claim right now — it may already be booked by AStudio, or not issued yet",
1796
1804
  pointsNav: "AStudio points",
1797
1805
  pointsDailyHint: "One daily login reward per day — the same one the AStudio app books.",
1798
1806
  pointsSignedOut: "AStudio is not signed in; the balances appear here once it is.",
@@ -1868,7 +1876,8 @@ window.__ModuleLoader__.load({
1868
1876
  claiming: "领取中…",
1869
1877
  claimed: "+{points}",
1870
1878
  claimedUnknown: "已领取",
1871
- claimNone: "今日已领过",
1879
+ claimAlready: "今日已领过",
1880
+ claimNone: "当前没有可领取的奖励——可能已随 AStudio 入账,或今日尚未发放",
1872
1881
  pointsNav: "AStudio 积分",
1873
1882
  pointsDailyHint: "每日登录奖励每天一份,与 AStudio 客户端领的是同一份。",
1874
1883
  pointsSignedOut: "AStudio 未登录,登录后这里会显示账户积分。",
package/lib/index.d.ts CHANGED
@@ -246,7 +246,15 @@ type AStudioWebClaim = {
246
246
  points?: number;
247
247
  balance: AStudioWebPoints;
248
248
  } |
249
- /** Signed in, but the account service has no reward pending for today. */
249
+ /**
250
+ * Upstream answered `11003002` for every popup found: today's reward was
251
+ * booked before this press. The one state that proves a claim already
252
+ * happened, as opposed to merely finding nothing.
253
+ */
254
+ {
255
+ status: 'already';
256
+ } |
257
+ /** Signed in, but the account service is holding no reward for today. */
250
258
  {
251
259
  status: 'none';
252
260
  } | {
@@ -257,6 +265,119 @@ type AStudioWebClaim = {
257
265
  status: 'unavailable';
258
266
  };
259
267
  //#endregion
268
+ //#region src/points.d.ts
269
+ /** A read the points surface can present. */
270
+ type AStudioPointsResult = AStudioWebPoints;
271
+ /** Options for {@link createAStudioPointsClient}. */
272
+ interface AStudioPointsClientOptions {
273
+ /**
274
+ * The account cookie to call with, or undefined when nobody is signed in.
275
+ *
276
+ * A provider rather than a file path: the cookie can come from the AStudio
277
+ * app's `astron-session.json` or from a sign-in this plugin made itself, and
278
+ * this module has no business knowing which.
279
+ */
280
+ sessionId: () => string | undefined;
281
+ /** Injected for tests. */
282
+ fetchImpl?: typeof fetch;
283
+ /** Injected clock, for TTL tests. */
284
+ now?: () => number;
285
+ /** Per-read timeout. */
286
+ timeoutMs?: number;
287
+ }
288
+ /** What the plugin and route consume. */
289
+ interface AStudioPointsClient {
290
+ /**
291
+ * Read the balance.
292
+ *
293
+ * `fresh` bypasses the TTL cache — the section's ↻ uses it so a manual click
294
+ * always reaches upstream.
295
+ */
296
+ read(options?: {
297
+ fresh?: boolean;
298
+ }): Promise<AStudioPointsResult>;
299
+ }
300
+ /**
301
+ * Read the `ssoSessionId` from an AStudio session file.
302
+ *
303
+ * The app's way of finding the cookie. Kept exported because the auth store and
304
+ * the tests still read a file directly; the points client itself takes whichever
305
+ * provider it is handed.
306
+ *
307
+ * Returns undefined for a missing file, malformed JSON, an absent field, or a
308
+ * blank value — all of which mean "not signed in", a normal state the points
309
+ * section names rather than an error it reports.
310
+ */
311
+ declare function readSsoSessionId(sessionPath: string): string | undefined;
312
+ /**
313
+ * Parse the account service's balance envelope.
314
+ *
315
+ * The envelope is the legacy Astron shape — `{ flag, code, desc, data }` — and a
316
+ * dead session is answered with HTTP 200 plus `code: 80000`, which is why the
317
+ * status has to be read from the body as well as the HTTP status.
318
+ */
319
+ declare function parseAStudioPointsBalance(payload: unknown): AStudioPointsResult;
320
+ /**
321
+ * Create the balance client.
322
+ *
323
+ * The session file is re-read on every upstream call rather than cached: when
324
+ * AStudio is signed in again it rewrites that file with a new `ssoSessionId`, and
325
+ * a cached credential would keep the points section pinned to an expired session.
326
+ */
327
+ declare function createAStudioPointsClient(options: AStudioPointsClientOptions): AStudioPointsClient;
328
+ //#endregion
329
+ //#region src/daily-reward.d.ts
330
+ /** Options for {@link createAStudioDailyRewardClient}. */
331
+ interface AStudioDailyRewardClientOptions {
332
+ /**
333
+ * The account this claim is made against, or undefined when nobody is signed in.
334
+ *
335
+ * A provider, so the credential can come from the AStudio app's session file
336
+ * or from a sign-in this plugin made itself — and so a re-login between two
337
+ * presses is picked up instead of pinning the first session.
338
+ */
339
+ credentials: () => AStudioDailyRewardCredentials | undefined;
340
+ /** Balance client used to report the post-claim balance. */
341
+ points: AStudioPointsClient;
342
+ /** Injected for tests. */
343
+ fetchImpl?: typeof fetch;
344
+ /** Overrides {@link astudioClientType}; `undefined` means "this platform has none". */
345
+ clientType?: string | undefined;
346
+ /** Overrides {@link ASTRO_STUDIO_VERSION}. */
347
+ studioVersion?: string;
348
+ /** Overrides {@link ASTRO_CLIENT_VERSION}. */
349
+ clientVersion?: string;
350
+ /** Per-call timeout. */
351
+ timeoutMs?: number;
352
+ }
353
+ /**
354
+ * The cookie parts the account APIs authenticate with.
355
+ *
356
+ * Mirrors what the desktop client sends: `ssoSessionId` and `sso_sessionid`
357
+ * are the same value under both names, and `account_id`/`token` are what make
358
+ * the request *account-scoped* rather than merely session-scoped. `token` is
359
+ * absent for a QR sign-in this plugin made itself — that exchange returns a
360
+ * model key, not an account token — and is then left out rather than guessed.
361
+ */
362
+ interface AStudioDailyRewardCredentials {
363
+ ssoSessionId: string;
364
+ accountId?: string;
365
+ token?: string;
366
+ }
367
+ /** What the plugin and the claim route consume. */
368
+ interface AStudioDailyRewardClient {
369
+ /** Book today's reward, if the account service is holding one. */
370
+ claim(): Promise<AStudioWebClaim>;
371
+ }
372
+ /**
373
+ * Create the claim client.
374
+ *
375
+ * The session file is re-read on every call for the same reason the balance
376
+ * reader does: AStudio rewrites it on re-login, and a cached credential would
377
+ * pin this to an expired session.
378
+ */
379
+ declare function createAStudioDailyRewardClient(options: AStudioDailyRewardClientOptions): AStudioDailyRewardClient;
380
+ //#endregion
260
381
  //#region src/auth.d.ts
261
382
  /** What the shim and upstream need from a credential. */
262
383
  interface AStudioCredentialStore {
@@ -321,6 +442,14 @@ declare function createAStudioAuth(options?: AStudioAuthOptions): {
321
442
  * precedence too.
322
443
  */
323
444
  ssoSessionId(): AStudioCredential;
445
+ /**
446
+ * The same credential, in the shape the claim's cookie needs.
447
+ *
448
+ * Separate from {@link ssoSessionId} because the reward endpoints want the
449
+ * account identified by cookie rather than by session alone, and only this
450
+ * module knows where the account id and token live.
451
+ */
452
+ claimCredentials(): AStudioDailyRewardCredentials | undefined;
324
453
  /**
325
454
  * The `astron-session.json` this store reads the app's credentials from.
326
455
  *
@@ -398,102 +527,6 @@ interface AStudioAdapter {
398
527
  */
399
528
  declare function createAStudioAdapter(options: AStudioAdapterOptions): AStudioAdapter;
400
529
  //#endregion
401
- //#region src/points.d.ts
402
- /** A read the points surface can present. */
403
- type AStudioPointsResult = AStudioWebPoints;
404
- /** Options for {@link createAStudioPointsClient}. */
405
- interface AStudioPointsClientOptions {
406
- /**
407
- * The account cookie to call with, or undefined when nobody is signed in.
408
- *
409
- * A provider rather than a file path: the cookie can come from the AStudio
410
- * app's `astron-session.json` or from a sign-in this plugin made itself, and
411
- * this module has no business knowing which.
412
- */
413
- sessionId: () => string | undefined;
414
- /** Injected for tests. */
415
- fetchImpl?: typeof fetch;
416
- /** Injected clock, for TTL tests. */
417
- now?: () => number;
418
- /** Per-read timeout. */
419
- timeoutMs?: number;
420
- }
421
- /** What the plugin and route consume. */
422
- interface AStudioPointsClient {
423
- /**
424
- * Read the balance.
425
- *
426
- * `fresh` bypasses the TTL cache — the section's ↻ uses it so a manual click
427
- * always reaches upstream.
428
- */
429
- read(options?: {
430
- fresh?: boolean;
431
- }): Promise<AStudioPointsResult>;
432
- }
433
- /**
434
- * Read the `ssoSessionId` from an AStudio session file.
435
- *
436
- * The app's way of finding the cookie. Kept exported because the auth store and
437
- * the tests still read a file directly; the points client itself takes whichever
438
- * provider it is handed.
439
- *
440
- * Returns undefined for a missing file, malformed JSON, an absent field, or a
441
- * blank value — all of which mean "not signed in", a normal state the points
442
- * section names rather than an error it reports.
443
- */
444
- declare function readSsoSessionId(sessionPath: string): string | undefined;
445
- /**
446
- * Parse the account service's balance envelope.
447
- *
448
- * The envelope is the legacy Astron shape — `{ flag, code, desc, data }` — and a
449
- * dead session is answered with HTTP 200 plus `code: 80000`, which is why the
450
- * status has to be read from the body as well as the HTTP status.
451
- */
452
- declare function parseAStudioPointsBalance(payload: unknown): AStudioPointsResult;
453
- /**
454
- * Create the balance client.
455
- *
456
- * The session file is re-read on every upstream call rather than cached: when
457
- * AStudio is signed in again it rewrites that file with a new `ssoSessionId`, and
458
- * a cached credential would keep the points section pinned to an expired session.
459
- */
460
- declare function createAStudioPointsClient(options: AStudioPointsClientOptions): AStudioPointsClient;
461
- //#endregion
462
- //#region src/daily-reward.d.ts
463
- /** Options for {@link createAStudioDailyRewardClient}. */
464
- interface AStudioDailyRewardClientOptions {
465
- /**
466
- * The account cookie, or undefined when nobody is signed in.
467
- *
468
- * A provider, so the cookie can come from the AStudio app's session file or
469
- * from a sign-in this plugin made itself.
470
- */
471
- sessionId: () => string | undefined;
472
- /** Balance client used to report the post-claim balance. */
473
- points: AStudioPointsClient;
474
- /** Injected for tests. */
475
- fetchImpl?: typeof fetch;
476
- /** Overrides {@link astudioClientType}; `undefined` means "this platform has none". */
477
- clientType?: string | undefined;
478
- /** Overrides {@link ASTRO_STUDIO_VERSION}. */
479
- studioVersion?: string;
480
- /** Per-call timeout. */
481
- timeoutMs?: number;
482
- }
483
- /** What the plugin and the claim route consume. */
484
- interface AStudioDailyRewardClient {
485
- /** Book today's reward, if the account service is holding one. */
486
- claim(): Promise<AStudioWebClaim>;
487
- }
488
- /**
489
- * Create the claim client.
490
- *
491
- * The session file is re-read on every call for the same reason the balance
492
- * reader does: AStudio rewrites it on re-login, and a cached credential would
493
- * pin this to an expired session.
494
- */
495
- declare function createAStudioDailyRewardClient(options: AStudioDailyRewardClientOptions): AStudioDailyRewardClient;
496
- //#endregion
497
530
  //#region src/login-session.d.ts
498
531
  /** On-disk format; readers reject anything else. */
499
532
  declare const LOGIN_SESSION_FORMAT_VERSION = 1;
package/lib/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { a as readTokenFromConfig, c as describeLoginSession, d as writeLoginSession, i as createAStudioAuth, l as parseLoginSession, o as astudioLoginSessionPath, r as localIdentity, s as clearLoginSession, t as createAStudioCatalog, u as readLoginSession } from "./catalog-CF9LFZ4P.js";
1
+ import { a as readTokenFromConfig, c as describeLoginSession, d as writeLoginSession, i as createAStudioAuth, l as parseLoginSession, o as astudioLoginSessionPath, r as localIdentity, s as clearLoginSession, t as createAStudioCatalog, u as readLoginSession } from "./catalog-CILzWwRh.js";
2
2
  import z from "@deepseek-ai/schemastery";
3
3
  import { existsSync, readFileSync } from "node:fs";
4
4
  import { dirname, join } from "node:path";
@@ -2387,6 +2387,19 @@ function astudioClientType(platform = process.platform) {
2387
2387
  if (platform === "darwin") return "22";
2388
2388
  }
2389
2389
  /**
2390
+ * Read a popup id the way the desktop client does.
2391
+ *
2392
+ * The id is a positive integer on the wire, and `client-popups/complete` is
2393
+ * parsed with `Number.isSafeInteger` upstream. Accepting only strings here is
2394
+ * what once made every real reward look like "nothing pending": the popup was
2395
+ * delivered, failed this test, and the section announced a reward that had
2396
+ * never been booked.
2397
+ */
2398
+ function popupIdOf(value) {
2399
+ const id = typeof value === "number" ? value : typeof value === "string" ? Number(value) : NaN;
2400
+ return Number.isSafeInteger(id) && id > 0 ? id : void 0;
2401
+ }
2402
+ /**
2390
2403
  * Reduce the gateway's `{ flag, code, desc, data }` envelope.
2391
2404
  *
2392
2405
  * A dead session is answered with HTTP 200 plus `code: 80000`, so the status has
@@ -2427,9 +2440,9 @@ function pendingRewards(data) {
2427
2440
  if (typeof raw !== "object" || raw === null) continue;
2428
2441
  const item = raw;
2429
2442
  if (item["componentType"] !== "DAILY_REWARD_DIALOG") continue;
2430
- const popupId = item["popupId"];
2443
+ const popupId = popupIdOf(item["popupId"]);
2431
2444
  const instanceKey = item["instanceKey"];
2432
- if (typeof popupId !== "string" || popupId === "") continue;
2445
+ if (popupId === void 0) continue;
2433
2446
  if (typeof instanceKey !== "string" || instanceKey === "") continue;
2434
2447
  rewards.push({
2435
2448
  popupId,
@@ -2440,6 +2453,20 @@ function pendingRewards(data) {
2440
2453
  return rewards;
2441
2454
  }
2442
2455
  /**
2456
+ * Build the cookie header the account APIs expect.
2457
+ *
2458
+ * Same four pairs the desktop client sends, in the same order. The claim is not
2459
+ * an anonymous "is there a reward?" question — the popup service answers an
2460
+ * unidentified caller with an empty list, which is indistinguishable from "no
2461
+ * reward today" unless the account travels with the request.
2462
+ */
2463
+ function claimCookie(credentials) {
2464
+ const parts = [`ssoSessionId=${credentials.ssoSessionId}`, `sso_sessionid=${credentials.ssoSessionId}`];
2465
+ if (credentials.accountId !== void 0) parts.push(`account_id=${credentials.accountId}`);
2466
+ if (credentials.token !== void 0) parts.push(`token=${credentials.token}`);
2467
+ return parts.join("; ");
2468
+ }
2469
+ /**
2443
2470
  * Create the claim client.
2444
2471
  *
2445
2472
  * The session file is re-read on every call for the same reason the balance
@@ -2449,18 +2476,20 @@ function pendingRewards(data) {
2449
2476
  function createAStudioDailyRewardClient(options) {
2450
2477
  const fetchImpl = options.fetchImpl ?? fetch;
2451
2478
  const clientType = "clientType" in options ? options.clientType : astudioClientType();
2452
- const studioVersion = options.studioVersion ?? "3.5.5";
2479
+ const studioVersion = options.studioVersion ?? "3.6.0";
2480
+ const clientVersion = options.clientVersion ?? "3.6.0";
2453
2481
  const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS$1;
2454
- const call = async (sessionId, path, init = {}) => {
2482
+ const call = async (credentials, path, init = {}) => {
2455
2483
  let response;
2456
2484
  try {
2457
2485
  response = await fetchImpl(new URL(path, ASTRO_STUDIO_API_BASE), {
2458
2486
  ...init,
2459
2487
  headers: {
2460
2488
  Accept: "application/json",
2461
- Cookie: `ssoSessionId=${sessionId}`,
2489
+ Cookie: claimCookie(credentials),
2462
2490
  ...clientType === void 0 ? {} : { clientType },
2463
2491
  studioVersion,
2492
+ clientVersion,
2464
2493
  ...init.headers ?? {}
2465
2494
  },
2466
2495
  signal: AbortSignal.timeout(timeoutMs)
@@ -2479,18 +2508,19 @@ function createAStudioDailyRewardClient(options) {
2479
2508
  return readEnvelope(payload);
2480
2509
  };
2481
2510
  return { async claim() {
2482
- const sessionId = options.sessionId();
2483
- if (sessionId === void 0) return { status: "signed-out" };
2511
+ const credentials = options.credentials();
2512
+ if (credentials === void 0) return { status: "signed-out" };
2484
2513
  if (clientType === void 0) return { status: "unavailable" };
2485
- const pending = await call(sessionId, ASTRO_DAILY_REWARD_PENDING_PATH);
2514
+ const pending = await call(credentials, ASTRO_DAILY_REWARD_PENDING_PATH);
2486
2515
  if (pending.kind === "expired") return { status: "expired" };
2487
2516
  if (pending.kind !== "ok") return { status: "unavailable" };
2488
2517
  const rewards = pendingRewards(pending.data);
2489
2518
  if (rewards.length === 0) return { status: "none" };
2490
2519
  let booked = 0;
2520
+ let alreadyBooked = 0;
2491
2521
  let bookedAmount;
2492
2522
  for (const reward of rewards) {
2493
- const answer = await call(sessionId, ASTRO_DAILY_REWARD_COMPLETE_PATH, {
2523
+ const answer = await call(credentials, ASTRO_DAILY_REWARD_COMPLETE_PATH, {
2494
2524
  method: "POST",
2495
2525
  headers: { "Content-Type": "application/json" },
2496
2526
  body: JSON.stringify({
@@ -2499,7 +2529,11 @@ function createAStudioDailyRewardClient(options) {
2499
2529
  })
2500
2530
  });
2501
2531
  if (answer.kind === "expired") return { status: "expired" };
2502
- if (answer.kind === "ok" || answer.kind === "already") {
2532
+ if (answer.kind === "already") {
2533
+ alreadyBooked += 1;
2534
+ continue;
2535
+ }
2536
+ if (answer.kind === "ok") {
2503
2537
  booked += 1;
2504
2538
  if (reward.points !== void 0) bookedAmount = (bookedAmount ?? 0) + reward.points;
2505
2539
  continue;
@@ -2507,6 +2541,7 @@ function createAStudioDailyRewardClient(options) {
2507
2541
  if (booked === 0) return { status: "unavailable" };
2508
2542
  break;
2509
2543
  }
2544
+ if (booked === 0) return { status: alreadyBooked > 0 ? "already" : "none" };
2510
2545
  const balance = await options.points.read({ fresh: true });
2511
2546
  return bookedAmount === void 0 ? {
2512
2547
  status: "claimed",
@@ -3267,7 +3302,7 @@ function registerAStudioWebRoutes(ctx, deps) {
3267
3302
  const pointsClient = createAStudioPointsClient({ sessionId: () => store.ssoSessionId() });
3268
3303
  registerAStudioPointsRoute(ctx, { client: pointsClient });
3269
3304
  registerAStudioClaimRoute(ctx, { client: createAStudioDailyRewardClient({
3270
- sessionId: () => store.ssoSessionId(),
3305
+ credentials: () => store.claimCredentials(),
3271
3306
  points: pointsClient
3272
3307
  }) }, controlKey);
3273
3308
  const afterSignInChange = () => {
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "dsh-astudio-connect",
3
3
  "displayName": "DSH AStudio Connect",
4
4
  "description": "将 AStudio 应用中已登录的模型复用到 DeepSeek Harness — bring AStudio app models into DeepSeek Harness with zero configuration.",
5
- "version": "0.2.0",
5
+ "version": "0.2.2",
6
6
  "author": "astudio-connect",
7
7
  "keywords": [
8
8
  "dsh-plugin",