@love-moon/ai-sdk 0.17.1 → 0.18.0
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
CHANGED
|
@@ -1,5 +1,99 @@
|
|
|
1
1
|
# @love-moon/ai-sdk
|
|
2
2
|
|
|
3
|
+
## 0.18.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 72324fd: Add a local multi-chat runtime with isolated Chromium workers, persistent profiles,
|
|
8
|
+
per-account concurrency, resumable events, request deduplication, and SDK/CLI access.
|
|
9
|
+
Add experimental Kimi and Claude webpage adapters and optional AI SDK socket transport.
|
|
10
|
+
|
|
11
|
+
Support Kimi Connect JSON envelopes and DeepSeek XHR SSE patches, with verified
|
|
12
|
+
parallel real-account text chats, independent cancellation, and restart recovery.
|
|
13
|
+
|
|
14
|
+
Add DeepSeek --deepthink / --no-deepthink controls and SDK options with persisted
|
|
15
|
+
chat defaults, per-turn overrides, verified UI state, and runtime capability checks.
|
|
16
|
+
|
|
17
|
+
Connect Conductor web-kimi and web-deepseek to managed isolated Chromium workers
|
|
18
|
+
by default. Add conductor chat-web login, account credential bootstrap, automatic
|
|
19
|
+
daemon startup and idle shutdown, stable cross-process resume IDs, and browser
|
|
20
|
+
release when tasks close. Preserve the shared-profile queue for other providers.
|
|
21
|
+
Retain cancelled conversation URLs and report confirmed stops as interruptions.
|
|
22
|
+
|
|
23
|
+
### Patch Changes
|
|
24
|
+
|
|
25
|
+
- bdbda18: Preserve lightweight conversation identity archives across profile/history GC so
|
|
26
|
+
Conductor can resume the same provider conversation using its existing local ID.
|
|
27
|
+
Archive idle, workerless open chats; persist the completed-turn resume guard; and
|
|
28
|
+
distinguish slow page loading from confirmed authentication failures.
|
|
29
|
+
|
|
30
|
+
Upgrading a CLI while an older managed runtime is running can report
|
|
31
|
+
RUNTIME_UPGRADE_REQUIRED when queuedStartup or durableResume support is missing.
|
|
32
|
+
After active chats finish, run `conductor chat-web runtime stop` and retry;
|
|
33
|
+
the next managed session starts the updated runtime. Alternatively, a runtime
|
|
34
|
+
with no active turns or pinned login windows exits after ten minutes without
|
|
35
|
+
requests. The CLI never terminates running chats automatically.
|
|
36
|
+
|
|
37
|
+
Profile deletion now starts only after the retention checkpoint commits, and
|
|
38
|
+
closed chats awaiting uncertainty review retain their profiles and turn history.
|
|
39
|
+
Accepted prompts refresh idle activity before collection, keeping their parent
|
|
40
|
+
conversation resident even when a client submits directly after a long idle.
|
|
41
|
+
|
|
42
|
+
- 063b97c: Show daemon-host inspection and acknowledgement commands on uncertain chat-web
|
|
43
|
+
errors and REVIEW_REQUIRED, including interrupted sends and timeouts. Keep
|
|
44
|
+
acknowledgement explicit: it does not replay a request, and the user sends the
|
|
45
|
+
next message after checking the provider conversation. The web error message
|
|
46
|
+
contains the commands; acknowledgement currently runs through the host CLI.
|
|
47
|
+
|
|
48
|
+
Compact streamed event history into full text snapshots instead of failing
|
|
49
|
+
after 2000 events. Sequence numbers remain monotonic across compaction and
|
|
50
|
+
restart; replay cursors may cross gaps and recover through a message_snapshot.
|
|
51
|
+
The one-million-character text limit remains enforced.
|
|
52
|
+
|
|
53
|
+
Return CHAT*CLOSED when closing a chat races a headed resume. Strip CONDUCTOR*\*
|
|
54
|
+
identity variables from detached runtime and browser-worker environments while
|
|
55
|
+
preserving browser and proxy configuration.
|
|
56
|
+
|
|
57
|
+
Upgrade note: an already-running shared runtime continues executing its old
|
|
58
|
+
code. After active chats finish, run `conductor chat-web runtime stop` and retry
|
|
59
|
+
to launch the updated runtime. Older runtimes lacking required capabilities
|
|
60
|
+
report RUNTIME_UPGRADE_REQUIRED; unused managed runtimes exit after ten minutes
|
|
61
|
+
without requests or pinned login windows. No running chat is stopped
|
|
62
|
+
automatically during upgrade.
|
|
63
|
+
|
|
64
|
+
- 31b61ea: Bound closed-chat and terminal-turn retention, reclaim browser profiles and orphan
|
|
65
|
+
directories, and keep streaming deltas in memory between durable checkpoints.
|
|
66
|
+
Defer saturated or busy chat startup to the turn queue, validate authentication at
|
|
67
|
+
allocation, and expire queued prompts before crash recovery or delayed submission.
|
|
68
|
+
Keep Gemini on its legacy transport with global runtime sockets and consume the
|
|
69
|
+
Conductor wrapper separator before forwarding a packaged CLI command.
|
|
70
|
+
|
|
71
|
+
## 0.17.2
|
|
72
|
+
|
|
73
|
+
### Patch Changes
|
|
74
|
+
|
|
75
|
+
- b124565: Add QueuedChatSession with a shared local browser broker and a FIFO turn queue
|
|
76
|
+
across processes. Each task retains its own conversation tab; idle tasks no
|
|
77
|
+
longer hold exclusive access to the provider profile. The AI SDK uses queued
|
|
78
|
+
sessions when available and reports waiting positions through working status.
|
|
79
|
+
|
|
80
|
+
Cancel pending turns on disconnect, stop/close failed conversation tabs before
|
|
81
|
+
advancing the queue, and release the browser after its last client exits.
|
|
82
|
+
Keep the exclusive ChatSession API available for direct Playwright access.
|
|
83
|
+
|
|
84
|
+
Keep logical sessions usable after an active stop, timeout, or provider failure.
|
|
85
|
+
Restore the original provider conversation for the next explicit user operation;
|
|
86
|
+
require explicit clear when safe restoration is unavailable, and never replay a
|
|
87
|
+
failed prompt. Allow clear after a failed first submission. Correct the Windows
|
|
88
|
+
named-pipe address escaping.
|
|
89
|
+
|
|
90
|
+
- 0a9488e: Add Kimi website text conversations with persistent login, multi-turn context,
|
|
91
|
+
reply progress, new chats and browser diagnostics. Expose the provider through
|
|
92
|
+
ai-sdk's chat-web backend and the web-kimi Conductor configuration alias.
|
|
93
|
+
|
|
94
|
+
Explicit unknown chat-web providers now fail with a clear error instead of
|
|
95
|
+
silently opening ChatGPT. The existing kimi CLI backend is unchanged.
|
|
96
|
+
|
|
3
97
|
## 0.17.1
|
|
4
98
|
|
|
5
99
|
### Patch Changes
|
|
@@ -1,34 +1,35 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* AI SDK provider that delegates to the chat-web runtime
|
|
3
3
|
* (`@love-moon/chat-web`), which automates a real Chromium browser against
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* chatbot web UIs. Kimi and DeepSeek use isolated persistent chat workers
|
|
5
|
+
* with an automatically started local daemon. ChatGPT/Gemini retain the
|
|
6
|
+
* shared-profile queue unless a runtime socket is explicitly configured.
|
|
7
|
+
* Choose the underlying chat-web provider via:
|
|
6
8
|
*
|
|
7
|
-
* - `options.chatWebProvider`: "chatgpt" | "gemini"
|
|
9
|
+
* - `options.chatWebProvider`: "chatgpt" | "gemini" | "kimi" | "deepseek"
|
|
8
10
|
* - `options.provider`: same surface
|
|
9
11
|
* - `options.model`: same surface (fallback for ergonomic
|
|
10
12
|
* `createAiSession("chat-web", { model: "gemini" })`)
|
|
11
13
|
*
|
|
12
14
|
* Defaults to "chatgpt". Aliases: "openai"/"gpt" → chatgpt, "google" → gemini.
|
|
13
15
|
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
* - `close()` tears the Chromium context down.
|
|
20
|
-
*
|
|
21
|
-
* Resume: chat-web's "session" is a Chromium browser context, not a
|
|
22
|
-
* conversation ID — there is no native cross-process resume. We synthesise
|
|
23
|
-
* an id so the rest of ai-sdk has something stable to thread on, but
|
|
24
|
-
* passing `resumeSessionId` does not reattach to a prior conversation.
|
|
16
|
+
* Login on the daemon host: conductor chat-web login kimi|deepseek.
|
|
17
|
+
* Managed runtime IDs use chat-web-runtime:<id> so restarts restore the same
|
|
18
|
+
* isolated profile and provider conversation. Legacy provider IDs are imported.
|
|
19
|
+
* Set chatWebRuntime: false to explicitly use the shared-profile queue.
|
|
20
|
+
* Explicit chatWebRuntimeSocket clients keep caller-managed lifecycle.
|
|
25
21
|
*/
|
|
26
22
|
export class ChatWebSession extends EventEmitter<[never]> {
|
|
27
23
|
constructor(backend: any, options?: {});
|
|
28
24
|
backend: string;
|
|
29
25
|
options: {};
|
|
30
26
|
logger: any;
|
|
27
|
+
runtimeSocket: any;
|
|
31
28
|
chatWebProvider: string;
|
|
29
|
+
runtimeMode: boolean;
|
|
30
|
+
managedRuntime: boolean;
|
|
31
|
+
runtimeConversationId: any;
|
|
32
|
+
runtimeProviderConversationId: any;
|
|
32
33
|
headless: boolean;
|
|
33
34
|
browserChannel: any;
|
|
34
35
|
browserExecutablePath: any;
|
|
@@ -50,6 +51,7 @@ export class ChatWebSession extends EventEmitter<[never]> {
|
|
|
50
51
|
closed: boolean;
|
|
51
52
|
currentTurn: {
|
|
52
53
|
aborted: boolean;
|
|
54
|
+
controller: AbortController;
|
|
53
55
|
} | null;
|
|
54
56
|
currentTurnStatus: {
|
|
55
57
|
source: string;
|
|
@@ -65,6 +67,7 @@ export class ChatWebSession extends EventEmitter<[never]> {
|
|
|
65
67
|
session_file_path: undefined;
|
|
66
68
|
updated_at: string;
|
|
67
69
|
} | null;
|
|
70
|
+
turnAttempted: boolean;
|
|
68
71
|
sessionMessageHandler: any;
|
|
69
72
|
workingStatusHandler: any;
|
|
70
73
|
activeReplyTarget: string;
|
|
@@ -161,6 +164,8 @@ export class ChatWebSession extends EventEmitter<[never]> {
|
|
|
161
164
|
manualResume: null;
|
|
162
165
|
}>;
|
|
163
166
|
getChatWebModule(): Promise<any>;
|
|
167
|
+
loginCommand(): string;
|
|
168
|
+
reviewInstructions(): string;
|
|
164
169
|
boot(): Promise<void>;
|
|
165
170
|
bootInternal(): Promise<void>;
|
|
166
171
|
/**
|
|
@@ -4,7 +4,8 @@ import { PROVIDER_MEDIA_CAPABILITIES } from "../media-adapters.js";
|
|
|
4
4
|
import { assertMediaCapabilities, resolveTurnMedia } from "../media-input.js";
|
|
5
5
|
import { normalizeContextFiles } from "../context-files.js";
|
|
6
6
|
import { emitLog, normalizeLogger } from "../shared.js";
|
|
7
|
-
const SUPPORTED_CHAT_WEB_PROVIDERS = new Set(["chatgpt", "gemini"]);
|
|
7
|
+
const SUPPORTED_CHAT_WEB_PROVIDERS = new Set(["chatgpt", "gemini", "kimi", "deepseek"]);
|
|
8
|
+
const RUNTIME_CHAT_WEB_PROVIDERS = new Set(["chatgpt", "deepseek", "kimi", "claude"]);
|
|
8
9
|
const DEFAULT_CHAT_WEB_PROVIDER = "chatgpt";
|
|
9
10
|
const DEFAULT_TURN_TIMEOUT_MS = 5 * 60 * 1000;
|
|
10
11
|
function normalizeChatWebProvider(value) {
|
|
@@ -19,15 +20,25 @@ function normalizeChatWebProvider(value) {
|
|
|
19
20
|
return raw;
|
|
20
21
|
}
|
|
21
22
|
function resolveChatWebProvider(options = {}) {
|
|
22
|
-
const
|
|
23
|
-
|
|
23
|
+
const supported = options.chatWebRuntimeSocket || process.env.CHAT_WEB_RUNTIME_SOCKET
|
|
24
|
+
? new Set([...SUPPORTED_CHAT_WEB_PROVIDERS, ...RUNTIME_CHAT_WEB_PROVIDERS]) : SUPPORTED_CHAT_WEB_PROVIDERS;
|
|
25
|
+
for (const candidate of [options.chatWebProvider, options.provider, options.model]) {
|
|
24
26
|
const normalized = normalizeChatWebProvider(candidate);
|
|
25
|
-
if (
|
|
26
|
-
|
|
27
|
+
if (!normalized)
|
|
28
|
+
continue;
|
|
29
|
+
if (!supported.has(normalized)) {
|
|
30
|
+
const error = new Error(`Unknown chat-web provider "${normalized}". Supported: ${[...supported].join(", ")}`);
|
|
31
|
+
error.reason = "unknown_chat_web_provider";
|
|
32
|
+
throw error;
|
|
27
33
|
}
|
|
34
|
+
return normalized;
|
|
28
35
|
}
|
|
29
36
|
return DEFAULT_CHAT_WEB_PROVIDER;
|
|
30
37
|
}
|
|
38
|
+
function quoteRuntimeArgument(value) {
|
|
39
|
+
const text = String(value);
|
|
40
|
+
return /^[A-Za-z0-9_./:@%+=,-]+$/.test(text) ? text : "'" + text.replaceAll("'", "'\\''") + "'";
|
|
41
|
+
}
|
|
31
42
|
function extractErrorMessage(error) {
|
|
32
43
|
if (typeof error?.message === "string" && error.message.trim()) {
|
|
33
44
|
return error.message.trim();
|
|
@@ -93,27 +104,23 @@ function isPlaywrightMissingError(error) {
|
|
|
93
104
|
/**
|
|
94
105
|
* AI SDK provider that delegates to the chat-web runtime
|
|
95
106
|
* (`@love-moon/chat-web`), which automates a real Chromium browser against
|
|
96
|
-
*
|
|
97
|
-
*
|
|
107
|
+
* chatbot web UIs. Kimi and DeepSeek use isolated persistent chat workers
|
|
108
|
+
* with an automatically started local daemon. ChatGPT/Gemini retain the
|
|
109
|
+
* shared-profile queue unless a runtime socket is explicitly configured.
|
|
110
|
+
* Choose the underlying chat-web provider via:
|
|
98
111
|
*
|
|
99
|
-
* - `options.chatWebProvider`: "chatgpt" | "gemini"
|
|
112
|
+
* - `options.chatWebProvider`: "chatgpt" | "gemini" | "kimi" | "deepseek"
|
|
100
113
|
* - `options.provider`: same surface
|
|
101
114
|
* - `options.model`: same surface (fallback for ergonomic
|
|
102
115
|
* `createAiSession("chat-web", { model: "gemini" })`)
|
|
103
116
|
*
|
|
104
117
|
* Defaults to "chatgpt". Aliases: "openai"/"gpt" → chatgpt, "google" → gemini.
|
|
105
118
|
*
|
|
106
|
-
*
|
|
107
|
-
*
|
|
108
|
-
*
|
|
109
|
-
*
|
|
110
|
-
*
|
|
111
|
-
* - `close()` tears the Chromium context down.
|
|
112
|
-
*
|
|
113
|
-
* Resume: chat-web's "session" is a Chromium browser context, not a
|
|
114
|
-
* conversation ID — there is no native cross-process resume. We synthesise
|
|
115
|
-
* an id so the rest of ai-sdk has something stable to thread on, but
|
|
116
|
-
* passing `resumeSessionId` does not reattach to a prior conversation.
|
|
119
|
+
* Login on the daemon host: conductor chat-web login kimi|deepseek.
|
|
120
|
+
* Managed runtime IDs use chat-web-runtime:<id> so restarts restore the same
|
|
121
|
+
* isolated profile and provider conversation. Legacy provider IDs are imported.
|
|
122
|
+
* Set chatWebRuntime: false to explicitly use the shared-profile queue.
|
|
123
|
+
* Explicit chatWebRuntimeSocket clients keep caller-managed lifecycle.
|
|
117
124
|
*/
|
|
118
125
|
export class ChatWebSession extends EventEmitter {
|
|
119
126
|
constructor(backend, options = {}) {
|
|
@@ -121,17 +128,25 @@ export class ChatWebSession extends EventEmitter {
|
|
|
121
128
|
this.backend = "chat-web";
|
|
122
129
|
this.options = options;
|
|
123
130
|
this.logger = normalizeLogger(options.logger);
|
|
131
|
+
this.runtimeSocket = options.chatWebRuntimeSocket || process.env.CHAT_WEB_RUNTIME_SOCKET || "";
|
|
124
132
|
this.chatWebProvider = resolveChatWebProvider(options);
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
this.
|
|
133
|
+
if (!RUNTIME_CHAT_WEB_PROVIDERS.has(this.chatWebProvider))
|
|
134
|
+
this.runtimeSocket = "";
|
|
135
|
+
this.runtimeMode = Boolean(this.runtimeSocket) ||
|
|
136
|
+
(["kimi", "deepseek"].includes(this.chatWebProvider) && options.chatWebRuntime !== false);
|
|
137
|
+
this.managedRuntime = this.runtimeMode && !this.runtimeSocket;
|
|
138
|
+
const resumed = typeof options.resumeSessionId === "string" ? options.resumeSessionId : "";
|
|
139
|
+
const prefix = "chat-web-runtime:";
|
|
140
|
+
this.runtimeConversationId = options.chatWebConversationId ||
|
|
141
|
+
(this.managedRuntime ? (resumed.startsWith(prefix) ? resumed.slice(prefix.length) : undefined) : this.runtimeMode ? resumed : undefined);
|
|
142
|
+
this.runtimeProviderConversationId = this.managedRuntime && resumed && !resumed.startsWith(prefix) && !options.chatWebConversationId ? resumed : undefined;
|
|
143
|
+
if (options.chatWebDeepThink !== undefined &&
|
|
144
|
+
(typeof options.chatWebDeepThink !== "boolean" || this.chatWebProvider !== "deepseek" || !this.runtimeMode)) {
|
|
145
|
+
throw new Error("chatWebDeepThink requires a boolean and the DeepSeek chat runtime");
|
|
146
|
+
}
|
|
147
|
+
// Kimi requires headed Chromium with the tested login flow. DeepSeek's
|
|
148
|
+
// managed workers support headless; explicit options override either default.
|
|
149
|
+
this.headless = this.runtimeMode && this.chatWebProvider === "deepseek" ? options.headless !== false : options.headless === true;
|
|
135
150
|
// Optional: use a specific Chromium-family binary (system Chrome /
|
|
136
151
|
// Edge / explicit path) instead of Playwright's bundled
|
|
137
152
|
// `chrome-headless-shell`. Useful when the user's network treats
|
|
@@ -176,6 +191,7 @@ export class ChatWebSession extends EventEmitter {
|
|
|
176
191
|
this.closed = false;
|
|
177
192
|
this.currentTurn = null;
|
|
178
193
|
this.currentTurnStatus = null;
|
|
194
|
+
this.turnAttempted = false;
|
|
179
195
|
this.sessionMessageHandler = null;
|
|
180
196
|
this.workingStatusHandler = null;
|
|
181
197
|
this.activeReplyTarget = "";
|
|
@@ -207,12 +223,12 @@ export class ChatWebSession extends EventEmitter {
|
|
|
207
223
|
sessionId: this.sessionId,
|
|
208
224
|
sessionInfo: this.getSessionInfo(),
|
|
209
225
|
useSessionFileReplyStream: this.usesSessionFileReplyStream(),
|
|
210
|
-
resumeReady: Boolean(this.providerConversationId),
|
|
226
|
+
resumeReady: Boolean(this.runtimeMode ? this.runtimeConversationId : this.providerConversationId),
|
|
211
227
|
manualResume: this.providerConversationUrl()
|
|
212
228
|
? { ready: true, command: this.providerConversationUrl() }
|
|
213
229
|
: null,
|
|
214
230
|
currentTurnStatus: this.getCurrentTurnStatus(),
|
|
215
|
-
capabilities: { clear:
|
|
231
|
+
capabilities: { clear: !this.runtimeMode, media: PROVIDER_MEDIA_CAPABILITIES[CHAT_WEB_SESSION_VARIANT] },
|
|
216
232
|
chatWebProvider: this.chatWebProvider,
|
|
217
233
|
providerConversationId: this.providerConversationId,
|
|
218
234
|
providerUrl: this.providerConversationUrl(),
|
|
@@ -229,7 +245,7 @@ export class ChatWebSession extends EventEmitter {
|
|
|
229
245
|
//
|
|
230
246
|
// We expose `sessionIdDeferred: true` so the fire-side announce can
|
|
231
247
|
// hold off until the real id arrives, then re-announce.
|
|
232
|
-
const hasRealId = Boolean(this.providerConversationId);
|
|
248
|
+
const hasRealId = Boolean(this.runtimeMode ? this.runtimeConversationId : this.providerConversationId);
|
|
233
249
|
return {
|
|
234
250
|
...this.sessionInfo,
|
|
235
251
|
sessionIdDeferred: !hasRealId,
|
|
@@ -294,6 +310,21 @@ export class ChatWebSession extends EventEmitter {
|
|
|
294
310
|
throw enriched;
|
|
295
311
|
}
|
|
296
312
|
}
|
|
313
|
+
loginCommand() {
|
|
314
|
+
if (!this.runtimeMode)
|
|
315
|
+
return `chat-web login ${this.chatWebProvider}`;
|
|
316
|
+
if (this.managedRuntime && (!this.options.chatWebAccountId || this.options.chatWebAccountId === "default")) {
|
|
317
|
+
return `conductor chat-web login ${this.chatWebProvider}`;
|
|
318
|
+
}
|
|
319
|
+
return `conductor chat-web chat login ${this.runtimeConversationId} --socket ${this.runtimeSocket}`;
|
|
320
|
+
}
|
|
321
|
+
reviewInstructions() {
|
|
322
|
+
if (!this.runtimeConversationId || !this.runtimeSocket)
|
|
323
|
+
return "";
|
|
324
|
+
const id = quoteRuntimeArgument(this.runtimeConversationId);
|
|
325
|
+
const socket = quoteRuntimeArgument(this.runtimeSocket);
|
|
326
|
+
return ` The previous webpage request has an uncertain outcome. On the daemon host, inspect the same conversation with: conductor chat-web chat login ${id} --socket ${socket}. After checking its result, acknowledge with: conductor chat-web chat resume ${id} --acknowledge --socket ${socket}. Then resend only the message you still want to send; acknowledgement does not replay the previous request.`;
|
|
327
|
+
}
|
|
297
328
|
async boot() {
|
|
298
329
|
if (this.booted)
|
|
299
330
|
return;
|
|
@@ -304,6 +335,13 @@ export class ChatWebSession extends EventEmitter {
|
|
|
304
335
|
await this.bootPromise;
|
|
305
336
|
this.booted = true;
|
|
306
337
|
}
|
|
338
|
+
catch (error) {
|
|
339
|
+
// A failed login must release this task's browser so a fresh account
|
|
340
|
+
// snapshot can be imported when the user retries the same task.
|
|
341
|
+
await this.chatSession?.close().catch(() => undefined);
|
|
342
|
+
this.chatSession = null;
|
|
343
|
+
throw error;
|
|
344
|
+
}
|
|
307
345
|
finally {
|
|
308
346
|
this.bootPromise = null;
|
|
309
347
|
}
|
|
@@ -316,8 +354,9 @@ export class ChatWebSession extends EventEmitter {
|
|
|
316
354
|
if (typeof mod.registerBuiltinProviders === "function") {
|
|
317
355
|
mod.registerBuiltinProviders();
|
|
318
356
|
}
|
|
319
|
-
|
|
320
|
-
|
|
357
|
+
const Session = this.runtimeMode ? mod.RuntimeChatSession : (mod.QueuedChatSession ?? mod.ChatSession);
|
|
358
|
+
if (typeof Session?.open !== "function") {
|
|
359
|
+
throw new Error(`Loaded @love-moon/chat-web is missing ${this.runtimeMode ? "RuntimeChatSession" : "ChatSession"}.open; upgrade chat-web`);
|
|
321
360
|
}
|
|
322
361
|
// Forward optional browser-binary overrides to chat-web. The env vars
|
|
323
362
|
// CHAT_WEB_BROWSER_CHANNEL / CHAT_WEB_BROWSER_EXECUTABLE are also
|
|
@@ -328,7 +367,8 @@ export class ChatWebSession extends EventEmitter {
|
|
|
328
367
|
launch.channel = this.browserChannel;
|
|
329
368
|
if (this.browserExecutablePath)
|
|
330
369
|
launch.executablePath = this.browserExecutablePath;
|
|
331
|
-
this.chatSession = await
|
|
370
|
+
this.chatSession = await Session.open(this.chatWebProvider, {
|
|
371
|
+
...(this.runtimeMode ? { socketPath: this.managedRuntime ? undefined : this.runtimeSocket || undefined, conversationId: this.runtimeConversationId, providerConversationId: this.runtimeProviderConversationId, accountId: this.options.chatWebAccountId, deepThink: this.options.chatWebDeepThink, releaseOnClose: this.managedRuntime } : {}),
|
|
332
372
|
headless: this.headless,
|
|
333
373
|
// IMPORTANT: chat-web's logger contract is `{ error, warn, info, debug }`;
|
|
334
374
|
// ai-sdk's normalised logger is `{ log }`. Passing the ai-sdk logger
|
|
@@ -342,9 +382,23 @@ export class ChatWebSession extends EventEmitter {
|
|
|
342
382
|
this.chatSession = null;
|
|
343
383
|
throw this.createSessionClosedError();
|
|
344
384
|
}
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
385
|
+
if (this.runtimeMode) {
|
|
386
|
+
this.runtimeConversationId = this.chatSession.id;
|
|
387
|
+
this.runtimeSocket = this.chatSession.client?.socketPath || this.runtimeSocket;
|
|
388
|
+
this.sessionId = this.managedRuntime ? "chat-web-runtime:" + this.runtimeConversationId : this.runtimeConversationId;
|
|
389
|
+
if (this.chatSession.providerConversationId)
|
|
390
|
+
this.providerConversationId = this.chatSession.providerConversationId;
|
|
391
|
+
this.sessionInfo = { ...this.sessionInfo, sessionId: this.sessionId };
|
|
392
|
+
this.emit("session", this.getSessionInfo());
|
|
393
|
+
}
|
|
394
|
+
const readiness = this.runtimeMode && typeof this.chatSession.prepare === "function"
|
|
395
|
+
? await this.chatSession.prepare()
|
|
396
|
+
: { ready: this.runtimeMode ? await this.chatSession.isLoggedIn() : await this.chatSession.isLoggedIn().catch(() => false), authRequired: true };
|
|
397
|
+
if (!readiness.ready && !readiness.queued) {
|
|
398
|
+
if (readiness.authRequired !== true) {
|
|
399
|
+
throw Object.assign(new Error("Chat page did not become ready; wait for it to finish loading and retry"), { code: "PAGE_NOT_READY" });
|
|
400
|
+
}
|
|
401
|
+
const error = new Error(`Chat requires login. On this daemon host run: ${this.loginCommand()}`);
|
|
348
402
|
error.reason = "not_logged_in";
|
|
349
403
|
this.emit("auth_required", {
|
|
350
404
|
reason: "login_required",
|
|
@@ -366,6 +420,11 @@ export class ChatWebSession extends EventEmitter {
|
|
|
366
420
|
* for exactly this window.
|
|
367
421
|
*/
|
|
368
422
|
async runClear() {
|
|
423
|
+
if (this.runtimeMode) {
|
|
424
|
+
const error = new Error("Create a new runtime conversation to clear an isolated chat");
|
|
425
|
+
error.reason = "unsupported_clear";
|
|
426
|
+
throw error;
|
|
427
|
+
}
|
|
369
428
|
if (this.closeRequested) {
|
|
370
429
|
throw this.createSessionClosedError();
|
|
371
430
|
}
|
|
@@ -376,7 +435,7 @@ export class ChatWebSession extends EventEmitter {
|
|
|
376
435
|
}
|
|
377
436
|
// Nothing has been said yet — fire boots the browser at announce time, so
|
|
378
437
|
// "booted" alone does not mean there is a conversation to walk away from.
|
|
379
|
-
if (!this.providerConversationId && this.history.length === 0) {
|
|
438
|
+
if (!this.providerConversationId && this.history.length === 0 && !this.turnAttempted) {
|
|
380
439
|
return { clear: { status: "noop" }, usage: null, metadata: {} };
|
|
381
440
|
}
|
|
382
441
|
await this.boot();
|
|
@@ -394,6 +453,7 @@ export class ChatWebSession extends EventEmitter {
|
|
|
394
453
|
providerUrl: undefined,
|
|
395
454
|
};
|
|
396
455
|
this.history = [];
|
|
456
|
+
this.turnAttempted = false;
|
|
397
457
|
this.emit("session", this.getSessionInfo());
|
|
398
458
|
// No id to report: the provider mints one on the next reply.
|
|
399
459
|
return { clear: { status: "cleared" }, usage: null, metadata: {} };
|
|
@@ -415,11 +475,12 @@ export class ChatWebSession extends EventEmitter {
|
|
|
415
475
|
if (normalized === this.providerConversationId)
|
|
416
476
|
return;
|
|
417
477
|
this.providerConversationId = normalized;
|
|
418
|
-
this.
|
|
478
|
+
if (!this.runtimeMode)
|
|
479
|
+
this.sessionId = normalized;
|
|
419
480
|
this.sessionInfo = {
|
|
420
481
|
...(this.sessionInfo || {}),
|
|
421
482
|
backend: this.backend,
|
|
422
|
-
sessionId:
|
|
483
|
+
sessionId: this.sessionId,
|
|
423
484
|
model: this.chatWebProvider,
|
|
424
485
|
modelProvider: "chat-web",
|
|
425
486
|
providerConversationId: normalized,
|
|
@@ -438,6 +499,12 @@ export class ChatWebSession extends EventEmitter {
|
|
|
438
499
|
switch (this.chatWebProvider) {
|
|
439
500
|
case "chatgpt":
|
|
440
501
|
return `https://chatgpt.com/c/${this.providerConversationId}`;
|
|
502
|
+
case "deepseek":
|
|
503
|
+
return `https://chat.deepseek.com/a/chat/s/${this.providerConversationId}`;
|
|
504
|
+
case "kimi":
|
|
505
|
+
return `https://www.kimi.com/chat/${encodeURIComponent(this.providerConversationId)}`;
|
|
506
|
+
case "claude":
|
|
507
|
+
return `https://claude.ai/chat/${this.providerConversationId}`;
|
|
441
508
|
case "gemini":
|
|
442
509
|
// chat-web's `gemini` provider targets AI Studio
|
|
443
510
|
// (aistudio.google.com/prompts/new_chat) — that's the free web
|
|
@@ -476,7 +543,7 @@ export class ChatWebSession extends EventEmitter {
|
|
|
476
543
|
error.reason = "turn_already_running";
|
|
477
544
|
throw error;
|
|
478
545
|
}
|
|
479
|
-
this.currentTurn = { aborted: false };
|
|
546
|
+
this.currentTurn = { aborted: false, controller: new AbortController() };
|
|
480
547
|
await this.emitWorkingStatus({
|
|
481
548
|
phase: "turn_started",
|
|
482
549
|
reply_in_progress: true,
|
|
@@ -486,8 +553,29 @@ export class ChatWebSession extends EventEmitter {
|
|
|
486
553
|
await this.boot();
|
|
487
554
|
if (this.closeRequested)
|
|
488
555
|
throw this.createSessionClosedError();
|
|
556
|
+
if (this.currentTurn.aborted)
|
|
557
|
+
throw new DOMException("Aborted", "AbortError");
|
|
558
|
+
this.turnAttempted = true;
|
|
489
559
|
const result = await this.chatSession.send(prompt, {
|
|
490
560
|
timeoutMs: this.turnTimeoutMs,
|
|
561
|
+
signal: this.currentTurn.controller.signal,
|
|
562
|
+
onStatus: (phase) => {
|
|
563
|
+
if (phase === "queued" || phase === "starting")
|
|
564
|
+
void this.emitWorkingStatus({
|
|
565
|
+
phase: phase === "queued" ? "queued" : "turn_started",
|
|
566
|
+
reply_in_progress: true,
|
|
567
|
+
status_line: phase === "queued" ? `chat-web (${this.chatWebProvider}) waiting for a browser worker` : `chat-web (${this.chatWebProvider}) is working`,
|
|
568
|
+
}, onProgress);
|
|
569
|
+
},
|
|
570
|
+
onQueue: (position) => {
|
|
571
|
+
void this.emitWorkingStatus({
|
|
572
|
+
phase: position > 0 ? "queued" : "turn_started",
|
|
573
|
+
reply_in_progress: true,
|
|
574
|
+
status_line: position > 0
|
|
575
|
+
? `chat-web (${this.chatWebProvider}) queued: ${position} ahead`
|
|
576
|
+
: `chat-web (${this.chatWebProvider}) is working`,
|
|
577
|
+
}, onProgress);
|
|
578
|
+
},
|
|
491
579
|
onProgress: (text) => {
|
|
492
580
|
// chat-web's onProgress fires while streaming text grows; we
|
|
493
581
|
// forward those as working_status updates with a short preview.
|
|
@@ -543,6 +631,17 @@ export class ChatWebSession extends EventEmitter {
|
|
|
543
631
|
catch (error) {
|
|
544
632
|
const message = extractErrorMessage(error);
|
|
545
633
|
const code = typeof error?.code === "string" ? error.code : "";
|
|
634
|
+
if (this.runtimeMode && (code === "AUTH_REQUIRED" || /Provider HTTP 401/.test(message))) {
|
|
635
|
+
const login = this.loginCommand();
|
|
636
|
+
error.message = `Browser login needs renewal. On this daemon host run: ${login}.`;
|
|
637
|
+
error.reason = "not_logged_in";
|
|
638
|
+
}
|
|
639
|
+
if (this.runtimeMode && (code === "REVIEW_REQUIRED" || error?.turnStatus === "uncertain")) {
|
|
640
|
+
error.message = (error.message || message) + this.reviewInstructions();
|
|
641
|
+
}
|
|
642
|
+
if (this.runtimeMode && (code === "AUTH_REQUIRED" || /Provider HTTP 401/.test(message))) {
|
|
643
|
+
this.emit("auth_required", { reason: "login_required", message: error.message, provider: this.chatWebProvider });
|
|
644
|
+
}
|
|
546
645
|
// Surface chat-web's typed "needs API key" / "permission denied"
|
|
547
646
|
// errors as auth_required so the UI / daemon can route them
|
|
548
647
|
// through the same flow as ChatGPT's "not logged in" — they're
|
|
@@ -557,7 +656,7 @@ export class ChatWebSession extends EventEmitter {
|
|
|
557
656
|
}
|
|
558
657
|
await this.emitTerminalWorkingStatus({
|
|
559
658
|
phase: this.currentTurn?.aborted ? "turn_interrupted" : "turn_failed",
|
|
560
|
-
status_done_line: message,
|
|
659
|
+
status_done_line: error.message || message,
|
|
561
660
|
}, onProgress);
|
|
562
661
|
throw error;
|
|
563
662
|
}
|
|
@@ -567,13 +666,18 @@ export class ChatWebSession extends EventEmitter {
|
|
|
567
666
|
}
|
|
568
667
|
}
|
|
569
668
|
async interruptCurrentTurn() {
|
|
570
|
-
// chat-web
|
|
669
|
+
// Direct chat-web ChatSession doesn't expose a turn abort — the underlying
|
|
571
670
|
// Chromium tab is still busy with the model. We mark the turn as
|
|
572
671
|
// aborted so the next status emission is "interrupted", but the
|
|
573
672
|
// assistant_message may still arrive once the model finishes.
|
|
574
673
|
if (this.currentTurn) {
|
|
575
674
|
this.currentTurn.aborted = true;
|
|
675
|
+
// The shared broker removes a pending turn, or stops/closes the active
|
|
676
|
+
// task's tab before moving to the next task. No queued prompt is sent.
|
|
677
|
+
this.currentTurn.controller.abort();
|
|
576
678
|
}
|
|
679
|
+
if (this.runtimeMode)
|
|
680
|
+
await this.chatSession?.interruptCurrentTurn?.();
|
|
577
681
|
}
|
|
578
682
|
async close() {
|
|
579
683
|
if (this.closed)
|
|
@@ -1,9 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
* contract by surfacing a synthetic session id and a "best effort" context,
|
|
6
|
-
* but downstream callers should treat resume here as a no-op.
|
|
2
|
+
* Managed Kimi/DeepSeek sessions use a stable chat-web-runtime:<id> token.
|
|
3
|
+
* RuntimeChatSession restores the saved isolated profile and provider URL.
|
|
4
|
+
* Other chat-web modes retain their existing provider-specific resume behavior.
|
|
7
5
|
*/
|
|
8
6
|
export function buildCliArgs(sessionId: any): string[];
|
|
9
7
|
export function findSessionPath(): Promise<null>;
|
package/dist/resume/chat-web.js
CHANGED
|
@@ -1,11 +1,9 @@
|
|
|
1
1
|
import { buildResumeContext, normalizeSessionId } from "./shared.js";
|
|
2
2
|
export const BACKEND = "chat-web";
|
|
3
3
|
/**
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* contract by surfacing a synthetic session id and a "best effort" context,
|
|
8
|
-
* but downstream callers should treat resume here as a no-op.
|
|
4
|
+
* Managed Kimi/DeepSeek sessions use a stable chat-web-runtime:<id> token.
|
|
5
|
+
* RuntimeChatSession restores the saved isolated profile and provider URL.
|
|
6
|
+
* Other chat-web modes retain their existing provider-specific resume behavior.
|
|
9
7
|
*/
|
|
10
8
|
export function buildCliArgs(sessionId) {
|
|
11
9
|
// chat-web has no CLI to pass these args to (it's an in-process SDK), but
|
|
@@ -38,7 +36,9 @@ export async function resolveResumeContext(sessionId, options = {}) {
|
|
|
38
36
|
cwd,
|
|
39
37
|
cwdSource: "process_cwd",
|
|
40
38
|
extraDebug: {
|
|
41
|
-
note: "chat-web
|
|
39
|
+
note: normalizedSessionId.startsWith("chat-web-runtime:")
|
|
40
|
+
? "The isolated chat runtime restores this saved conversation; a missing runtime id is an error."
|
|
41
|
+
: "The selected chat-web transport resolves this provider conversation id when opening.",
|
|
42
42
|
},
|
|
43
43
|
});
|
|
44
44
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@love-moon/ai-sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.18.0",
|
|
4
4
|
"repository": {
|
|
5
5
|
"type": "git",
|
|
6
6
|
"url": "git+https://github.com/lovemoon-ai/conductor.git"
|
|
@@ -53,12 +53,12 @@
|
|
|
53
53
|
"zod": "^4.1.5"
|
|
54
54
|
},
|
|
55
55
|
"optionalDependencies": {
|
|
56
|
-
"@love-moon/chat-web": "^0.
|
|
56
|
+
"@love-moon/chat-web": "^0.18.0"
|
|
57
57
|
},
|
|
58
58
|
"devDependencies": {
|
|
59
59
|
"@types/node": "^22.10.2",
|
|
60
60
|
"tsx": "^4.20.6",
|
|
61
61
|
"typescript": "^5.6.3"
|
|
62
62
|
},
|
|
63
|
-
"gitCommitId": "
|
|
63
|
+
"gitCommitId": "825d90a"
|
|
64
64
|
}
|