dsh-connect-trae 4.9.1 → 4.14.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/lib/index.d.ts CHANGED
@@ -775,6 +775,12 @@ interface TraeWebPoolAccount {
775
775
  * card must render "no free model" rather than silently substituting a paid one.
776
776
  */
777
777
  interface TraeWebPool {
778
+ autoCheckinState?: {
779
+ done: number;
780
+ deviceBlocked: boolean;
781
+ failures: number;
782
+ };
783
+ autoCheckin?: boolean;
778
784
  enabled: boolean;
779
785
  /**
780
786
  * Whether this panel offers the daily check-in at all.
@@ -865,6 +871,8 @@ interface TraeWebPool {
865
871
  * slot must still load, and a missing `enabled` means OFF rather than ON.
866
872
  */
867
873
  interface TraePoolPreferences {
874
+ /** 保存后启用国内个人账号池的每日自动签到。 */
875
+ autoCheckin?: boolean;
868
876
  /** Whether this panel bills through the pool at all. Default OFF. */
869
877
  enabled?: boolean;
870
878
  /**
@@ -4362,6 +4370,15 @@ interface TraePoolTestRow {
4362
4370
  retryAtMs?: number;
4363
4371
  }
4364
4372
  //#endregion
4373
+ //#region src/auto-checkin.d.ts
4374
+ interface AutoCheckinState {
4375
+ day: string;
4376
+ done: string[];
4377
+ deviceBlocked: boolean;
4378
+ failures: number;
4379
+ retryAt: number;
4380
+ }
4381
+ //#endregion
4365
4382
  //#region src/web-status.d.ts
4366
4383
  /**
4367
4384
  * Constructor dependencies. Everything panel-specific is addressed by the
@@ -4370,6 +4387,7 @@ interface TraePoolTestRow {
4370
4387
  * user is on.
4371
4388
  */
4372
4389
  interface TraeUsageRouteOptions {
4390
+ autoCheckinProgress?(): Promise<AutoCheckinState | undefined>;
4373
4391
  /** The panel-scoped credential store backing each region's requests. */
4374
4392
  store(panel: TraePanel): TraeCredentialStore;
4375
4393
  /** The panel-scoped usage client (CN work credits / international pay status). */
package/lib/index.js CHANGED
@@ -1,20 +1,101 @@
1
+ import { copyFile, mkdir, readFile, readdir, rename, rm, stat, writeFile } from "node:fs/promises";
2
+ import { basename, dirname, join } from "node:path";
3
+ import { withFileLock, writeFileAtomic } from "@deepseek-ai/dsh-atomic-write";
4
+ import { resolveDshHome } from "@deepseek-ai/dsh-home-paths";
1
5
  import z from "@deepseek-ai/schemastery";
2
6
  import { createProvider } from "@earendil-works/pi-ai";
3
7
  import { openAICompletionsApi } from "@earendil-works/pi-ai/api/openai-completions.lazy";
4
8
  import { ReasoningEffortId, resolveRetryPolicy } from "@deepseek-ai/dsh-llm";
5
9
  import { PiAiAdapter } from "@deepseek-ai/dsh-llm-pi-ai";
6
10
  import { existsSync, readFileSync } from "node:fs";
7
- import { basename, dirname, join } from "node:path";
8
11
  import { fileURLToPath } from "node:url";
9
- import { copyFile, mkdir, readFile, readdir, rename, rm, stat, writeFile } from "node:fs/promises";
10
12
  import { createCipheriv, createDecipheriv, createHash, randomBytes, randomUUID, sign, timingSafeEqual } from "node:crypto";
11
- import { withFileLock, writeFileAtomic } from "@deepseek-ai/dsh-atomic-write";
12
- import { resolveDshHome } from "@deepseek-ai/dsh-home-paths";
13
13
  import { cpus, homedir, hostname, release, userInfo } from "node:os";
14
14
  import { execFile } from "node:child_process";
15
15
  import { promisify } from "node:util";
16
16
  import { createServer } from "node:http";
17
17
  import { Readable } from "node:stream";
18
+ //#region src/auto-checkin.ts
19
+ /** 按运行机器的本地日期组织进度;领取资格始终以服务器响应为准。 */
20
+ function checkinDay(now) {
21
+ const date = new Date(now);
22
+ return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, "0")}-${String(date.getDate()).padStart(2, "0")}`;
23
+ }
24
+ function foldCheckin(state, rows, now) {
25
+ const done = new Set(state.done);
26
+ for (const row of rows) if (row.status === "already" || row.status === "claimed") done.add(row.accountId);
27
+ const failures = rows.some((row) => row.status === "failed") ? state.failures + 1 : 0;
28
+ return {
29
+ day: state.day,
30
+ done: [...done],
31
+ deviceBlocked: state.deviceBlocked || rows.some((row) => row.status === "device-already" || row.status === "claimed"),
32
+ failures,
33
+ retryAt: now + (rows.some((row) => row.status === "disabled") && failures === 0 ? 18e5 : Math.min(18e5, 3e4 * 2 ** Math.min(failures, 6)))
34
+ };
35
+ }
36
+ /** 单实例内串行执行。关闭开关或卸载后不再启动新批次。 */
37
+ function createAutoCheckin(deps) {
38
+ let running = false;
39
+ let stopped = false;
40
+ let memory;
41
+ return {
42
+ stop() {
43
+ stopped = true;
44
+ },
45
+ async tick() {
46
+ if (stopped || running || !deps.enabled()) return;
47
+ running = true;
48
+ try {
49
+ const now = deps.now?.() ?? Date.now();
50
+ const day = checkinDay(now);
51
+ const stored = memory ?? await deps.read();
52
+ const state = stored?.day === day ? stored : {
53
+ day,
54
+ done: [],
55
+ deviceBlocked: false,
56
+ failures: 0,
57
+ retryAt: 0
58
+ };
59
+ memory = state;
60
+ if (stopped || !deps.enabled() || state.deviceBlocked || now < state.retryAt) return;
61
+ let rows;
62
+ try {
63
+ rows = await deps.run(state.done);
64
+ } catch {
65
+ rows = [{
66
+ accountId: "",
67
+ accountName: "",
68
+ status: "failed"
69
+ }];
70
+ }
71
+ memory = foldCheckin(state, rows, deps.now?.() ?? Date.now());
72
+ await deps.write(memory);
73
+ } finally {
74
+ running = false;
75
+ }
76
+ }
77
+ };
78
+ }
79
+ //#endregion
80
+ //#region src/auto-checkin-store.ts
81
+ /** 只保存日期、账号 id 和重试进度,不保存凭据或上游错误正文。 */
82
+ const autoCheckinPath = () => join(resolveDshHome(), ".trae-auto-checkin.cn.json");
83
+ async function readAutoCheckin() {
84
+ try {
85
+ const value = JSON.parse(await readFile(autoCheckinPath(), "utf8"));
86
+ return typeof value.day === "string" && Array.isArray(value.done) && value.done.every((id) => typeof id === "string") && typeof value.deviceBlocked === "boolean" && typeof value.failures === "number" && Number.isFinite(value.failures) && typeof value.retryAt === "number" && Number.isFinite(value.retryAt) ? value : void 0;
87
+ } catch {
88
+ return;
89
+ }
90
+ }
91
+ async function writeAutoCheckin(state) {
92
+ const path = autoCheckinPath();
93
+ await withFileLock(path, () => writeFileAtomic(path, `${JSON.stringify(state)}\n`, {
94
+ mode: 384,
95
+ dirMode: 448
96
+ }));
97
+ }
98
+ //#endregion
18
99
  //#region src/context-budget.ts
19
100
  /**
20
101
  * Context-budget arithmetic shared by the HOST (which applies a saved budget to
@@ -8710,13 +8791,56 @@ async function readTransferBody(req) {
8710
8791
  if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) throw new Error("body must be a JSON object");
8711
8792
  return parsed;
8712
8793
  }
8713
- /** Loopback browser origins only; other devices are refused until trusted origins exist. */
8794
+ /**
8795
+ * One hostname spelling: lowercased, IPv6 brackets dropped.
8796
+ *
8797
+ * `new URL('http://[::1]:3080').hostname` is the string `[::1]`, while a socket's
8798
+ * `localAddress` is `::1` — comparing the two without normalising means the
8799
+ * bracketed form never matches, which is why the old predicate listed both
8800
+ * spellings by hand.
8801
+ */
8802
+ function bareHostname(value) {
8803
+ const text = value.trim().toLowerCase();
8804
+ return text.startsWith("[") && text.endsWith("]") ? text.slice(1, -1) : text;
8805
+ }
8806
+ /**
8807
+ * Whether a request's `Origin` may be honoured (ISSUE #78).
8808
+ *
8809
+ * This used to accept loopback hostnames ONLY, and that was wrong in a way that
8810
+ * hid from every non-browser test: a browser always sends `Origin` on a
8811
+ * same-origin POST, so `dsh web --host 100.64.0.2` served the page from an
8812
+ * address the HOST layer had already accepted (`--trusted-host`) while this
8813
+ * predicate called that page's own origin foreign. All six write routes 403'd
8814
+ * in the real UI, and `curl` — which sends no `Origin` — stayed green. The
8815
+ * plugin's own fetch does not send it either, so nothing on the plugin side
8816
+ * noticed.
8817
+ *
8818
+ * The rule is now "same origin, however this process is addressed": loopback, or
8819
+ * a hostname equal to the `Host` header's, or one equal to the socket's own
8820
+ * local address. Hostnames only — a reverse proxy that changes the PORT must
8821
+ * still be admitted, and the port says nothing about who is asking.
8822
+ *
8823
+ * The `Host` comparison is what the sibling dsh-connect-workbuddy predicate
8824
+ * lacks, and it is here deliberately: a front proxy can rewrite `Host` to a
8825
+ * different trusted spelling than the address the browser used, so neither
8826
+ * `Host` nor `localAddress` alone covers every arrangement.
8827
+ *
8828
+ * Exact equality, never `endsWith`/`startsWith`: a suffix test would admit
8829
+ * `evil-100.64.0.2`, and a prefix test would admit `100.64.0.20`.
8830
+ */
8714
8831
  function loopbackOrigin(req) {
8715
8832
  const origin = req.headers.origin;
8716
- if (origin === void 0) return true;
8833
+ if (origin === void 0 || origin.trim() === "") return true;
8717
8834
  try {
8718
- const { hostname } = new URL(origin);
8719
- return hostname === "localhost" || hostname === "127.0.0.1" || hostname === "[::1]" || hostname === "::1";
8835
+ const hostname = bareHostname(new URL(origin).hostname);
8836
+ if (hostname === "localhost" || hostname === "127.0.0.1" || hostname === "::1") return true;
8837
+ const rawHost = req.headers.host;
8838
+ if (typeof rawHost === "string" && rawHost.trim() !== "") {
8839
+ if (hostname === bareHostname(new URL(`http://${rawHost}`).hostname)) return true;
8840
+ }
8841
+ const local = req.socket?.localAddress;
8842
+ if (typeof local === "string" && local.trim() !== "" && hostname === bareHostname(local)) return true;
8843
+ return false;
8720
8844
  } catch {
8721
8845
  return false;
8722
8846
  }
@@ -8993,7 +9117,9 @@ async function traeWebUsage(deps, panel, options = {}) {
8993
9117
  ...poolSection,
8994
9118
  creditsUnavailable: "enterprise"
8995
9119
  };
8996
- const [snapshotResult, checkinResult] = await Promise.allSettled([client.snapshot(), client.checkinStatus()]);
9120
+ const checkinProgress = await deps.autoCheckinProgress?.();
9121
+ const checkinCompleted = checkinProgress?.day === checkinDay(Date.now()) && (checkinProgress.deviceBlocked || checkinProgress.done.includes(account.accountId));
9122
+ const [snapshotResult, checkinResult] = await Promise.allSettled([client.snapshot(), checkinCompleted ? Promise.resolve(void 0) : client.checkinStatus()]);
8997
9123
  const credits = snapshotResult.status === "fulfilled" ? toCredits(snapshotResult.value) : void 0;
8998
9124
  const alternatives = credits !== void 0 && credits.workAvailable <= 0 && credits.generalAvailable <= 0 ? await creditAlternatives(deps, panel, accounts) : void 0;
8999
9125
  return {
@@ -9001,7 +9127,7 @@ async function traeWebUsage(deps, panel, options = {}) {
9001
9127
  ...account,
9002
9128
  ...poolSection,
9003
9129
  ...credits === void 0 ? { creditsError: snapshotResult.status === "rejected" ? safeMessage(snapshotResult.reason) : "unknown" } : { credits },
9004
- ...checkinResult.status === "fulfilled" ? { checkin: toCheckin(checkinResult.value) } : { checkinError: safeMessage(checkinResult.reason) },
9130
+ ...checkinResult.status === "fulfilled" ? checkinResult.value === void 0 ? {} : { checkin: toCheckin(checkinResult.value) } : { checkinError: safeMessage(checkinResult.reason) },
9005
9131
  ...alternatives === void 0 ? {} : { alternatives }
9006
9132
  };
9007
9133
  }
@@ -9191,9 +9317,16 @@ async function traeWebPool(deps, panel, fast = false) {
9191
9317
  else if (ranked.every((row) => row.excludedBy !== void 0)) noCandidate = new Set(ranked.map((row) => row.excludedBy)).size === 1 ? ranked[0]?.excludedBy ?? "unusable" : "unusable";
9192
9318
  }
9193
9319
  const effectiveMemberAccountIds = effectiveMembersOf(rows.filter((row) => row.member).map((row) => row.account.id), new Set(members.map((member) => member.account.id)));
9320
+ const automatic = panel === "cn" && preferences.autoCheckin === true ? await readAutoCheckin() : void 0;
9194
9321
  const copies = await deps.copiesOf?.(panel).catch(() => void 0);
9195
9322
  return {
9196
9323
  enabled: preferences.enabled === true,
9324
+ autoCheckin: preferences.autoCheckin === true,
9325
+ ...automatic?.day === checkinDay(Date.now()) ? { autoCheckinState: {
9326
+ done: automatic.done.length,
9327
+ deviceBlocked: automatic.deviceBlocked,
9328
+ failures: automatic.failures
9329
+ } } : {},
9197
9330
  checkinSupported: pool.checkin !== void 0 && regionOfPanel(panel) === "cn",
9198
9331
  ...deps.deleteAccountOf === void 0 ? {} : { deleteSupported: true },
9199
9332
  ...copies === void 0 ? {} : { copies },
@@ -11148,6 +11281,7 @@ const regionStateConfig = z.object({
11148
11281
  thinkingEfforts: z.dict(z.string()).default({}).description("每个模型的思考强度(模型 id → 档位)。留空 = 跟随该模型自己的默认档。"),
11149
11282
  creditPosition: z.union([z.const("composer"), z.const("stats")]).default("composer").description("积分读数显示位置:\"composer\" = 输入框工具栏(默认),\"stats\" = 输入框下方(与「轮次 / 步数 / token」统计同一行)。装了别的插件后读数不显示时改成 stats。"),
11150
11283
  pool: z.object({
11284
+ autoCheckin: z.boolean().default(false).description("每日自动签到:仅国内个人账号,遵守设备每日奖励限制。"),
11151
11285
  enabled: z.boolean().default(false).description("账号池:打开后本面板由池中可用账号轮流计费(关闭时按下面的绑定账号)"),
11152
11286
  targetModelId: z.string().default("").description("账号池测速使用的模型 id(留空 = 自动选择免费模型)"),
11153
11287
  memberAccountIds: z.array(z.string()).default([]).description("加入本面板账号池的账号 id(显式勾选,默认为空)"),
@@ -12008,7 +12142,7 @@ function apply(ctx, config) {
12008
12142
  * The bag is built per REQUEST, so it captures the panel and never has to be
12009
12143
  * told which tab asked.
12010
12144
  */
12011
- const poolDepsOf = (panel) => {
12145
+ const poolDepsOf = (panel, checkinDone = []) => {
12012
12146
  const bridges = /* @__PURE__ */ new Map();
12013
12147
  const chatBridgeFor = (accountId) => {
12014
12148
  const existing = bridges.get(accountId);
@@ -12080,7 +12214,7 @@ function apply(ctx, config) {
12080
12214
  },
12081
12215
  catalog: (stored) => poolCatalogOf(stored),
12082
12216
  checkinSupported: (stored) => regionOfPanel(stored) === "cn",
12083
- checkin: async (stored) => checkinAllAccounts(await poolTargetsOf(stored), probeDeps()),
12217
+ checkin: async (stored) => checkinAllAccounts((await poolTargetsOf(stored)).filter((target) => !checkinDone.includes(target.accountId)), probeDeps()),
12084
12218
  checkinState: async (stored) => {
12085
12219
  const state = cachedCheckinStateOf(stored);
12086
12220
  const nowMs = Date.now();
@@ -12140,6 +12274,7 @@ function apply(ctx, config) {
12140
12274
  };
12141
12275
  };
12142
12276
  ctx.inject(["webServer"], (webCtx) => registerTraeUsageRoute(webCtx, {
12277
+ autoCheckinProgress: readAutoCheckin,
12143
12278
  store: (panel) => stacks[panel].store,
12144
12279
  client: (panel) => stacks[panel].usageClient,
12145
12280
  displayModels: (panel) => displayModels(current(), panel),
@@ -12410,6 +12545,23 @@ function apply(ctx, config) {
12410
12545
  applySelection(current());
12411
12546
  syncTraeDrawImageTool(current());
12412
12547
  });
12548
+ const automaticCheckin = createAutoCheckin({
12549
+ enabled: () => poolPreferencesOf("cn").autoCheckin === true,
12550
+ read: readAutoCheckin,
12551
+ write: writeAutoCheckin,
12552
+ run: async (done) => await poolDepsOf("cn", done).checkin?.("cn") ?? []
12553
+ });
12554
+ ctx.effect(() => {
12555
+ const tick = () => {
12556
+ automaticCheckin.tick().catch(() => ctx.logger.warn("无法读取或保存 Trae 自动签到进度,请检查本地数据目录的访问权限。"));
12557
+ };
12558
+ const timer = setInterval(tick, 3e4);
12559
+ tick();
12560
+ return () => {
12561
+ clearInterval(timer);
12562
+ automaticCheckin.stop();
12563
+ };
12564
+ });
12413
12565
  let stopped = false;
12414
12566
  ctx.effect(() => () => {
12415
12567
  stopped = true;
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "dsh-connect-trae",
3
3
  "displayName": "DSH Connect Trae",
4
4
  "description": "把本机登录的 Trae 模型接入 DeepSeek Harness:国内版与国际版双供应商并行,提供用量/积分概览与每日签到领取。",
5
- "version": "4.9.1",
5
+ "version": "4.14.2",
6
6
  "author": "dingminhua",
7
7
  "copyright": "Copyright (c) 2026 LaoDing",
8
8
  "license": "MIT",
@@ -35,6 +35,7 @@
35
35
  "icons",
36
36
  "docs/assets/dsh-connect-trae-usage-card.png",
37
37
  "docs/assets/dsh-connect-trae-sidebar-page.png",
38
+ "docs/assets/dsh-connect-trae-composer-credits.png",
38
39
  "scripts/verify-windows.mjs",
39
40
  "screenshots.json",
40
41
  "cordis.patch.yml",
@@ -51,7 +52,12 @@
51
52
  "typecheck": "tsc -p tsconfig.json && tsc -p tsconfig.client.json",
52
53
  "check": "pnpm run typecheck && pnpm run test && pnpm run build",
53
54
  "verify:release": "node scripts/verify-release.mjs",
55
+ "verify:privacy": "node scripts/verify-privacy.mjs",
56
+ "prepublishOnly": "node scripts/verify-privacy.mjs",
54
57
  "card:shot": "vitest run --config vitest.card.config.ts",
58
+ "page:shot": "vitest run --config vitest.card.config.ts -t \"page shot\"",
59
+ "composer:shot": "vitest run --config vitest.card.config.ts -t \"composer shot\"",
60
+ "section:shot": "vitest run --config vitest.card.config.ts -t \"section shot\"",
55
61
  "promo:shot": "vitest run --config vitest.card.config.ts -t \"promo shot\"",
56
62
  "panels:shot": "vitest run --config vitest.card.config.ts -t \"panels shot\"",
57
63
  "prebuild": "node scripts/warn-live-host.mjs"
@@ -126,6 +132,7 @@
126
132
  "react": "^18.3.1",
127
133
  "react-dom": "18.3.1",
128
134
  "semver": "^7.7.2",
135
+ "tesseract.js": "^7.0.0",
129
136
  "tsdown": "^0.22.2",
130
137
  "typescript": "^5.9.3",
131
138
  "vitest": "^4.1.8"