fix(deepseek-web): lazy start session refresh (#2742)

Integrated into release/v3.8.4
This commit is contained in:
Thanet S.
2026-05-27 01:09:40 +07:00
committed by GitHub
parent 24624f8802
commit 475393748e
3 changed files with 147 additions and 6 deletions

View File

@@ -1,5 +1,10 @@
import type { ExecuteInput } from "./base.ts";
import { DeepSeekWebExecutor, acquireAccessToken, tokenCache } from "./deepseek-web.ts";
import {
DeepSeekWebExecutor,
acquireAccessToken,
extractUserToken,
tokenCache,
} from "./deepseek-web.ts";
interface AutoRefreshConfig {
sessionRefreshInterval?: number;
@@ -28,15 +33,12 @@ export class DeepSeekWebWithAutoRefreshExecutor extends DeepSeekWebExecutor {
autoRefresh: true,
...config,
};
if (this.refreshConfig.autoRefresh) {
this.startAutoRefresh();
}
}
override async execute(input: ExecuteInput) {
this.retryCount = 0;
const creds = input.credentials as unknown as Record<string, unknown>;
this.currentUserToken = (creds.apiKey as string) || (creds.accessToken as string) || "";
this.setCurrentUserToken(extractUserToken(creds));
return this.executeWithRetry(input);
}
@@ -62,6 +64,10 @@ export class DeepSeekWebWithAutoRefreshExecutor extends DeepSeekWebExecutor {
private startAutoRefresh(): void {
if (this.refreshTimer) clearInterval(this.refreshTimer);
this.refreshTimer = setInterval(async () => {
if (!this.currentUserToken) {
this.sessionValid = false;
return;
}
try {
await this.doRefreshSession();
} catch (error) {
@@ -70,6 +76,20 @@ export class DeepSeekWebWithAutoRefreshExecutor extends DeepSeekWebExecutor {
}, this.refreshConfig.sessionRefreshInterval);
}
private setCurrentUserToken(userToken: string | null): void {
if (!userToken) {
return;
}
if (this.currentUserToken === userToken) {
return;
}
this.currentUserToken = userToken;
this.sessionValid = false;
if (this.refreshConfig.autoRefresh) {
this.startAutoRefresh();
}
}
private async doRefreshSession(): Promise<void> {
if (!this.currentUserToken) {
this.sessionValid = false;

View File

@@ -53,7 +53,7 @@ function evictOldest(cache: Map<string, unknown>): void {
// ── Helpers ──────────────────────────────────────────────────────────────
function extractUserToken(credentials: Record<string, unknown>): string | null {
export function extractUserToken(credentials: Record<string, unknown>): string | null {
const raw = credentials?.apiKey || credentials?.accessToken;
if (typeof raw !== "string" || raw.length === 0) return null;
// Handle JSON-wrapped tokens (DeepSeek stores token as {"value":"..."})

View File

@@ -470,6 +470,127 @@ test("isSessionValid starts false", () => {
assert.equal(exec.isSessionValid(), false);
});
test("auto-refresh stays idle until a userToken is provided", async () => {
const errors = [];
const originalError = console.error;
console.error = (...args) => {
errors.push(args);
};
const exec = new DeepSeekWebWithAutoRefreshExecutor({
autoRefresh: true,
sessionRefreshInterval: 5,
});
try {
await new Promise((resolve) => setTimeout(resolve, 25));
assert.equal(errors.length, 0);
} finally {
exec.destroy();
console.error = originalError;
}
});
test("execute without DeepSeek credentials does not start auto-refresh", async () => {
const errors = [];
const originalError = console.error;
console.error = (...args) => {
errors.push(args);
};
const exec = new DeepSeekWebWithAutoRefreshExecutor({
autoRefresh: true,
sessionRefreshInterval: 5,
});
try {
const result = await exec.execute({
model: "default",
body: { messages: [{ role: "user", content: "hi" }] },
stream: true,
credentials: {},
signal: AbortSignal.timeout(5000),
});
assert.equal(result.response.status, 400);
await new Promise((resolve) => setTimeout(resolve, 25));
assert.equal(errors.length, 0);
} finally {
exec.destroy();
console.error = originalError;
}
});
test("execute without DeepSeek credentials preserves an active auto-refresh session", async () => {
const mock = await mockDeepSeekFlow();
const exec = new DeepSeekWebWithAutoRefreshExecutor({
autoRefresh: true,
sessionRefreshInterval: 60_000,
});
try {
const validResult = await exec.execute({
model: "default",
body: { messages: [{ role: "user", content: "hi" }] },
stream: true,
credentials: { apiKey: "test-user-token-active-refresh" },
signal: AbortSignal.timeout(10000),
});
assert.ok(validResult.response.ok);
const activeTimer = exec.refreshTimer;
assert.ok(activeTimer);
assert.equal(exec.currentUserToken, "test-user-token-active-refresh");
const invalidResult = await exec.execute({
model: "default",
body: { messages: [{ role: "user", content: "hi" }] },
stream: true,
credentials: {},
signal: AbortSignal.timeout(5000),
});
assert.equal(invalidResult.response.status, 400);
assert.equal(exec.currentUserToken, "test-user-token-active-refresh");
assert.equal(exec.refreshTimer, activeTimer);
} finally {
exec.destroy();
mock.restore();
}
});
test("execute with a new DeepSeek userToken restarts auto-refresh", async () => {
const mock = await mockDeepSeekFlow();
const exec = new DeepSeekWebWithAutoRefreshExecutor({
autoRefresh: true,
sessionRefreshInterval: 60_000,
});
try {
const firstResult = await exec.execute({
model: "default",
body: { messages: [{ role: "user", content: "first" }] },
stream: true,
credentials: { apiKey: "test-user-token-refresh-1" },
signal: AbortSignal.timeout(10000),
});
assert.ok(firstResult.response.ok);
const firstTimer = exec.refreshTimer;
assert.ok(firstTimer);
const secondResult = await exec.execute({
model: "default",
body: { messages: [{ role: "user", content: "second" }] },
stream: true,
credentials: { apiKey: "test-user-token-refresh-2" },
signal: AbortSignal.timeout(10000),
});
assert.ok(secondResult.response.ok);
assert.equal(exec.currentUserToken, "test-user-token-refresh-2");
assert.ok(exec.refreshTimer);
assert.notEqual(exec.refreshTimer, firstTimer);
} finally {
exec.destroy();
mock.restore();
}
});
// ─── Abort handling ──────────────────────────────────────────────────────
test("execute: handles abort signal gracefully", async () => {