@cjhyy/code-shell-web 0.9.46 → 0.9.47
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.
|
@@ -5,6 +5,7 @@ type Result = NonNullable<Awaited<ReturnType<typeof recoverMobileOutput>>>;
|
|
|
5
5
|
export declare class MobileOutputClient {
|
|
6
6
|
private readonly deps;
|
|
7
7
|
private enabled;
|
|
8
|
+
private basePages;
|
|
8
9
|
private auth;
|
|
9
10
|
private sequence;
|
|
10
11
|
private selection?;
|
|
@@ -21,12 +22,15 @@ export declare class MobileOutputClient {
|
|
|
21
22
|
awaitingJournal?: (sessionId: string) => void;
|
|
22
23
|
});
|
|
23
24
|
cancel(notify?: boolean): void;
|
|
25
|
+
private clearPending;
|
|
24
26
|
begin(sessionId: string): boolean;
|
|
25
27
|
private current;
|
|
26
28
|
owns(sessionId: string): boolean;
|
|
27
29
|
rejectUnsequenced(sessionId: string): boolean;
|
|
28
30
|
private fail;
|
|
29
31
|
observe(event: MobileServerEvent): boolean;
|
|
32
|
+
private request;
|
|
33
|
+
private readBase;
|
|
30
34
|
private recover;
|
|
31
35
|
/** Live output is withheld until it is covered by the frozen candidate. */
|
|
32
36
|
hold(sessionId: string, raw: unknown): false | "pending" | "covered";
|
|
@@ -4,6 +4,7 @@ import { compareOutputCursors } from "../lib/outputJournalRecovery.js";
|
|
|
4
4
|
export class MobileOutputClient {
|
|
5
5
|
deps;
|
|
6
6
|
enabled = false;
|
|
7
|
+
basePages = false;
|
|
7
8
|
auth = 0;
|
|
8
9
|
sequence = 0;
|
|
9
10
|
selection;
|
|
@@ -17,13 +18,25 @@ export class MobileOutputClient {
|
|
|
17
18
|
clearTimeout(old.readyTimer);
|
|
18
19
|
if (old?.capacityTimer)
|
|
19
20
|
clearTimeout(old.capacityTimer);
|
|
20
|
-
if (old
|
|
21
|
-
|
|
22
|
-
old.waiter.reject(new Error("Recovery cancelled"));
|
|
23
|
-
}
|
|
21
|
+
if (old)
|
|
22
|
+
this.clearPending(old, "Recovery cancelled");
|
|
24
23
|
if (notify && old)
|
|
25
24
|
this.deps.send({ type: "session.recovery.cancel" });
|
|
26
25
|
}
|
|
26
|
+
clearPending(selection, message) {
|
|
27
|
+
if (selection.waiter) {
|
|
28
|
+
const waiter = selection.waiter;
|
|
29
|
+
selection.waiter = undefined;
|
|
30
|
+
clearTimeout(waiter.timer);
|
|
31
|
+
waiter.reject(new Error(message));
|
|
32
|
+
}
|
|
33
|
+
if (selection.retry) {
|
|
34
|
+
const retry = selection.retry;
|
|
35
|
+
selection.retry = undefined;
|
|
36
|
+
clearTimeout(retry.timer);
|
|
37
|
+
retry.reject(new Error(message));
|
|
38
|
+
}
|
|
39
|
+
}
|
|
27
40
|
begin(sessionId) {
|
|
28
41
|
if (!this.enabled)
|
|
29
42
|
return false;
|
|
@@ -61,7 +74,7 @@ export class MobileOutputClient {
|
|
|
61
74
|
return true;
|
|
62
75
|
}
|
|
63
76
|
fail(selection) {
|
|
64
|
-
if (!this.current(selection))
|
|
77
|
+
if (!this.current(selection) || selection.failed)
|
|
65
78
|
return;
|
|
66
79
|
if (selection.readyTimer)
|
|
67
80
|
clearTimeout(selection.readyTimer);
|
|
@@ -72,6 +85,7 @@ export class MobileOutputClient {
|
|
|
72
85
|
selection.readyTimer = undefined;
|
|
73
86
|
selection.recovering = false;
|
|
74
87
|
selection.failed = true;
|
|
88
|
+
this.clearPending(selection, "Recovery failed");
|
|
75
89
|
this.deps.failed(selection.sessionId);
|
|
76
90
|
}
|
|
77
91
|
observe(event) {
|
|
@@ -79,9 +93,12 @@ export class MobileOutputClient {
|
|
|
79
93
|
this.cancel(false);
|
|
80
94
|
this.auth++;
|
|
81
95
|
this.enabled = event.capabilities?.outputJournal === 1;
|
|
96
|
+
this.basePages = this.enabled && event.capabilities?.legacyBasePages === 1;
|
|
82
97
|
return false;
|
|
83
98
|
}
|
|
84
|
-
if (event.type !== "session.recovery.ready" &&
|
|
99
|
+
if (event.type !== "session.recovery.ready" &&
|
|
100
|
+
event.type !== "session.outputJournal" &&
|
|
101
|
+
event.type !== "session.legacyBase")
|
|
85
102
|
return false;
|
|
86
103
|
const selection = this.selection;
|
|
87
104
|
if (!selection ||
|
|
@@ -107,7 +124,7 @@ export class MobileOutputClient {
|
|
|
107
124
|
else
|
|
108
125
|
void this.recover(selection);
|
|
109
126
|
}
|
|
110
|
-
else if (selection.waiter?.id === event.requestId) {
|
|
127
|
+
else if (selection.waiter?.id === event.requestId && selection.waiter.type === event.type) {
|
|
111
128
|
const waiter = selection.waiter;
|
|
112
129
|
selection.waiter = undefined;
|
|
113
130
|
clearTimeout(waiter.timer);
|
|
@@ -115,6 +132,60 @@ export class MobileOutputClient {
|
|
|
115
132
|
}
|
|
116
133
|
return true;
|
|
117
134
|
}
|
|
135
|
+
request(selection, type, options) {
|
|
136
|
+
return new Promise((resolve, reject) => {
|
|
137
|
+
if (!this.current(selection) || selection.failed || selection.waiter || selection.retry)
|
|
138
|
+
return reject(new Error("Recovery cancelled"));
|
|
139
|
+
const id = `page-${this.auth}-${++this.sequence}`;
|
|
140
|
+
const timer = setTimeout(() => {
|
|
141
|
+
selection.waiter = undefined;
|
|
142
|
+
reject(new Error("Recovery timed out"));
|
|
143
|
+
}, 10_000);
|
|
144
|
+
selection.waiter = {
|
|
145
|
+
id,
|
|
146
|
+
type,
|
|
147
|
+
timer,
|
|
148
|
+
reject,
|
|
149
|
+
resolve: (page) => resolve(page),
|
|
150
|
+
};
|
|
151
|
+
try {
|
|
152
|
+
if (!this.deps.send({
|
|
153
|
+
...options,
|
|
154
|
+
type,
|
|
155
|
+
sessionId: selection.sessionId,
|
|
156
|
+
recoveryId: selection.id,
|
|
157
|
+
requestId: id,
|
|
158
|
+
}))
|
|
159
|
+
throw new Error("Recovery disconnected");
|
|
160
|
+
}
|
|
161
|
+
catch (error) {
|
|
162
|
+
clearTimeout(timer);
|
|
163
|
+
selection.waiter = undefined;
|
|
164
|
+
reject(error);
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
async readBase(selection, options) {
|
|
169
|
+
for (let retries = 0;; retries++) {
|
|
170
|
+
const reply = await this.request(selection, "session.legacyBase", options);
|
|
171
|
+
if (new TextEncoder().encode(JSON.stringify(reply)).length > 1024 * 1024 + 4096 ||
|
|
172
|
+
(reply.ok === false &&
|
|
173
|
+
((reply.reason !== "busy" && reply.reason !== "incomplete") ||
|
|
174
|
+
Object.keys(reply).some((key) => !["type", "sessionId", "recoveryId", "requestId", "ok", "reason"].includes(key)))))
|
|
175
|
+
throw new Error("Invalid recovery base reply");
|
|
176
|
+
if (reply.ok !== false || reply.reason !== "busy" || retries >= 8)
|
|
177
|
+
return reply;
|
|
178
|
+
await new Promise((resolve, reject) => {
|
|
179
|
+
if (!this.current(selection) || selection.failed)
|
|
180
|
+
return reject(new Error("Recovery cancelled"));
|
|
181
|
+
const timer = setTimeout(() => {
|
|
182
|
+
selection.retry = undefined;
|
|
183
|
+
resolve();
|
|
184
|
+
}, 25);
|
|
185
|
+
selection.retry = { timer, reject };
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
}
|
|
118
189
|
async recover(selection, resume) {
|
|
119
190
|
try {
|
|
120
191
|
const result = await (resume
|
|
@@ -123,28 +194,12 @@ export class MobileOutputClient {
|
|
|
123
194
|
requireJournal: selection.journalRequired,
|
|
124
195
|
canContinue: () => this.current(selection) && !selection.failed,
|
|
125
196
|
latestCursor: () => selection.latest,
|
|
126
|
-
read: (options) =>
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
const timer = setTimeout(() => {
|
|
131
|
-
selection.waiter = undefined;
|
|
132
|
-
reject(new Error("Recovery timed out"));
|
|
133
|
-
}, 10_000);
|
|
134
|
-
selection.waiter = { id, resolve, reject, timer };
|
|
135
|
-
if (!this.deps.send({
|
|
136
|
-
type: "session.outputJournal",
|
|
137
|
-
sessionId: selection.sessionId,
|
|
138
|
-
recoveryId: selection.id,
|
|
139
|
-
requestId: id,
|
|
140
|
-
after: options.after,
|
|
141
|
-
through: options.through,
|
|
142
|
-
})) {
|
|
143
|
-
clearTimeout(timer);
|
|
144
|
-
selection.waiter = undefined;
|
|
145
|
-
reject(new Error("Recovery disconnected"));
|
|
197
|
+
read: (options) => this.request(selection, "session.outputJournal", options),
|
|
198
|
+
...(this.basePages
|
|
199
|
+
? {
|
|
200
|
+
readBase: (options) => this.readBase(selection, options),
|
|
146
201
|
}
|
|
147
|
-
|
|
202
|
+
: {}),
|
|
148
203
|
}));
|
|
149
204
|
if (!this.current(selection))
|
|
150
205
|
return;
|
|
@@ -4,6 +4,16 @@ import { initialChatState } from "./streamReducer.js";
|
|
|
4
4
|
type Reply = Extract<MobileServerEvent, {
|
|
5
5
|
type: "session.outputJournal";
|
|
6
6
|
}>;
|
|
7
|
+
type BaseReply = Extract<MobileServerEvent, {
|
|
8
|
+
type: "session.legacyBase";
|
|
9
|
+
}>;
|
|
10
|
+
export type MobileLegacyBaseOptions = {
|
|
11
|
+
throughEventId: string;
|
|
12
|
+
cursor?: never;
|
|
13
|
+
} | {
|
|
14
|
+
cursor: string;
|
|
15
|
+
throughEventId?: never;
|
|
16
|
+
};
|
|
7
17
|
/** Valid identities may arrive before their new Core Run acquires a durable anchor. */
|
|
8
18
|
export declare class MobilePendingInput extends Error {
|
|
9
19
|
readonly ids: string[];
|
|
@@ -27,6 +37,8 @@ type MobileOutputRecoveryResult = {
|
|
|
27
37
|
/** Build a private candidate, then join its frozen prefix to an actual Main snapshot. */
|
|
28
38
|
export declare function recoverMobileOutput(args: {
|
|
29
39
|
read: (options: OutputJournalOptions) => Promise<Reply>;
|
|
40
|
+
/** Supplied only after an implementing host explicitly advertises the capability. */
|
|
41
|
+
readBase?: (options: MobileLegacyBaseOptions) => Promise<BaseReply>;
|
|
30
42
|
canContinue: () => boolean;
|
|
31
43
|
latestCursor: () => string | undefined;
|
|
32
44
|
requireJournal?: boolean;
|
|
@@ -1,6 +1,87 @@
|
|
|
1
1
|
import { applyOutputJournalPage, compareOutputCursors, } from "./outputJournalRecovery.js";
|
|
2
2
|
import { assertOutputCoverage } from "./outputCoverage.js";
|
|
3
3
|
import { initialChatState, reduceStream } from "./streamReducer.js";
|
|
4
|
+
const MiB = 1024 * 1024;
|
|
5
|
+
const wireBytes = (value) => new TextEncoder().encode(JSON.stringify(value)).length;
|
|
6
|
+
function record(value) {
|
|
7
|
+
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
8
|
+
}
|
|
9
|
+
function onlyKeys(value, keys) {
|
|
10
|
+
return Object.keys(value).every((key) => keys.includes(key));
|
|
11
|
+
}
|
|
12
|
+
const BASE_EVENT_KEYS = {
|
|
13
|
+
session_started: ["type", "sessionId"],
|
|
14
|
+
stream_request_start: ["type", "agentId"],
|
|
15
|
+
assistant_message: ["type", "agentId"],
|
|
16
|
+
text_delta: ["type", "text", "agentId"],
|
|
17
|
+
thinking_delta: ["type", "text", "agentId"],
|
|
18
|
+
user_message: ["type", "text", "attachments", "clientMessageId"],
|
|
19
|
+
tool_use_start: ["type", "toolCall", "agentId"],
|
|
20
|
+
tool_use_args_delta: ["type", "toolCallId", "args", "agentId"],
|
|
21
|
+
tool_result: ["type", "result", "agentId"],
|
|
22
|
+
agent_start: ["type", "agentId", "name", "description", "status"],
|
|
23
|
+
error: ["type", "error"],
|
|
24
|
+
turn_complete: ["type", "reason"],
|
|
25
|
+
};
|
|
26
|
+
/** Only the display projection's event schema is accepted on this new wire path. */
|
|
27
|
+
function baseEvent(value, sessionId) {
|
|
28
|
+
if (!record(value) ||
|
|
29
|
+
typeof value.type !== "string" ||
|
|
30
|
+
!Object.hasOwn(BASE_EVENT_KEYS, value.type) ||
|
|
31
|
+
!onlyKeys(value, BASE_EVENT_KEYS[value.type]) ||
|
|
32
|
+
(value.agentId !== undefined && typeof value.agentId !== "string"))
|
|
33
|
+
return false;
|
|
34
|
+
switch (value.type) {
|
|
35
|
+
case "session_started":
|
|
36
|
+
return value.sessionId === sessionId;
|
|
37
|
+
case "stream_request_start":
|
|
38
|
+
case "assistant_message":
|
|
39
|
+
return true;
|
|
40
|
+
case "text_delta":
|
|
41
|
+
case "thinking_delta":
|
|
42
|
+
return typeof value.text === "string";
|
|
43
|
+
case "user_message":
|
|
44
|
+
return (typeof value.text === "string" &&
|
|
45
|
+
(value.clientMessageId === undefined || typeof value.clientMessageId === "string") &&
|
|
46
|
+
(value.attachments === undefined ||
|
|
47
|
+
(Array.isArray(value.attachments) &&
|
|
48
|
+
value.attachments.every((item) => record(item) &&
|
|
49
|
+
onlyKeys(item, ["name", "size", "mime", "path"]) &&
|
|
50
|
+
typeof item.name === "string" &&
|
|
51
|
+
Number.isSafeInteger(item.size) &&
|
|
52
|
+
item.size >= 0 &&
|
|
53
|
+
["mime", "path"].every((key) => item[key] === undefined || typeof item[key] === "string")))));
|
|
54
|
+
case "tool_use_start":
|
|
55
|
+
return (record(value.toolCall) &&
|
|
56
|
+
onlyKeys(value.toolCall, ["id", "toolName", "args"]) &&
|
|
57
|
+
typeof value.toolCall.id === "string" &&
|
|
58
|
+
!!value.toolCall.id &&
|
|
59
|
+
typeof value.toolCall.toolName === "string" &&
|
|
60
|
+
record(value.toolCall.args));
|
|
61
|
+
case "tool_use_args_delta":
|
|
62
|
+
return typeof value.toolCallId === "string" && !!value.toolCallId && record(value.args);
|
|
63
|
+
case "tool_result":
|
|
64
|
+
return (record(value.result) &&
|
|
65
|
+
onlyKeys(value.result, ["id", "result", "isError", "error"]) &&
|
|
66
|
+
typeof value.result.id === "string" &&
|
|
67
|
+
!!value.result.id &&
|
|
68
|
+
typeof value.result.result === "string" &&
|
|
69
|
+
typeof value.result.isError === "boolean" &&
|
|
70
|
+
(value.result.error === undefined || typeof value.result.error === "string"));
|
|
71
|
+
case "agent_start":
|
|
72
|
+
return (typeof value.agentId === "string" &&
|
|
73
|
+
!!value.agentId &&
|
|
74
|
+
typeof value.name === "string" &&
|
|
75
|
+
typeof value.description === "string" &&
|
|
76
|
+
value.status === "recorded");
|
|
77
|
+
case "error":
|
|
78
|
+
return typeof value.error === "string";
|
|
79
|
+
case "turn_complete":
|
|
80
|
+
return value.reason === "aborted_tools";
|
|
81
|
+
default:
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
4
85
|
/** Valid identities may arrive before their new Core Run acquires a durable anchor. */
|
|
5
86
|
export class MobilePendingInput extends Error {
|
|
6
87
|
ids;
|
|
@@ -63,21 +144,84 @@ export async function recoverMobileOutput(args) {
|
|
|
63
144
|
}
|
|
64
145
|
if (reply.page.status === "unavailable")
|
|
65
146
|
return null;
|
|
66
|
-
if (reply.page.status !== "ok" ||
|
|
67
|
-
!reply.legacyBaseComplete ||
|
|
68
|
-
!reply.legacyBase ||
|
|
69
|
-
!Array.isArray(reply.legacyBase.events) ||
|
|
70
|
-
reply.legacyBase.events[0]?.type !== "session_started" ||
|
|
71
|
-
reply.legacyBase.throughEventId !== reply.page.legacyBaseThroughEventId ||
|
|
72
|
-
new TextEncoder().encode(JSON.stringify(reply.legacyBase)).length > 1024 * 1024 + 4096)
|
|
73
|
-
throw new Error("Recovery base is incomplete");
|
|
74
147
|
let chat = initialChatState();
|
|
75
|
-
|
|
76
|
-
|
|
148
|
+
let pages = 0;
|
|
149
|
+
if (reply.page.status === "ok" && reply.legacyBaseComplete === false && args.readBase) {
|
|
150
|
+
const throughEventId = reply.page.legacyBaseThroughEventId;
|
|
151
|
+
if (typeof throughEventId !== "string" || !throughEventId || throughEventId.length > 512)
|
|
152
|
+
throw new Error("Recovery base is incomplete");
|
|
153
|
+
const cursors = new Set();
|
|
154
|
+
let cursor, index = 0, bytes = 0;
|
|
155
|
+
for (let count = 0;; count++) {
|
|
156
|
+
if (!args.canContinue() || count >= 40 || ++pages > 2048)
|
|
157
|
+
throw new Error("Recovery base cancelled or bounded");
|
|
158
|
+
const base = await args.readBase(cursor ? { cursor } : { throughEventId });
|
|
159
|
+
if (!args.canContinue())
|
|
160
|
+
throw new Error("Recovery cancelled");
|
|
161
|
+
const size = wireBytes(base);
|
|
162
|
+
bytes += size;
|
|
163
|
+
if (!record(base) ||
|
|
164
|
+
!onlyKeys(base, [
|
|
165
|
+
"type",
|
|
166
|
+
"sessionId",
|
|
167
|
+
"recoveryId",
|
|
168
|
+
"requestId",
|
|
169
|
+
"ok",
|
|
170
|
+
"throughEventId",
|
|
171
|
+
"index",
|
|
172
|
+
"events",
|
|
173
|
+
"complete",
|
|
174
|
+
"next",
|
|
175
|
+
]) ||
|
|
176
|
+
base.type !== "session.legacyBase" ||
|
|
177
|
+
typeof base.requestId !== "string" ||
|
|
178
|
+
!base.requestId ||
|
|
179
|
+
base.requestId.length > 128 ||
|
|
180
|
+
base.ok !== true ||
|
|
181
|
+
base.sessionId !== reply.sessionId ||
|
|
182
|
+
base.recoveryId !== reply.recoveryId ||
|
|
183
|
+
base.throughEventId !== throughEventId ||
|
|
184
|
+
!Number.isSafeInteger(base.index) ||
|
|
185
|
+
base.index !== index ||
|
|
186
|
+
!Array.isArray(base.events) ||
|
|
187
|
+
!base.events.length ||
|
|
188
|
+
!base.events.every((event) => baseEvent(event, reply.sessionId)) ||
|
|
189
|
+
(index === 0 && base.events[0].type !== "session_started") ||
|
|
190
|
+
base.events.some((event, offset) => event.type === "session_started" && index + offset !== 0) ||
|
|
191
|
+
size > MiB + 4096 ||
|
|
192
|
+
bytes > 32 * MiB + 40 * 4096 ||
|
|
193
|
+
typeof base.complete !== "boolean" ||
|
|
194
|
+
(base.complete
|
|
195
|
+
? base.next !== undefined
|
|
196
|
+
: typeof base.next !== "string" ||
|
|
197
|
+
!base.next ||
|
|
198
|
+
base.next.length > 2048 ||
|
|
199
|
+
cursors.has(base.next)))
|
|
200
|
+
throw new Error("Recovery base is incomplete or exceeds its bound");
|
|
201
|
+
for (const event of base.events)
|
|
202
|
+
chat = reduceStream(chat, event);
|
|
203
|
+
index += base.events.length;
|
|
204
|
+
if (base.complete)
|
|
205
|
+
break;
|
|
206
|
+
cursor = base.next;
|
|
207
|
+
cursors.add(cursor);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
else {
|
|
211
|
+
if (reply.page.status !== "ok" ||
|
|
212
|
+
!reply.legacyBaseComplete ||
|
|
213
|
+
!reply.legacyBase ||
|
|
214
|
+
!Array.isArray(reply.legacyBase.events) ||
|
|
215
|
+
reply.legacyBase.events[0]?.type !== "session_started" ||
|
|
216
|
+
reply.legacyBase.throughEventId !== reply.page.legacyBaseThroughEventId ||
|
|
217
|
+
wireBytes(reply.legacyBase) > MiB + 4096)
|
|
218
|
+
throw new Error("Recovery base is incomplete");
|
|
219
|
+
for (const event of reply.legacyBase.events)
|
|
220
|
+
chat = reduceStream(chat, event);
|
|
221
|
+
}
|
|
77
222
|
// A legacy transcript establishes display history, never live completion.
|
|
78
223
|
chat = { ...chat, run: chat.run === "error" ? "error" : "idle", liveByAgent: {} };
|
|
79
224
|
let recovery = { incomplete: false };
|
|
80
|
-
let pages = 0;
|
|
81
225
|
// A continuation retains the verified private candidate and the same total
|
|
82
226
|
// page budget. No prefix is published until the eventual frozen join passes.
|
|
83
227
|
const advance = async () => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cjhyy/code-shell-web",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.47",
|
|
4
4
|
"description": "CodeShell web client logic layer — remote-app state (chatReducer + server-event fold), reconnecting WebSocket hook, pairing/device-credential/risk helpers, and the mobile i18n namespace. Browser-only (no Electron, no Node); seed of the standalone browser client.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -40,8 +40,8 @@
|
|
|
40
40
|
"test:host-title-browser": "node ../../scripts/run-isolated-node-smoke.mjs ../../scripts/smoke-web-host-operations.mjs --late-title"
|
|
41
41
|
},
|
|
42
42
|
"dependencies": {
|
|
43
|
-
"@cjhyy/code-shell-core": "0.9.
|
|
44
|
-
"@cjhyy/code-shell-link": "0.9.
|
|
43
|
+
"@cjhyy/code-shell-core": "0.9.47",
|
|
44
|
+
"@cjhyy/code-shell-link": "0.9.47",
|
|
45
45
|
"@types/react": "^19.2.14",
|
|
46
46
|
"qrcode": "1.5.4"
|
|
47
47
|
},
|