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 +19 -3
- package/README.md +19 -3
- package/lib/bin.js +1 -1
- package/lib/{catalog-CF9LFZ4P.js → catalog-CILzWwRh.js} +36 -0
- package/lib/client.js +12 -3
- package/lib/index.d.ts +130 -97
- package/lib/index.js +47 -12
- package/package.json +1 -1
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
|
-
|
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
- [ ]
|
|
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-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
/**
|
|
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-
|
|
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 (
|
|
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.
|
|
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 (
|
|
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:
|
|
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
|
|
2483
|
-
if (
|
|
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(
|
|
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(
|
|
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 === "
|
|
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
|
-
|
|
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.
|
|
5
|
+
"version": "0.2.2",
|
|
6
6
|
"author": "astudio-connect",
|
|
7
7
|
"keywords": [
|
|
8
8
|
"dsh-plugin",
|