dsh-connect-trae 4.10.0 → 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/CHANGELOG.md +393 -5
- package/README.en.md +16 -0
- package/README.md +15 -0
- package/docs/assets/dsh-connect-trae-composer-credits.png +0 -0
- package/docs/assets/dsh-connect-trae-sidebar-page.png +0 -0
- package/docs/assets/dsh-connect-trae-usage-card.png +0 -0
- package/lib/client.js +1053 -416
- package/lib/index.d.ts +18 -0
- package/lib/index.js +164 -12
- package/package.json +8 -1
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
|
-
/**
|
|
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
|
|
8719
|
-
|
|
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
|
|
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.
|
|
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"
|